Reputation: 1476
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
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