Skip to content

Commit

Permalink
[#25] Provide more detailed TOML decoding errors (#122)
Browse files Browse the repository at this point in the history
Fixes #25 

Provide more detailed TOML decoding errors by adding `DecodeError` enum
to represent different error types.

Author's questions:

> Does the toml crate provide better decoding than as_str() and friends?
All such functions return Option<...> while it would be better to have
Result<..., ...>

I couldn't find better decoding than as_str() but I did map `Option<>`
to `Result<_, DecodeError>` so I guess dealing with Option is fine?

### Additional tasks

- [ ] Documentation for changes provided/changed
- [x] Tests added
- [ ] Updated CHANGELOG.md
  • Loading branch information
DukeManh authored Oct 9, 2022
1 parent 33c34a9 commit f345444
Showing 1 changed file with 100 additions and 18 deletions.
118 changes: 100 additions & 18 deletions src/config/toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,53 @@ use crate::infra::err;
use crate::model::asset_name::AssetName;
use crate::model::os::OS;

#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum TomlError {
IO(String),
Parse(toml::de::Error),
Decode,
Decode(DecodeError),
}

#[derive(Debug, PartialEq)]
pub enum DecodeError {
MissingKey {
key: String,
},
InvalidType {
key: String,
expected: Value,
found: Value,
},
}

impl Display for TomlError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TomlError::IO(e) => write!(f, "[IO Error] {}", e),
TomlError::Parse(e) => write!(f, "[Parsing Error] {}", e),
TomlError::Decode => write!(f, "[Decode Error]"),
TomlError::Decode(e) => write!(f, "[Decode Error] {}", e),
}
}
}

impl Display for DecodeError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
DecodeError::MissingKey { key } => {
write!(f, "The key '{}' is missing (a typo or not specified?)", key)
}
DecodeError::InvalidType {
key,
expected,
found,
} => write!(
f,
"unexpected value type '{}={}': expected `{}`, found `{}`",
key,
found,
expected.type_str(),
found.type_str()
),
}
}
}
Expand Down Expand Up @@ -55,30 +89,44 @@ fn parse_string(contents: &str, proxy: Option<String>) -> Result<Config, TomlErr
contents
.parse::<Value>()
.map_err(TomlError::Parse)
.and_then(|toml| match decode_config(toml, proxy) {
None => Err(TomlError::Decode),
Some(config) => Ok(config),
})
.and_then(|toml| decode_config(toml, proxy).map_err(TomlError::Decode))
}

fn decode_config(toml: Value, proxy: Option<String>) -> Option<Config> {
let str_store_directory = toml.get("store_directory")?.as_str()?;
fn decode_config(toml: Value, proxy: Option<String>) -> Result<Config, DecodeError> {
let str_store_directory = toml.get("store_directory");

let store_directory = str_store_directory.map_or(
Err(DecodeError::MissingKey {
key: String::from("store_directory"),
}),
|directory_config| match directory_config {
Value::String(directory) => Ok(directory.clone()),
other => Err(DecodeError::InvalidType {
key: String::from("store_directory"),
expected: Value::String("some_value".into()),
found: other.clone(),
}),
},
)?;

let proxy: Option<String> = proxy.or_else(|| match toml.get("proxy") {
Some(p) => Some(p.as_str().unwrap_or("").into()),
None => None,
});

let store_directory = String::from(str_store_directory);

let mut tools = BTreeMap::new();

for (key, val) in toml.as_table()?.iter() {
let table = toml
.as_table()
.expect("unable to parse config file to a table");

for (key, val) in table.iter() {
if let Value::Table(table) = val {
tools.insert(key.clone(), decode_config_asset(table, &proxy));
}
}

Some(Config {
Ok(Config {
store_directory,
tools,
proxy,
Expand Down Expand Up @@ -169,8 +217,25 @@ mod tests {

#[test]
fn test_toml_error_display_decode() {
let toml_error = TomlError::Decode;
assert_eq!(String::from("[Decode Error]"), toml_error.to_string());
let toml_error = TomlError::Decode(DecodeError::MissingKey {
key: String::from("store_directory"),
});
assert_eq!(
String::from(
"[Decode Error] The key 'store_directory' is missing (a typo or not specified?)"
),
toml_error.to_string()
);

let toml_error = TomlError::Decode(DecodeError::InvalidType {
key: String::from("store_directory"),
expected: Value::String("some_value".into()),
found: Value::Integer(32),
});
assert_eq!(
String::from("[Decode Error] unexpected value type 'store_directory=32': expected `string`, found `integer`"),
toml_error.to_string()
);
}

#[test]
Expand Down Expand Up @@ -203,23 +268,40 @@ mod tests {
let toml = "";
let res = parse_string(toml, None);

assert_eq!(res, Err(TomlError::Decode));
assert_eq!(
res,
Err(TomlError::Decode(DecodeError::MissingKey {
key: String::from("store_directory")
}))
);
}

#[test]
fn store_directory_is_dotted() {
let toml = "store.directory = \"pancake\"";
let res = parse_string(toml, None);

assert_eq!(res, Err(TomlError::Decode));
assert_eq!(
res,
Err(TomlError::Decode(DecodeError::MissingKey {
key: String::from("store_directory")
}))
);
}

#[test]
fn store_directory_is_a_number() {
let toml = "store_directory = 42";
let res = parse_string(toml, None);

assert_eq!(res, Err(TomlError::Decode));
assert_eq!(
res,
Err(TomlError::Decode(DecodeError::InvalidType {
key: String::from("store_directory"),
expected: Value::String("some_value".into()),
found: Value::Integer(42)
}))
);
}

#[test]
Expand Down

0 comments on commit f345444

Please sign in to comment.