Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a better way to initialize struct from entries of a HashMap?

Tags:

rust

I have a struct for connection info of PostgreSQL:

struct PgConf {
    host: String,
    port: i16,
    user: String,
    passwd: String,
    dbname: String
}

for now I have to init it from a HashMap<String, String>:

impl PgConf {
    fn init(&mut self, args: &ArgList) -> () {
        if let Some(val) = args.get("host") {
            self.host = val.clone();
        }
        if let Some(val) = args.get("port") {
            self.port = val.parse().unwrap_or(5432);
        }
        if let Some(val) = args.get("user") {
            self.user = val.clone();
        }
        if let Some(val) = args.get("passwd") {
            self.passwd = val.clone();
        }
        if let Some(val) = args.get("db") {
            self.dbname = val.clone();
        }
    }
}

I think the above code is a bit bloated. Is there a better way to achieve this?

like image 598
kyleqian Avatar asked Dec 31 '25 21:12

kyleqian


2 Answers

If you want a nicely cursed alternative, you could round-trip through a type-erased serde implementation e.g. serde_json, then it's just a matter of:

fn convert(h: HashMap<String, String>) -> Option<PgConf> {
    serde_json::to_value(h).ok()
        .and_then(|v| PgConf::deserialize(v).ok())
}

though that requires some support, annotating the struct to start with:

#[derive(Deserialize)]
struct PgConf {
    host: String,
    #[serde(
        default = "default_port",
        deserialize_with = "deserialize_number_from_string"
    )]
    port: u16,
    user: String,
    passwd: String,
    dbname: String,
}

and then implementing the relevant hook to get the processing you need on the port:

fn default_port() -> u16 {
    5432
}
/// copied from serde_aux as the playground doesn't have it
pub fn deserialize_number_from_string<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: FromStr + serde::Deserialize<'de>,
    <T as FromStr>::Err: Display,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum StringOrInt<T> {
        String(String),
        Number(T),
    }

    match StringOrInt::<T>::deserialize(deserializer)? {
        StringOrInt::String(s) => s.parse::<T>().map_err(serde::de::Error::custom),
        StringOrInt::Number(i) => Ok(i),
    }
}

There are cases where this can make sense, mostly situations where you're already using serde for that struct so all the "support" part is already there, and you just need to leverage it. It would not recommend it otherwise as it should be... less than ideally efficient. But it's one of those things you're glad to think of when they do apply.

like image 63
Masklinn Avatar answered Jan 04 '26 11:01

Masklinn


If you don't want to use serde, as @Masklinn suggested (for example, you don't want to introduce a dependency), and the behavior change in the code provided by @Simson is unacceptable, a simple macro can simplify the work a bit:

struct PgConf {
    host: String,
    port: i16,
    user: String,
    passwd: String,
    db: String, // Changed from `dbname` to simplify.
}

impl PgConf {
    fn init(&mut self, args: &HashMap<String, String>) {
        macro_rules! assign {
            ( $var:ident = $default:expr ) => {
                if let Some(val) = args.get(stringify!($var)) {
                    self.$var = val.parse().unwrap_or_else(|_| $default);
                }
            };
            ( $var:ident ) => { assign!($var = unreachable!("no default provided")) };
        }
        
        assign!(host);
        assign!(port = 5432);
        assign!(user);
        assign!(passwd);
        assign!(db);
    }
}

This macro is less type-safe since you can forget to add a default value to non-String entires and it will silently pass and panic at runtime. This can be improved, at the expense of making the macro longer.

like image 43
Chayim Friedman Avatar answered Jan 04 '26 12:01

Chayim Friedman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!