Skip to content

Commit

Permalink
fix: Support Yaml values
Browse files Browse the repository at this point in the history
- Required switching to `serde_yaml`.
- Like with `yaml-rust`, requires special handling for table keys.

Signed-off-by: Brennan Kinney <[email protected]>
  • Loading branch information
polarathene committed Oct 10, 2023
1 parent e597f6a commit d4f2f35
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 102 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ maintenance = { status = "actively-developed" }
[features]
default = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"]
json = ["serde_json"]
yaml = ["yaml-rust"]
yaml = ["serde_yaml"]
ini = ["rust-ini"]
json5 = ["json5_rs", "serde/derive"]
convert-case = ["convert_case"]
Expand All @@ -33,7 +33,7 @@ nom = "7"
async-trait = { version = "0.1.50", optional = true }
toml = { version = "0.8", optional = true }
serde_json = { version = "1.0.2", optional = true }
yaml-rust = { version = "0.4", optional = true }
serde_yaml = { version = "0.9", optional = true }
rust-ini = { version = "0.19", optional = true }
ron = { version = "0.8", optional = true }
json5_rs = { version = "0.4", optional = true, package = "json5" }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

[JSON]: https://github.com/serde-rs/json
[TOML]: https://github.com/toml-lang/toml
[YAML]: https://github.com/chyh1990/yaml-rust
[YAML]: https://github.com/dtolnay/serde-yaml
[INI]: https://github.com/zonyitoo/rust-ini
[RON]: https://github.com/ron-rs/ron
[JSON5]: https://github.com/callum-oakley/json5-rs
Expand Down
2 changes: 1 addition & 1 deletion src/file/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub enum FileFormat {
#[cfg(feature = "json")]
Json,

/// YAML (parsed with yaml_rust)
/// YAML (parsed with serde_yaml)
#[cfg(feature = "yaml")]
Yaml,

Expand Down
97 changes: 3 additions & 94 deletions src/file/format/yaml.rs
Original file line number Diff line number Diff line change
@@ -1,105 +1,14 @@
use std::error::Error;
use std::fmt;
use std::mem;

use yaml_rust as yaml;

use crate::format;
use crate::map::Map;
use crate::value::{Value, ValueKind};
use crate::value::Value;

pub fn parse(
uri: Option<&String>,
text: &str,
) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
// Parse a YAML object from file
let mut docs = yaml::YamlLoader::load_from_str(text)?;
let root = match docs.len() {
0 => yaml::Yaml::Hash(yaml::yaml::Hash::new()),
1 => mem::replace(&mut docs[0], yaml::Yaml::Null),
n => {
return Err(Box::new(MultipleDocumentsError(n)));
}
};

let value = from_yaml_value(uri, &root)?;
// Parse a YAML input from the provided text
let value = format::from_parsed_value(uri, serde_yaml::from_str(text)?);
format::extract_root_table(uri, value)
}

fn from_yaml_value(
uri: Option<&String>,
value: &yaml::Yaml,
) -> Result<Value, Box<dyn Error + Send + Sync>> {
match *value {
yaml::Yaml::String(ref value) => Ok(Value::new(uri, ValueKind::String(value.clone()))),
yaml::Yaml::Real(ref value) => {
// TODO: Figure out in what cases this can panic?
value
.parse::<f64>()
.map_err(|_| {
Box::new(FloatParsingError(value.to_string())) as Box<(dyn Error + Send + Sync)>
})
.map(ValueKind::Float)
.map(|f| Value::new(uri, f))
}
yaml::Yaml::Integer(value) => Ok(Value::new(uri, ValueKind::I64(value))),
yaml::Yaml::Boolean(value) => Ok(Value::new(uri, ValueKind::Boolean(value))),
yaml::Yaml::Hash(ref table) => {
let mut m = Map::new();
for (key, value) in table {
match key {
yaml::Yaml::String(k) => m.insert(k.to_owned(), from_yaml_value(uri, value)?),
yaml::Yaml::Integer(k) => m.insert(k.to_string(), from_yaml_value(uri, value)?),
_ => unreachable!(),
};
}
Ok(Value::new(uri, ValueKind::Table(m)))
}
yaml::Yaml::Array(ref array) => {
let mut l = Vec::new();

for value in array {
l.push(from_yaml_value(uri, value)?);
}

Ok(Value::new(uri, ValueKind::Array(l)))
}

// 1. Yaml NULL
// 2. BadValue – It shouldn't be possible to hit BadValue as this only happens when
// using the index trait badly or on a type error but we send back nil.
// 3. Alias – No idea what to do with this and there is a note in the lib that its
// not fully supported yet anyway
_ => Ok(Value::new(uri, ValueKind::Nil)),
}
}

#[derive(Debug, Copy, Clone)]
struct MultipleDocumentsError(usize);

impl fmt::Display for MultipleDocumentsError {
fn fmt(&self, format: &mut fmt::Formatter) -> fmt::Result {
write!(format, "Got {} YAML documents, expected 1", self.0)
}
}

impl Error for MultipleDocumentsError {
fn description(&self) -> &str {
"More than one YAML document provided"
}
}

#[derive(Debug, Clone)]
struct FloatParsingError(String);

impl fmt::Display for FloatParsingError {
fn fmt(&self, format: &mut fmt::Formatter) -> fmt::Result {
write!(format, "Parsing {} as floating point number failed", self.0)
}
}

impl Error for FloatParsingError {
fn description(&self) -> &str {
"Floating point number parsing failed"
}
}
40 changes: 40 additions & 0 deletions src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub enum ParsedValue {
Float(f64),
#[serde(deserialize_with = "deserialize_parsed_string")]
String(String),
#[serde(deserialize_with = "deserialize_parsed_map")]
Table(Map<String, Self>),
Array(Vec<Self>),
Option(Option<Box<Self>>),
Expand Down Expand Up @@ -139,3 +140,42 @@ where
},
}
}

fn deserialize_parsed_map<'de, D>(deserializer: D) -> Result<Map<String, ParsedValue>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum ParsedMap {
// Anything that can deserialize into a Map successfully:
Table(Map<String, ParsedValue>),
// Config specific support for types that need string conversion:
#[cfg(feature = "yaml")]
YamlMap(serde_yaml::Mapping),
}

match ParsedMap::deserialize(deserializer)? {
ParsedMap::Table(v) => Ok(v),
#[cfg(feature = "yaml")]
ParsedMap::YamlMap(table) => {
table
.into_iter()
.map(|(key, value)| {
let key = match key {
serde_yaml::Value::Number(k) => Some(k.to_string()),
serde_yaml::Value::String(k) => Some(k),
_ => None,
};
let value = serde_yaml::from_value::<ParsedValue>(value).ok();

// Option to Result:
match (key, value) {
(Some(k), Some(v)) => Ok((k, v)),
_ => Err(serde::de::Error::custom("should not be serialized to Map")),
}
})
.collect()
}
}
}
5 changes: 3 additions & 2 deletions tests/file_yaml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,13 @@ fn test_error_parse() {

let path_with_extension: PathBuf = ["tests", "Settings-invalid.yaml"].iter().collect();

// Should fail to parse block mapping as no `:` exists to identify a key
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
format!(
"while parsing a block mapping, did not find expected key at \
line 2 column 1 in {}",
"could not find expected ':' at line 3 column 1, \
while scanning a simple key at line 2 column 1 in {}",
path_with_extension.display()
)
);
Expand Down
5 changes: 3 additions & 2 deletions tests/legacy/file_yaml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,13 @@ fn test_error_parse() {

let path_with_extension: PathBuf = ["tests", "Settings-invalid.yaml"].iter().collect();

// Should fail to parse block mapping as no `:` exists to identify a key
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
format!(
"while parsing a block mapping, did not find expected key at \
line 2 column 1 in {}",
"could not find expected ':' at line 3 column 1, \
while scanning a simple key at line 2 column 1 in {}",
path_with_extension.display()
)
);
Expand Down

0 comments on commit d4f2f35

Please sign in to comment.