use crate::{
error::Result,
parser::strategies::ParsingStrategy,
value::{FlexValue, Source},
};
use saphyr::{LoadableYamlNode, Scalar, Yaml};
use serde_json::{Map, Number, Value as JsonValue};
#[derive(Debug, Clone, Default)]
pub struct YamlStrategy;
impl YamlStrategy {
#[must_use]
pub const fn new() -> Self {
Self
}
fn yaml_to_json(yaml: &Yaml) -> Option<JsonValue> {
match yaml {
Yaml::Value(scalar) => match scalar {
Scalar::Null => Some(JsonValue::Null),
Scalar::Boolean(b) => Some(JsonValue::Bool(*b)),
Scalar::Integer(i) => Some(JsonValue::Number(Number::from(*i))),
Scalar::FloatingPoint(f) => Number::from_f64(f.into_inner()).map(JsonValue::Number),
Scalar::String(s) => Some(JsonValue::String(s.to_string())),
},
Yaml::Sequence(seq) => {
let vec: Option<Vec<JsonValue>> = seq.iter().map(Self::yaml_to_json).collect();
vec.map(JsonValue::Array)
}
Yaml::Mapping(mapping) => {
let mut map = Map::new();
for (k, v) in mapping {
if let Some(key_str) = Self::yaml_to_string(k) {
if let Some(value) = Self::yaml_to_json(v) {
map.insert(key_str, value);
}
}
}
Some(JsonValue::Object(map))
}
Yaml::Tagged(_tag, node) => Self::yaml_to_json(node),
Yaml::Representation(s, _, _) => Some(JsonValue::String(s.to_string())),
Yaml::Alias(_) | Yaml::BadValue => None,
}
}
fn yaml_to_string(yaml: &Yaml) -> Option<String> {
match yaml {
Yaml::Value(scalar) => Some(match scalar {
Scalar::String(s) => s.to_string(),
Scalar::Integer(i) => i.to_string(),
Scalar::FloatingPoint(f) => f.to_string(),
Scalar::Boolean(b) => b.to_string(),
Scalar::Null => String::from("null"),
}),
Yaml::Representation(s, _, _) => Some(s.to_string()),
_ => None,
}
}
fn looks_like_yaml(input: &str) -> bool {
let trimmed = input.trim();
if trimmed.starts_with('{') || trimmed.starts_with('[') {
return false;
}
let yaml_pattern_count = trimmed
.lines()
.filter(|line| {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return false;
}
line.contains(':') && !line.starts_with('-')
})
.count();
yaml_pattern_count >= 2
}
}
impl ParsingStrategy for YamlStrategy {
fn name(&self) -> &'static str {
"yaml"
}
fn parse(&self, input: &str) -> Result<Vec<FlexValue>> {
if !Self::looks_like_yaml(input) {
return Ok(Vec::new());
}
match Yaml::load_from_str(input) {
Ok(docs) => {
if let Some(yaml_doc) = docs.first() {
if let Some(json_value) = Self::yaml_to_json(yaml_doc) {
let flex_value = FlexValue::new(json_value, Source::Yaml);
return Ok(vec![flex_value]);
}
}
Ok(Vec::new())
}
Err(_) => {
Ok(Vec::new())
}
}
}
fn priority(&self) -> u8 {
3
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_looks_like_yaml() {
assert!(YamlStrategy::looks_like_yaml("name: Alice\nage: 30"));
assert!(YamlStrategy::looks_like_yaml(
"user:\n name: Bob\n age: 25"
));
assert!(YamlStrategy::looks_like_yaml(
"# Comment\nname: Charlie\nage: 35"
));
assert!(!YamlStrategy::looks_like_yaml("{\"name\": \"Alice\"}"));
assert!(!YamlStrategy::looks_like_yaml("[1, 2, 3]"));
assert!(!YamlStrategy::looks_like_yaml("Just plain text"));
assert!(!YamlStrategy::looks_like_yaml("name: Alice")); }
#[test]
fn test_parse_simple_yaml() {
let strategy = YamlStrategy::new();
let input = "name: Alice\nage: 30";
let result = strategy.parse(input).unwrap();
assert_eq!(result.len(), 1);
assert!(matches!(result[0].source, Source::Yaml));
let obj = result[0].value.as_object().unwrap();
assert_eq!(obj.get("name").unwrap().as_str().unwrap(), "Alice");
assert_eq!(obj.get("age").unwrap().as_u64().unwrap(), 30);
}
#[test]
fn test_parse_nested_yaml() {
let strategy = YamlStrategy::new();
let input = "user:\n name: Bob\n age: 25";
let result = strategy.parse(input).unwrap();
assert_eq!(result.len(), 1);
let obj = result[0].value.as_object().unwrap();
let user = obj.get("user").unwrap().as_object().unwrap();
assert_eq!(user.get("name").unwrap().as_str().unwrap(), "Bob");
assert_eq!(user.get("age").unwrap().as_u64().unwrap(), 25);
}
#[test]
fn test_parse_yaml_with_array() {
let strategy = YamlStrategy::new();
let input = "names:\n - Alice\n - Bob\ncount: 2";
let result = strategy.parse(input).unwrap();
assert_eq!(result.len(), 1);
let obj = result[0].value.as_object().unwrap();
let names = obj.get("names").unwrap().as_array().unwrap();
assert_eq!(names.len(), 2);
assert_eq!(names[0].as_str().unwrap(), "Alice");
assert_eq!(names[1].as_str().unwrap(), "Bob");
}
#[test]
fn test_parse_json_not_yaml() {
let strategy = YamlStrategy::new();
let input = r#"{"name": "Alice", "age": 30}"#;
let result = strategy.parse(input).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_parse_invalid_yaml() {
let strategy = YamlStrategy::new();
let input = "name: Alice\n invalid indentation\nage: 30";
let _result = strategy.parse(input).unwrap();
}
#[test]
fn test_yaml_with_comments() {
let strategy = YamlStrategy::new();
let input = "# User data\nname: Alice # Full name\nage: 30";
let result = strategy.parse(input).unwrap();
assert_eq!(result.len(), 1);
let obj = result[0].value.as_object().unwrap();
assert_eq!(obj.get("name").unwrap().as_str().unwrap(), "Alice");
}
#[test]
fn test_strategy_name() {
let strategy = YamlStrategy::new();
assert_eq!(strategy.name(), "yaml");
}
#[test]
fn test_strategy_priority() {
let strategy = YamlStrategy::new();
assert_eq!(strategy.priority(), 3);
}
}