use crate::Result;
use hashlink::linked_hash_map;
use saphyr::{MarkedYaml, Scalar, YamlData};
use std::borrow::Cow;
use std::collections::HashMap;
use std::hash::Hash;
pub fn hash_map<K, V>(key: K, value: V) -> HashMap<K, V>
where
K: Hash + Eq + Clone,
{
let mut hash_map = HashMap::with_capacity(1);
hash_map.insert(key, value);
hash_map
}
pub fn linked_hash_map<K, V>(key: K, value: V) -> linked_hash_map::LinkedHashMap<K, V>
where
K: Hash + Eq + Clone,
{
let mut linked_hash_map = linked_hash_map::LinkedHashMap::new();
linked_hash_map.insert(key, value);
linked_hash_map
}
pub const fn saphyr_yaml_string(s: &str) -> saphyr::Yaml<'_> {
saphyr::Yaml::Value(saphyr::Scalar::String(Cow::Borrowed(s)))
}
pub fn try_unwrap_saphyr_scalar<'a>(yaml: &'a saphyr::Yaml) -> Result<&'a saphyr::Scalar<'a>> {
if let saphyr::Yaml::Value(scalar) = yaml {
Ok(scalar)
} else {
Err(expected_scalar!("Expected a scalar, got: {:?}", yaml))
}
}
pub fn scalar_to_string(scalar: &saphyr::Scalar) -> String {
match scalar {
saphyr::Scalar::Null => "null".to_string(),
saphyr::Scalar::Boolean(b) => b.to_string(),
saphyr::Scalar::Integer(i) => i.to_string(),
saphyr::Scalar::FloatingPoint(o) => o.to_string(),
saphyr::Scalar::String(s) => s.to_string(),
}
}
pub fn format_scalar(scalar: &saphyr::Scalar) -> String {
match scalar {
saphyr::Scalar::String(s) => format!("\"{s}\""),
_ => scalar_to_string(scalar),
}
}
pub fn format_marked_yaml(marked_yaml: &saphyr::MarkedYaml) -> String {
format!(
"{} {}",
format_marker(&marked_yaml.span.start),
format_yaml_data(&marked_yaml.data)
)
}
pub fn format_annotated_mapping(
mapping: &saphyr::AnnotatedMapping<'_, saphyr::MarkedYaml<'_>>,
) -> String {
let items: Vec<String> = mapping
.iter()
.map(|(k, v)| format!("{}: {}", format_yaml_data(&k.data), format_marked_yaml(v)))
.collect();
format!("{{ {} }}", items.join(", "))
}
pub fn format_yaml_data<'a>(data: &saphyr::YamlData<'a, saphyr::MarkedYaml<'a>>) -> String {
match data {
saphyr::YamlData::Value(scalar) => format_scalar(scalar),
saphyr::YamlData::Sequence(seq) => {
let items: Vec<String> = seq
.iter()
.map(|marked_yaml| format_marked_yaml(marked_yaml))
.collect();
format!("[{}]", items.join(", "))
}
saphyr::YamlData::Mapping(mapping) => format_annotated_mapping(mapping),
_ => format!("<unsupported type: {data:?}>"),
}
}
pub fn format_marker(marker: &saphyr::Marker) -> String {
format!("[{}, {}]", marker.line(), marker.col())
}
pub fn humanize_yaml_data<'input>(data: &YamlData<'input, MarkedYaml<'input>>) -> String {
match data {
YamlData::Value(Scalar::String(s)) => format!(
"{} (string)",
serde_json::to_string(s.as_ref()).unwrap_or_else(|_| format!("{:?}", s.as_ref()))
),
YamlData::Value(Scalar::Integer(i)) => format!("{i} (int)"),
YamlData::Value(Scalar::FloatingPoint(f)) => {
let x = f.into_inner();
format!(
"{} (float)",
serde_json::to_string(&x).unwrap_or_else(|_| format!("{x}"))
)
}
YamlData::Value(Scalar::Boolean(b)) => format!("{b} (bool)"),
_ => format!("{data:?}"),
}
}
pub fn format_vec<V>(vec: &[V]) -> String
where
V: std::fmt::Display,
{
let items: Vec<String> = vec.iter().map(|v| format!("{v}")).collect();
format!("[{}]", items.join(", "))
}
pub fn format_linked_hash_map<K, V>(
linked_hash_map: &linked_hash_map::LinkedHashMap<K, V>,
) -> String
where
K: AsRef<str>,
V: std::fmt::Display,
{
let items: Vec<String> = linked_hash_map
.iter()
.map(|(k, v)| format!("{}: {}", k.as_ref(), v))
.collect();
format!("{{ {} }}", items.join(", "))
}
pub fn format_hash_map<K, V>(hash_map: &HashMap<K, V>) -> String
where
K: AsRef<str>,
V: std::fmt::Display,
{
if hash_map.is_empty() {
return "{}".to_string();
}
let items: Vec<String> = hash_map
.iter()
.map(|(k, v)| format!("\"{}\": {}", k.as_ref(), v))
.collect();
format!("{{ {} }}", items.join(", "))
}
pub fn collect_keys(a: &'static [&'static str], b: &'static [&'static str]) -> Vec<&'static str> {
let mut keys = Vec::with_capacity(a.len() + b.len());
keys.extend_from_slice(a);
keys.extend_from_slice(b);
keys.sort();
keys.dedup();
keys
}
pub fn filter_mapping<'a>(
mapping: &saphyr::AnnotatedMapping<'a, saphyr::MarkedYaml<'a>>,
keys: Vec<&'static str>,
override_type: &'a str,
) -> Result<saphyr::AnnotatedMapping<'a, saphyr::MarkedYaml<'a>>> {
let mut filtered_mapping = saphyr::AnnotatedMapping::new();
for (k, v) in mapping.iter() {
if let YamlData::Value(Scalar::String(key)) = &k.data {
if keys.contains(&key.as_ref()) {
match key.as_ref() {
"type" => {
filtered_mapping
.insert(k.clone(), MarkedYaml::value_from_str(override_type));
}
_ => {
filtered_mapping.insert(k.clone(), v.clone());
}
}
}
} else {
return Err(expected_scalar!("Expected a string key, got: {:?}", k.data));
}
}
Ok(filtered_mapping.into_iter().collect())
}
#[cfg(test)]
mod tests {
use ordered_float::OrderedFloat;
use saphyr::LoadableYamlNode as _;
use std::collections::HashMap;
use super::*;
#[test]
fn test_hash_map() {
let expected = vec![("foo".to_string(), "bar".to_string())]
.into_iter()
.collect::<HashMap<String, String>>();
let actual = hash_map("foo".to_string(), "bar".to_string());
assert_eq!(expected, actual);
}
#[test]
#[allow(clippy::approx_constant)]
fn test_scalar_to_string() {
assert_eq!("null", scalar_to_string(&saphyr::Scalar::Null));
assert_eq!("true", scalar_to_string(&saphyr::Scalar::Boolean(true)));
assert_eq!("false", scalar_to_string(&saphyr::Scalar::Boolean(false)));
assert_eq!("42", scalar_to_string(&saphyr::Scalar::Integer(42)));
assert_eq!("-1", scalar_to_string(&saphyr::Scalar::Integer(-1)));
assert_eq!(
"3.14",
scalar_to_string(&saphyr::Scalar::FloatingPoint(OrderedFloat::from(3.14)))
);
assert_eq!(
"foo",
scalar_to_string(&saphyr::Scalar::String("foo".into()))
);
}
#[test]
#[allow(clippy::approx_constant)]
fn test_format_scalar() {
assert_eq!("null", format_scalar(&saphyr::Scalar::Null));
assert_eq!("true", format_scalar(&saphyr::Scalar::Boolean(true)));
assert_eq!("false", format_scalar(&saphyr::Scalar::Boolean(false)));
assert_eq!("42", format_scalar(&saphyr::Scalar::Integer(42)));
assert_eq!("-1", format_scalar(&saphyr::Scalar::Integer(-1)));
assert_eq!(
"3.14",
format_scalar(&saphyr::Scalar::FloatingPoint(OrderedFloat::from(3.14)))
);
assert_eq!(
"\"foo\"",
format_scalar(&saphyr::Scalar::String("foo".into()))
);
}
#[test]
fn test_format_linked_hash_map() {
let docs = MarkedYaml::load_from_str("foo: bar").unwrap();
let doc = docs.first().expect("Expected a document");
let mapping = doc.data.as_mapping().expect("Expected a mapping");
let linked_hash_map = mapping
.into_iter()
.map(|(k, v)| (format_yaml_data(&k.data), format_yaml_data(&v.data)))
.collect::<linked_hash_map::LinkedHashMap<String, String>>();
assert_eq!(
"{ \"foo\": \"bar\" }",
format_linked_hash_map(&linked_hash_map)
);
}
#[test]
fn humanize_yaml_data_integer() {
let docs = MarkedYaml::load_from_str("42").unwrap();
assert_eq!(humanize_yaml_data(&docs.first().unwrap().data), "42 (int)");
}
#[test]
fn humanize_yaml_data_float() {
let docs = MarkedYaml::load_from_str("3.14").unwrap();
assert_eq!(
humanize_yaml_data(&docs.first().unwrap().data),
"3.14 (float)"
);
}
#[test]
fn humanize_yaml_data_boolean() {
let docs = MarkedYaml::load_from_str("true").unwrap();
assert_eq!(
humanize_yaml_data(&docs.first().unwrap().data),
"true (bool)"
);
let docs = MarkedYaml::load_from_str("false").unwrap();
assert_eq!(
humanize_yaml_data(&docs.first().unwrap().data),
"false (bool)"
);
}
#[test]
fn humanize_yaml_data_string_plain() {
let docs = MarkedYaml::load_from_str("hello").unwrap();
assert_eq!(
humanize_yaml_data(&docs.first().unwrap().data),
r#""hello" (string)"#
);
}
#[test]
fn humanize_yaml_data_string_with_quotes_escaped() {
let docs = MarkedYaml::load_from_str(r#""str\" ing""#).unwrap();
assert_eq!(
humanize_yaml_data(&docs.first().unwrap().data),
r#""str\" ing" (string)"#
);
}
#[test]
fn humanize_yaml_data_non_scalar_uses_debug() {
let docs = MarkedYaml::load_from_str("a: 1").unwrap();
let s = humanize_yaml_data(&docs.first().unwrap().data);
assert!(
s.starts_with("Mapping("),
"expected Debug fallback for mapping, got {s:?}"
);
}
}