1use indexmap::IndexMap;
16use yaml_rust2::{Yaml, YamlLoader};
17
18use super::{config_from_object, AdapterError};
19use crate::value::{HoconValue, ScalarValue};
20use crate::Config;
21
22pub fn parse(input: &str, origin: Option<&str>) -> Result<Config, AdapterError> {
24 let docs = YamlLoader::load_from_str(super::strip_bom(input))
25 .map_err(|e| AdapterError::new(format!("yaml: {e}")))?;
26 if docs.len() > 1 {
27 return Err(AdapterError::new(
28 "yaml: multi-document streams are not supported (spec F5.7); a config is one document",
29 ));
30 }
31 match docs.into_iter().next() {
32 None => Ok(config_from_object(
35 HoconValue::Object(IndexMap::new()),
36 origin,
37 )),
38 Some(doc) => from_value(&doc, origin),
39 }
40}
41
42pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Config, AdapterError> {
44 let path = path.as_ref();
45 let text = std::fs::read_to_string(path)
46 .map_err(|e| AdapterError::new(format!("yaml: {}: {e}", path.display())))?;
47 parse(&text, Some(&path.display().to_string()))
48}
49
50pub fn from_value(doc: &Yaml, origin: Option<&str>) -> Result<Config, AdapterError> {
54 if matches!(doc, Yaml::Null | Yaml::BadValue) {
55 return Ok(config_from_object(
56 HoconValue::Object(IndexMap::new()),
57 origin,
58 ));
59 }
60 let Yaml::Hash(_) = doc else {
61 return Err(AdapterError::new(format!(
62 "yaml: document root is {}, but a config root must be a mapping (spec F0.3)",
63 describe(doc)
64 )));
65 };
66 let value = convert(doc, "")?;
67 Ok(config_from_object(value, origin))
68}
69
70fn describe(v: &Yaml) -> &'static str {
71 match v {
72 Yaml::Array(_) => "a sequence",
73 Yaml::Hash(_) => "a mapping",
74 Yaml::Null => "null",
75 _ => "a scalar",
76 }
77}
78
79fn convert(v: &Yaml, at: &str) -> Result<HoconValue, AdapterError> {
80 match v {
81 Yaml::Hash(h) => {
82 let mut out: IndexMap<String, HoconValue> = IndexMap::new();
83 let mut merged: IndexMap<String, HoconValue> = IndexMap::new();
88 for (k, e) in h {
89 if matches!(k, Yaml::String(s) if s == "<<") {
90 collect_merge(e, at, &mut merged)?;
91 continue;
92 }
93 let ks = key_string(k, at)?;
94 let path = if at.is_empty() {
95 ks.clone()
96 } else {
97 format!("{at}.{ks}")
98 };
99 out.insert(ks, convert(e, &path)?);
100 }
101 for (k, e) in merged {
102 out.entry(k).or_insert(e);
103 }
104 Ok(HoconValue::Object(out))
105 }
106 Yaml::Array(items) => {
107 let mut out = Vec::with_capacity(items.len());
108 for (i, e) in items.iter().enumerate() {
109 out.push(convert(e, &format!("{at}[{i}]"))?);
110 }
111 Ok(HoconValue::Array(out))
112 }
113 Yaml::String(s) => Ok(HoconValue::Scalar(ScalarValue::string(s.clone()))),
114 Yaml::Boolean(b) => Ok(HoconValue::Scalar(ScalarValue::boolean(*b))),
115 Yaml::Integer(i) => Ok(HoconValue::Scalar(ScalarValue::number(i.to_string()))),
116 Yaml::Real(raw) => {
117 let lower = raw.to_ascii_lowercase();
120 if lower.ends_with(".nan") || lower.ends_with(".inf") {
121 return Err(AdapterError::new(format!(
122 "yaml: at {at}: {raw} is not representable in HOCON (spec F0.6)"
123 )));
124 }
125 let f: f64 = raw.parse().map_err(|_| {
126 AdapterError::new(format!("yaml: at {at}: {raw:?} is not a number"))
127 })?;
128 if f.is_nan() || f.is_infinite() {
129 return Err(AdapterError::new(format!(
130 "yaml: at {at}: {raw} is not representable in HOCON (spec F0.6)"
131 )));
132 }
133 Ok(HoconValue::Scalar(ScalarValue::number(raw.clone())))
134 }
135 Yaml::Null => Ok(HoconValue::Scalar(ScalarValue::null())),
136 Yaml::Alias(_) | Yaml::BadValue => Err(AdapterError::new(format!(
137 "yaml: at {at}: unresolved node in the decoded tree"
138 ))),
139 }
140}
141
142fn collect_merge(
143 e: &Yaml,
144 at: &str,
145 into: &mut IndexMap<String, HoconValue>,
146) -> Result<(), AdapterError> {
147 match e {
148 Yaml::Hash(_) => {
149 if let HoconValue::Object(fields) = convert(e, at)? {
150 for (k, v) in fields {
151 into.entry(k).or_insert(v);
152 }
153 }
154 Ok(())
155 }
156 Yaml::Array(items) => {
158 for item in items {
159 collect_merge(item, at, into)?;
160 }
161 Ok(())
162 }
163 _ => Err(AdapterError::new(format!(
164 "yaml: at {at}: a merge key must reference a mapping (spec F5.2)"
165 ))),
166 }
167}
168
169fn key_string(k: &Yaml, at: &str) -> Result<String, AdapterError> {
172 match k {
173 Yaml::String(s) => Ok(s.clone()),
174 Yaml::Integer(i) => Ok(i.to_string()),
175 Yaml::Real(r) => Ok(r.clone()),
176 Yaml::Boolean(b) => Ok(b.to_string()),
177 Yaml::Null => Ok("null".to_string()),
178 _ => Err(AdapterError::new(format!(
179 "yaml: at {at}: a collection key is not usable as an object key (spec F5.3)"
180 ))),
181 }
182}