DurableConfig
is a struct that persists itself to a file. I want to mock DurableConfig::write
for a test so instead of writing to a file, it makes assertions about parameters that were passed in. From what I've read, the standard way to do this is to use traits.
Here is what I've tried. This code does not compile.
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
#[derive(Debug, Default, PartialEq)]
pub struct DurableConfig {
file_path: PathBuf,
pub values: ConfigValues,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub struct ConfigValues {
pub foo: Option<String>,
pub bar: Option<String>,
pub baz: Option<String>,
}
impl From<PathBuf> for DurableConfig {
fn from(path: PathBuf) -> DurableConfig {
let content = match fs::read_to_string(&path) {
Ok(text) => text,
Err(_) => String::from(""),
};
DurableConfig {
file_path: path,
values: ConfigValues::from(content),
}
}
}
impl From<String> for ConfigValues {
fn from(s: String) -> ConfigValues {
toml::from_str(&s).unwrap()
}
}
pub trait Config: std::fmt::Debug {
fn merge(self, other: ConfigValues) -> Self;
fn write(self) -> Result<(), Box<dyn Error>>;
}
impl Config for DurableConfig {
fn merge(self, other: ConfigValues) -> DurableConfig {
DurableConfig {
file_path: self.file_path,
values: self.values.merge(other),
}
}
fn write(self: DurableConfig) -> Result<(), Box<dyn Error>> {
let content = self.values.to_string();
let file_path = self.file_path;
let mut file = match File::create(&file_path) {
Err(_) => {
fs::create_dir(&file_path.parent().unwrap())?;
File::create(&file_path).unwrap()
}
Ok(file) => file,
};
match file.write_all(content.as_bytes()) {
Err(e) => panic!("Could not write to {:?}: {}", file_path, e),
Ok(_) => Ok(()),
}
}
}
impl ConfigValues {
pub fn to_string(self) -> String {
toml::to_string(&self).unwrap()
}
pub fn merge(self, other: ConfigValues) -> ConfigValues {
ConfigValues {
foo: match other.foo {
Some(value) => Some(value),
None => self.foo,
},
bar: match other.bar {
Some(value) => Some(value),
None => self.bar,
},
baz: match other.baz {
Some(value) => Some(value),
None => self.baz,
},
}
}
}
struct ConfigArgs {
pub key: String,
pub value: Option<String>,
}
fn get_or_set_config_value<T: Config>(
args: ConfigArgs,
config: T,
) -> Result<(), Box<dyn Error>> {
match args.value {
Some(value) => {
let new_values = match &args.key[..] {
"foo" => ConfigValues {
foo: Some(value),
..ConfigValues::default()
},
"bar" => ConfigValues {
bar: Some(value),
..ConfigValues::default()
},
"baz" => ConfigValues {
baz: Some(value),
..ConfigValues::default()
},
_ => panic!("Unknown key: {}", args.key),
};
config.merge(new_values).write()?;
Ok(())
}
None => {
let value = match &args.key[..] {
"foo" => config.values.foo.unwrap(),
"bar" => config.values.bar.unwrap(),
"baz" => config.values.baz.unwrap(),
_ => panic!("Unknown key: {}", args.key),
};
println!("{}", value);
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
use std::path::PathBuf;
#[test]
fn should_write_config_key_when_config_is_empty() {
#[derive(Debug, Default, PartialEq)]
struct MockConfig {
file_path: PathBuf,
values: ConfigValues,
}
impl Config for MockConfig {
fn merge(self, other: ConfigValues) -> MockConfig {
MockConfig {
file_path: self.file_path,
values: self.values.merge(other),
}
}
fn write(self: MockConfig) -> Result<(), Box<dyn Error>> {
assert_eq!(self.values.foo, Some("foo".to_string()));
assert_eq!(self.values.bar, None);
assert_eq!(self.values.baz, None);
Ok(())
}
}
let config = MockConfig {
file_path: PathBuf::from(""),
values: ConfigValues {
foo: None,
bar: None,
baz: None,
},
};
let args = ConfigArgs {
key: String::from("foo"),
value: Some(String::from("foo")),
};
let _ = get_or_set_config_value(args, config).unwrap();
}
}
Here are some of the compiler errors. Suggestions?
error[E0609]: no field `values` on type `T`
--> cli/src/foo.rs:130:33
|
102 | fn get_or_set_config_value<T: Config>(
| - type parameter 'T' declared here
...
130 | "foo" => config.values.foo.unwrap(),
| ^^^^^^
error[E0609]: no field `values` on type `T`
--> cli/src/foo.rs:131:33
|
102 | fn get_or_set_config_value<T: Config>(
| - type parameter 'T' declared here
...
131 | "bar" => config.values.bar.unwrap(),
| ^^^^^^
error[E0609]: no field `values` on type `T`
--> cli/src/foo.rs:132:33
|
102 | fn get_or_set_config_value<T: Config>(
| - type parameter 'T' declared here
...
132 | "baz" => config.values.baz.unwrap(),
| ^^^^^^
error[E0609]: no field `values` on type `T`
--> cli/src/foo.rs:130:33
|
102 | fn get_or_set_config_value<T: Config>(
| - type parameter 'T' declared here
...
130 | "foo" => config.values.foo.unwrap(),
| ^^^^^^
error[E0609]: no field `values` on type `T`
--> cli/src/foo.rs:131:33
|
102 | fn get_or_set_config_value<T: Config>(
| - type parameter 'T' declared here
...
131 | "bar" => config.values.bar.unwrap(),
| ^^^^^^
error: aborting due to 3 previous errors
For more information about this error, try `rustc --explain E0609`.
error[E0609]: no field `values` on type `T`
--> cli/src/foo.rs:132:33
|
102 | fn get_or_set_config_value<T: Config>(
| - type parameter 'T' declared here
...
132 | "baz" => config.values.baz.unwrap(),
| ^^^^^^