Urban48
Urban48

Reputation: 1476

lib for configuration handling in an ergonomic way

I'm writing a common library for loading and handling configuration for my applications,
using the config crate.

Im trying to make it as ergonomic as possible to the user, but can't seem to figure it out.

my lib.rs:

impl RunEnv {
    fn to_string(&self) -> String {
        match self {
            RunEnv::Production => "prod".to_string(),
            RunEnv::Dev => "dev".to_string(),
            RunEnv::Staging => "stag".to_string(),
        }
    }
}

impl FromStr for RunEnv {
    type Err = String;

    fn from_str(s: &str) -> Result<RunEnv, String> {
        match s {
            "dev" => Ok(RunEnv::Dev),
            "stag" => Ok(RunEnv::Staging),
            "prod" => Ok(RunEnv::Production),
            _ => Err(format!("Could not parse {:?}", s)),
        }
    }
}

#[derive(Debug, StructOpt)]
#[structopt(name = "CLI Options", about = "Common CLI options for running applications")]
pub struct Arguments {
    /// Run in a specific environment mode: dev, stag, prod.
    // short and long flags (-e, --env) will be deduced from the field's name
    #[structopt(short, long, default_value = "dev")]
    pub env: RunEnv,
}

pub trait LoadConfig {
    fn new() -> Result<Config, ConfigError>{
        const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME");
        let mut s = Config::new();
        let args = Arguments::from_args();

        let mut conf_path = String::new();
        match args.env {
            RunEnv::Production => {
                conf_path = format!("/path/to/config/{}/config.toml", PACKAGE_NAME);
            }
            RunEnv::Staging => {
                conf_path = format!("/path/to/config/{}/config.toml", PACKAGE_NAME);
            }
            RunEnv::Dev => {
                conf_path = "tests/config.toml".to_string();
            }
        }

        // Start off by merging in the "default" configuration file
        s.merge(File::with_name(&conf_path))?;

        // Add in the current environment
        // Default to 'dev' env  
        s.set("run_mode", args.env.to_string())?;

        Ok(s)
    }
}

In my app:

setting.rs

#[derive(Debug, Deserialize)]
pub struct Server {
    port: u32,
    address: String,
}

#[derive(Debug, Deserialize)]
pub struct Settings {
   server: Server,
   run_mode: Option<String>,
}
impl LoadConfig for Settings {}

main.rs

fn main() {
    let conf: Settings = Settings::new().unwrap().try_into().unwrap();
    println!("{:?}", conf);

And here is my problem, I'm trying to "hide" the.unwrap().try_into().unwrap(); part away so the lib user will only need to define his setting.rs and run let conf: Settings = Settings::new()

If i move .try_into() inside a trait then I'm getting an error i can't find a way to go around:

   |         s.try_into()
   |           ^^^^^^^^ the trait `_IMPL_DESERIALIZE_FOR_Configuration::_serde::Deserialize<'_>` is not implemented for `config::config::Config`

I'm new to rust and probably missing some obvious things

Upvotes: 1

Views: 82

Answers (1)

attdona
attdona

Reputation: 18943

For convention the new function is the way to build an instance and must be an inherent method of the struct, a method available directly on a type.

In your example you are trying to define new as a trait default method. If it was at all possible the signature should be:

pub trait LoadConfig {
  fn new() -> Self {

  }
}

Such trait method is impossible to implement because the trait does not know nothing about Self concrete type.

To follow this convention it is best to rename the LoadConfig::new trait method to something else:

pub trait LoadConfig {
  fn load() -> Result<Config, ConfigError>;
}

Then implement the new constructor function as an inherent method, for example:

impl Settings {

    fn new() -> Settings {
        let config = Settings::load().unwrap(); // TBD: manage errors

        let port: u32 = config.get("port").unwrap_or("8080").parse().unwrap();

        Settings {
            server: Server {
                port: port,
                address: config.get("address").unwrap_or("localhost").to_owned()
            },
            run_mode: None
        }
    }
}

Note that a robust implementation should not unwrap but manage more explicitly config errors.

Upvotes: 1

Related Questions