canonical way to treat enums with various representations in rust

Viewed 106

I am using an enum for each continent in my Rust program. Asia, Europe, etc. However this enum corresponds to an integer in the database definition, it can be constructed from an external source and can be transformed to another "type" if requested.

For example

enum ContinentKind {
    Asia,
    Europe,
    Africa,
    America,
    Oceania,
}

impl ContinentKind {
    fn new(external_input: &str) -> Result<ContinentKind, String> {
        match external_input {
            "ASIA" => Ok(ContinentKind::Asia),
            "EUROPE" => Ok(ContinentKind::Europe),
            "AFRICA" => Ok(ContinentKind::Africa),
            "AMERICA" => Ok(ContinentKind::America),
            "OCEANIA" => Ok(ContinentKind::Oceania),
            _ => Err("Wrong external input".to_string()),
        }
    }

    fn id(&self) -> u32 {
        match *self {
            ContinentKind::Asia => 1,
            ContinentKind::Europe => 2,
            ContinentKind::Africa => 3,
            ContinentKind::America => 4,
            ContinentKind::Oceania => 5,
        }
    }

    fn api_string(&self) -> String {
        match *self {
            ContinentKind::Asia => String::from("I love Asia"),
            ContinentKind::Europe => String::from("I travel to Europe"),
            ContinentKind::Africa => String::from("Hello Africa"),
            ContinentKind::America => String::from("North and South America"),
            ContinentKind::Oceania => String::from("O C E A N I A"),
        }
    }
}

So I can use ContinentKind::Asia in most of the time, but I am using the id(&self) method to take an integer and save to database, or api_string(&self) to return a string in my http server.

Each continent, has a type, an integer, an external definition and a description string.

Asia, "ASIA", 1, "I love Asia"
Europe, "EUROPE", 2 "I travel to Europe"
Africa, "AFRICA", 3, "Hello Africa"
America, "AMERICA", 4, "North and South America"
Oceania "OCEANIA", 5, "O C E A N I A"

With the first glance the code works when I am calling, but I was wondering if there is a better way to do it, that takes less code.

let my_type = ContinentKind::America;
println!("{}", my_type.id());
println!("{}", my_type.api_string());

let another_type = ContinentKind::new("AFRICA");

match another_type {
    Ok(v)=> println!("{}", v.id()),
    _ => println!("an error happend"),
}

And what I mean by that is to finally create a struct like this:

struct Continent {
    id: u32,
    kind: ContinentKind,
    externaL_str : String,
    internal_str: String,
}

that has 2 constructs, from_id or from_external_str and have the type nested.

For example:

#[derive(Debug)]
struct Continent {
    id: u32,
    kind: ContinentKind,
    external_str : String,
    internal_str: String,
}

impl Continent {

    fn from_external_string(external_input: &str) -> Result<Continent, String> {

        match external_input {

            "ASIA" => Ok(Continent{id: 1, kind:ContinentKind::Asia, external_str: String::from("ASIA"), internal_str:String::from("I love Asia")}),
            "EUROPE" => Ok(Continent{id: 2, kind:ContinentKind::Europe, external_str: String::from("EUROPE"), internal_str:String::from("I travel to Europe")}),
            "AFRICA" => Ok(Continent{id: 3, kind:ContinentKind::Africa, external_str: String::from("AFRICA"), internal_str:String::from("Hello Africa")}),
            "AMERICA" => Ok(Continent{id: 4, kind:ContinentKind::America, external_str: String::from("AMERICA"), internal_str:String::from("North and South America")}),
            "OCEANIA" => Ok(Continent{id: 5, kind:ContinentKind::Oceania, external_str: String::from("OCEANIA"), internal_str:String::from("O C E A N I A")}),
            _ => Err("Wrong external input".to_string()),
        }
    }

    fn from_database_id(id: u32) -> Result<Continent, String> {

        match id {

            1 => Ok(Continent{id: 1, kind:ContinentKind::Asia, external_str: String::from("ASIA"), internal_str:String::from("I love Asia")}),
            2 => Ok(Continent{id: 2, kind:ContinentKind::Europe, external_str: String::from("EUROPE"), internal_str:String::from("I travel to Europe")}),
            3 => Ok(Continent{id: 3, kind:ContinentKind::Africa, external_str: String::from("AFRICA"), internal_str:String::from("Hello Africa")}),
            4 => Ok(Continent{id: 4, kind:ContinentKind::America, external_str: String::from("AMERICA"), internal_str:String::from("North and South America")}),
            5 => Ok(Continent{id: 5, kind:ContinentKind::Oceania, external_str: String::from("OCEANIA"), internal_str:String::from("O C E A N I A")}),
            _ => Err("Wrong external input".to_string()),
        }
    }
}
2 Answers

One way that you can clean this up a bit is to use lazy_static to put your values into static global values. The core of it looks like this:

lazy_static! {
    static ref ASIA: Continent = Continent::new(1, ContinentKind::Asia, "ASIA", "I love Asia");
    static ref EUROPE: Continent = Continent::new(2, ContinentKind::Europe, "EUROPE", "I travel to Europe");
    // ...
}

You can then build your Continent::from_external_string and Continent::from_database_id to return references to these (or copies if you really need values rather than references).

impl Continent {
    fn from_external_string(external_input: &str) -> Result<&'static Continent, String> {
        match external_input {
            "ASIA" => Ok(&*ASIA),
            "EUROPE" => Ok(&*EUROPE),
            // ...
            _ => Err("Wrong external input".to_string()),
        }
    }

    fn from_database_id(id: u32) -> Result<&'static Continent, String> {
        match id {
            1 => Ok(&*ASIA),
            2 => Ok(&*EUROPE),
            // ...
            _ => Err("Wrong external input".to_string()),
        }
    }
}

And finally, if you still need them, you can hook up the enum functions to use these too:

impl ContinentKind {
    fn to_continent(&self) -> &'static Continent {
        match self {
            ContinentKind::Asia => &*ASIA,
            ContinentKind::Europe => &*EUROPE,
            // ...
        }
    }
    fn id(&self) -> u32 {
        self.to_continent().id
    }
    // ...
}

NOTE: In general I don't like using global to manage this kind of thing, but if you really want to use hardcoded enums this at least minimizes the bits you need to keep in sync.

For the numerical coversations, you can specify these inline:

#[repr(u32)]
enum Continent {
  Asia = 1,
  Europe = 2,
  ...
}

You can then use a crate like num_enum to derive From/TryFrom implementations for you. Similarly, you could use a crate like strum to derive the string conversions.

These have the advantage over the structs you describe of not taking up extra memory storing redundant copies of information, at the expense of needing to perform matches at runtime (which have some overhead) instead of simple struct field lookups.

Related