use serde_yaml::Value;
use crate::types::{new_key_path, ProcessHandling, TypedValue, ValueType};
fn typed_value_from_yaml(val: &Value) -> Option<(TypedValue, ValueType)> {
match val {
Value::Bool(b) => Some((TypedValue::Bool(*b), ValueType::Bool)),
Value::String(s) => Some((TypedValue::String(s.clone()), ValueType::String)),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
if n.is_i64() {
return Some((TypedValue::Integer(i), ValueType::Integer));
}
}
n.as_f64()
.map(|f| (TypedValue::Number(f), ValueType::Number))
}
Value::Null => Some((TypedValue::Null, ValueType::Null)),
Value::Tagged(tagged) => match &tagged.value {
Value::String(s) => Some((TypedValue::String(s.clone()), ValueType::String)),
inner => typed_value_from_yaml(inner),
},
_ => None, }
}
fn typed_value_to_yaml(typed: TypedValue) -> Value {
match typed {
TypedValue::Bool(b) => Value::Bool(b),
TypedValue::Integer(i) => Value::Number(serde_yaml::Number::from(i)),
TypedValue::Number(f) => Value::Number(serde_yaml::Number::from(f)),
TypedValue::String(s) => Value::String(s),
TypedValue::Null => Value::Null,
}
}
type Processor<'a> = &'a mut dyn FnMut(
TypedValue,
ValueType,
&str,
) -> Result<(TypedValue, ValueType, ProcessHandling), Box<dyn std::error::Error>>;
fn traverse(
val: &mut Value,
key_path: &str,
processor: Processor<'_>,
filter_keys: &[String],
) -> Result<bool, Box<dyn std::error::Error>> {
match val {
Value::Mapping(map) => {
let keys: Vec<Value> = map.keys().cloned().collect();
for yaml_key in &keys {
let key_str = match yaml_key {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
_ => continue, };
let new_path = new_key_path(key_path, &key_str);
if let Some(child) = map.get_mut(yaml_key) {
if !traverse(child, &new_path, processor, filter_keys)? {
return Ok(false);
}
}
}
}
Value::Sequence(seq) => {
for item in seq.iter_mut() {
if !traverse(item, key_path, processor, filter_keys)? {
return Ok(false);
}
}
}
scalar => {
if !filter_keys.is_empty()
&& !filter_keys.iter().any(|k| k.as_str() == key_path)
{
return Ok(true);
}
if let Some((typed, vt)) = typed_value_from_yaml(scalar) {
match processor(typed, vt, key_path) {
Ok((new_typed, _, ProcessHandling::Process)) => {
*scalar = typed_value_to_yaml(new_typed);
}
Ok((_, _, ProcessHandling::Cancel)) => return Ok(false),
Ok((_, _, ProcessHandling::Skip)) => {}
Err(e) => {
eprintln!("error processing key '{}': {}", key_path, e);
return Ok(false);
}
}
}
}
}
Ok(true)
}
pub fn process_file(
input: &str,
output: &str,
filter_keys: &[String],
processor: Processor<'_>,
) -> Result<(), Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(input)
.map_err(|e| format!("error reading '{}': {}", input, e))?;
let mut root: Value = serde_yaml::from_str(&content)
.map_err(|e| format!("error parsing YAML from '{}': {}", input, e))?;
if !traverse(&mut root, "", processor, filter_keys)? {
return Err("processing canceled".into());
}
let yaml_str = serde_yaml::to_string(&root)?;
if output == "stdout" {
print!("{}", yaml_str);
} else {
std::fs::write(output, &yaml_str)
.map_err(|e| format!("error writing '{}': {}", output, e))?;
}
Ok(())
}