mercredi 26 octobre 2022

Allow pattern destructuring but disallow construction

Suppose I have the following src/lib.rs

pub enum Toppings {
    Almond,
    Caramel,
    Cookie,
    Marshmallow,
    ChocollateSprinkles,
}
pub enum Icecream {
    Chocollate(Toppings),
    Mint(Toppings),
    Vanilla(Toppings),
}

I want to allow pattern destructure s.t. people can use the library like any other enum with associated data:

use Icecream::*;
use Toppings::*;

fn order_how_many(icecream: Icecream) -> usize {
    match icecream {
        Chocollate(Marshmallow) => 42,
        Vanilla(Cookie) => 69,
        _ => 42069,
    }
}

But at the same time I want to forbid creation of certain combinations of enum and its associated data, e.g. maybe the local ice cream store thinks double chocolate is too much and would never sell Icecream::Chocollate(Toppings::ChocollateSprinkles), so we should outright forbid any possibility this combination is constructed. I find this hard to implement in Rust, since I need pub enum to allow pattern destruction, but making the enum public means all of its variants are pub.

I tried private token similar to which sometimes could be found in sealed trait pattern, using private modules and prevents any accidental pub use via #[forbid(missing_docs)], s.t. only crate implementation can decide what Icecream/Toppings combination are possible, but this makes pattern matching ugly (require _ or .. in every pattern destruction).

mod icecream {
    // Intentionally NOT use /// comment for this mod
    // to prevent accidental `pub use`
    #[forbid(missing_docs)]
    mod hide {
        pub struct Hide;
    }
    pub enum Toppings {
        Almond,
        Caramel,
        Cookie,
        Marshmallow,
        ChocollateSprinkles,
    }
    pub enum Icecream {
        Chocollate(Toppings, hide::Hide),
        Mint(Toppings, hide::Hide),
        Vanilla(Toppings, hide::Hide),
    }
    pub use Icecream::*;
    pub use Toppings::*;
}

pub use icecream::*;
fn order_how_many(icecream: Icecream) -> usize { // OK
    match icecream {
        Chocollate(Marshmallow, _) => 42,
        Vanilla(Cookie, ..) => 69,
        _ => 42069,
    }
}
fn str_to_icecream(base: &str, topping: &str) -> Option<Icecream> { // Compile fail as intended
    if base.to_lowercase() == "chocollate" && topping.to_lowercase() == "chocollatesprinkles" {
        Some(Chocollate(ChocollateSprinkles, icecream::hide::Hide))
    } else {
        None
    }
}

Before posting the question SO suggested me this, but it also doesn't seem to fix the problem of enum here, since enum unlike struct cannot have different visibility between the type itself and its associated members, and that this would make having different shape of associated data more cumbersome to implement, e.g. if the length of tuples are different I'd probably have to implement a trait and return trait objects if I were to use this method.

Is there a more elegant/natural/rustacean way to do this? Or one should outright try avoid such code at the beginning, maybe since it's deemed as a bad practice?

Aucun commentaire:

Enregistrer un commentaire