1use crate::Result;
4use hashlink::linked_hash_map;
5use saphyr::{MarkedYaml, Scalar, YamlData};
6use std::borrow::Cow;
7use std::collections::HashMap;
8use std::hash::Hash;
9
10pub fn hash_map<K, V>(key: K, value: V) -> HashMap<K, V>
12where
13 K: Hash + Eq + Clone,
14{
15 let mut hash_map = HashMap::with_capacity(1);
16 hash_map.insert(key, value);
17 hash_map
18}
19
20pub fn linked_hash_map<K, V>(key: K, value: V) -> linked_hash_map::LinkedHashMap<K, V>
22where
23 K: Hash + Eq + Clone,
24{
25 let mut linked_hash_map = linked_hash_map::LinkedHashMap::new();
26 linked_hash_map.insert(key, value);
27 linked_hash_map
28}
29
30pub const fn saphyr_yaml_string(s: &str) -> saphyr::Yaml<'_> {
32 saphyr::Yaml::Value(saphyr::Scalar::String(Cow::Borrowed(s)))
33}
34
35pub fn try_unwrap_saphyr_scalar<'a>(yaml: &'a saphyr::Yaml) -> Result<&'a saphyr::Scalar<'a>> {
37 if let saphyr::Yaml::Value(scalar) = yaml {
38 Ok(scalar)
39 } else {
40 Err(expected_scalar!("Expected a scalar, got: {:?}", yaml))
41 }
42}
43
44pub fn scalar_to_string(scalar: &saphyr::Scalar) -> String {
47 match scalar {
48 saphyr::Scalar::Null => "null".to_string(),
49 saphyr::Scalar::Boolean(b) => b.to_string(),
50 saphyr::Scalar::Integer(i) => i.to_string(),
51 saphyr::Scalar::FloatingPoint(o) => o.to_string(),
52 saphyr::Scalar::String(s) => s.to_string(),
53 }
54}
55
56pub fn format_scalar(scalar: &saphyr::Scalar) -> String {
58 match scalar {
59 saphyr::Scalar::String(s) => format!("\"{s}\""),
60 _ => scalar_to_string(scalar),
61 }
62}
63
64pub fn format_marked_yaml(marked_yaml: &saphyr::MarkedYaml) -> String {
65 format!(
66 "{} {}",
67 format_marker(&marked_yaml.span.start),
68 format_yaml_data(&marked_yaml.data)
69 )
70}
71
72pub fn format_annotated_mapping(
73 mapping: &saphyr::AnnotatedMapping<'_, saphyr::MarkedYaml<'_>>,
74) -> String {
75 let items: Vec<String> = mapping
76 .iter()
77 .map(|(k, v)| format!("{}: {}", format_yaml_data(&k.data), format_marked_yaml(v)))
78 .collect();
79 format!("{{ {} }}", items.join(", "))
80}
81
82pub fn format_yaml_data<'a>(data: &saphyr::YamlData<'a, saphyr::MarkedYaml<'a>>) -> String {
84 match data {
85 saphyr::YamlData::Value(scalar) => format_scalar(scalar),
86 saphyr::YamlData::Sequence(seq) => {
87 let items: Vec<String> = seq
88 .iter()
89 .map(|marked_yaml| format_marked_yaml(marked_yaml))
90 .collect();
91 format!("[{}]", items.join(", "))
92 }
93 saphyr::YamlData::Mapping(mapping) => format_annotated_mapping(mapping),
94 _ => format!("<unsupported type: {data:?}>"),
95 }
96}
97
98pub fn format_marker(marker: &saphyr::Marker) -> String {
100 format!("[{}, {}]", marker.line(), marker.col())
101}
102
103pub fn humanize_yaml_data<'input>(data: &YamlData<'input, MarkedYaml<'input>>) -> String {
139 match data {
140 YamlData::Value(Scalar::String(s)) => format!(
141 "{} (string)",
142 serde_json::to_string(s.as_ref()).unwrap_or_else(|_| format!("{:?}", s.as_ref()))
143 ),
144 YamlData::Value(Scalar::Integer(i)) => format!("{i} (int)"),
145 YamlData::Value(Scalar::FloatingPoint(f)) => {
146 let x = f.into_inner();
147 format!(
148 "{} (float)",
149 serde_json::to_string(&x).unwrap_or_else(|_| format!("{x}"))
150 )
151 }
152 YamlData::Value(Scalar::Boolean(b)) => format!("{b} (bool)"),
153 _ => format!("{data:?}"),
154 }
155}
156
157pub fn format_vec<V>(vec: &[V]) -> String
159where
160 V: std::fmt::Display,
161{
162 let items: Vec<String> = vec.iter().map(|v| format!("{v}")).collect();
163 format!("[{}]", items.join(", "))
164}
165
166pub fn format_linked_hash_map<K, V>(
168 linked_hash_map: &linked_hash_map::LinkedHashMap<K, V>,
169) -> String
170where
171 K: AsRef<str>,
172 V: std::fmt::Display,
173{
174 let items: Vec<String> = linked_hash_map
175 .iter()
176 .map(|(k, v)| format!("{}: {}", k.as_ref(), v))
177 .collect();
178 format!("{{ {} }}", items.join(", "))
179}
180
181pub fn format_hash_map<K, V>(hash_map: &HashMap<K, V>) -> String
183where
184 K: AsRef<str>,
185 V: std::fmt::Display,
186{
187 if hash_map.is_empty() {
188 return "{}".to_string();
189 }
190 let items: Vec<String> = hash_map
191 .iter()
192 .map(|(k, v)| format!("\"{}\": {}", k.as_ref(), v))
193 .collect();
194 format!("{{ {} }}", items.join(", "))
195}
196pub fn collect_keys(a: &'static [&'static str], b: &'static [&'static str]) -> Vec<&'static str> {
198 let mut keys = Vec::with_capacity(a.len() + b.len());
199 keys.extend_from_slice(a);
200 keys.extend_from_slice(b);
201 keys.sort();
202 keys.dedup();
203 keys
204}
205
206pub fn filter_mapping<'a>(
208 mapping: &saphyr::AnnotatedMapping<'a, saphyr::MarkedYaml<'a>>,
209 keys: Vec<&'static str>,
210 override_type: &'a str,
211) -> Result<saphyr::AnnotatedMapping<'a, saphyr::MarkedYaml<'a>>> {
212 let mut filtered_mapping = saphyr::AnnotatedMapping::new();
213 for (k, v) in mapping.iter() {
214 if let YamlData::Value(Scalar::String(key)) = &k.data {
215 if keys.contains(&key.as_ref()) {
216 match key.as_ref() {
217 "type" => {
218 filtered_mapping
219 .insert(k.clone(), MarkedYaml::value_from_str(override_type));
220 }
221 _ => {
222 filtered_mapping.insert(k.clone(), v.clone());
223 }
224 }
225 }
226 } else {
227 return Err(expected_scalar!("Expected a string key, got: {:?}", k.data));
228 }
229 }
230 Ok(filtered_mapping.into_iter().collect())
231}
232
233#[cfg(test)]
234mod tests {
235 use ordered_float::OrderedFloat;
236 use saphyr::LoadableYamlNode as _;
237 use std::collections::HashMap;
238
239 use super::*;
240
241 #[test]
242 fn test_hash_map() {
243 let expected = vec![("foo".to_string(), "bar".to_string())]
244 .into_iter()
245 .collect::<HashMap<String, String>>();
246
247 let actual = hash_map("foo".to_string(), "bar".to_string());
248 assert_eq!(expected, actual);
249 }
250
251 #[test]
252 #[allow(clippy::approx_constant)]
253 fn test_scalar_to_string() {
254 assert_eq!("null", scalar_to_string(&saphyr::Scalar::Null));
255 assert_eq!("true", scalar_to_string(&saphyr::Scalar::Boolean(true)));
256 assert_eq!("false", scalar_to_string(&saphyr::Scalar::Boolean(false)));
257 assert_eq!("42", scalar_to_string(&saphyr::Scalar::Integer(42)));
258 assert_eq!("-1", scalar_to_string(&saphyr::Scalar::Integer(-1)));
259 assert_eq!(
260 "3.14",
261 scalar_to_string(&saphyr::Scalar::FloatingPoint(OrderedFloat::from(3.14)))
262 );
263 assert_eq!(
264 "foo",
265 scalar_to_string(&saphyr::Scalar::String("foo".into()))
266 );
267 }
268
269 #[test]
270 #[allow(clippy::approx_constant)]
271 fn test_format_scalar() {
272 assert_eq!("null", format_scalar(&saphyr::Scalar::Null));
273 assert_eq!("true", format_scalar(&saphyr::Scalar::Boolean(true)));
274 assert_eq!("false", format_scalar(&saphyr::Scalar::Boolean(false)));
275 assert_eq!("42", format_scalar(&saphyr::Scalar::Integer(42)));
276 assert_eq!("-1", format_scalar(&saphyr::Scalar::Integer(-1)));
277 assert_eq!(
278 "3.14",
279 format_scalar(&saphyr::Scalar::FloatingPoint(OrderedFloat::from(3.14)))
280 );
281 assert_eq!(
282 "\"foo\"",
283 format_scalar(&saphyr::Scalar::String("foo".into()))
284 );
285 }
286
287 #[test]
288 fn test_format_linked_hash_map() {
289 let docs = MarkedYaml::load_from_str("foo: bar").unwrap();
290 let doc = docs.first().expect("Expected a document");
291 let mapping = doc.data.as_mapping().expect("Expected a mapping");
292 let linked_hash_map = mapping
293 .into_iter()
294 .map(|(k, v)| (format_yaml_data(&k.data), format_yaml_data(&v.data)))
295 .collect::<linked_hash_map::LinkedHashMap<String, String>>();
296 assert_eq!(
297 "{ \"foo\": \"bar\" }",
298 format_linked_hash_map(&linked_hash_map)
299 );
300 }
301
302 #[test]
303 fn humanize_yaml_data_integer() {
304 let docs = MarkedYaml::load_from_str("42").unwrap();
305 assert_eq!(humanize_yaml_data(&docs.first().unwrap().data), "42 (int)");
306 }
307
308 #[test]
309 fn humanize_yaml_data_float() {
310 let docs = MarkedYaml::load_from_str("3.14").unwrap();
311 assert_eq!(
312 humanize_yaml_data(&docs.first().unwrap().data),
313 "3.14 (float)"
314 );
315 }
316
317 #[test]
318 fn humanize_yaml_data_boolean() {
319 let docs = MarkedYaml::load_from_str("true").unwrap();
320 assert_eq!(
321 humanize_yaml_data(&docs.first().unwrap().data),
322 "true (bool)"
323 );
324 let docs = MarkedYaml::load_from_str("false").unwrap();
325 assert_eq!(
326 humanize_yaml_data(&docs.first().unwrap().data),
327 "false (bool)"
328 );
329 }
330
331 #[test]
332 fn humanize_yaml_data_string_plain() {
333 let docs = MarkedYaml::load_from_str("hello").unwrap();
334 assert_eq!(
335 humanize_yaml_data(&docs.first().unwrap().data),
336 r#""hello" (string)"#
337 );
338 }
339
340 #[test]
341 fn humanize_yaml_data_string_with_quotes_escaped() {
342 let docs = MarkedYaml::load_from_str(r#""str\" ing""#).unwrap();
343 assert_eq!(
344 humanize_yaml_data(&docs.first().unwrap().data),
345 r#""str\" ing" (string)"#
346 );
347 }
348
349 #[test]
350 fn humanize_yaml_data_non_scalar_uses_debug() {
351 let docs = MarkedYaml::load_from_str("a: 1").unwrap();
352 let s = humanize_yaml_data(&docs.first().unwrap().data);
353 assert!(
354 s.starts_with("Mapping("),
355 "expected Debug fallback for mapping, got {s:?}"
356 );
357 }
358}