#[path = "support.rs"]
mod support;
use noyalib::{from_str, Value};
fn type_name(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(n) => {
if n.as_f64().fract() == 0.0 {
"int"
} else {
"float"
}
}
Value::String(_) => "string",
Value::Sequence(_) => "list",
Value::Mapping(_) => "map",
Value::Tagged(_) => "tagged",
}
}
fn describe(value: &Value, prefix: &str, lines: &mut Vec<String>) {
match value {
Value::Mapping(map) => {
for (key, val) in map.iter() {
let path = if prefix.is_empty() {
key.to_string()
} else {
format!("{prefix}.{key}")
};
match val {
Value::Mapping(_) => {
lines.push(format!("{path:<30} map"));
describe(val, &path, lines);
}
Value::Sequence(seq) => {
let item_type = seq.first().map(type_name).unwrap_or("any");
lines.push(format!("{path:<30} list<{item_type}>"));
}
_ => {
let example = match val {
Value::String(s) => format!("\"{}\"", truncate(s, 20)),
other => other.to_string(),
};
lines.push(format!("{path:<30} {:<8} (e.g. {example})", type_name(val)));
}
}
}
}
_ => {
let path = if prefix.is_empty() { "root" } else { prefix };
lines.push(format!("{path:<30} {}", type_name(value)));
}
}
}
fn truncate(s: &str, max: usize) -> &str {
if s.len() <= max {
s
} else {
&s[..max]
}
}
fn main() {
support::header("noyalib -- schema_ext");
let yaml = r#"
server:
host: localhost
port: 8080
ssl: true
workers: 4
database:
url: postgres://localhost:5432/mydb
pool_size: 10
timeout_ms: 5000
logging:
level: info
format: json
outputs:
- stdout
- /var/log/app.log
features:
- authentication
- rate_limiting
- caching
"#;
let value: Value = from_str(yaml).unwrap();
support::task_with_output("Extract schema from YAML", || {
let mut lines = Vec::new();
describe(&value, "", &mut lines);
lines
});
support::task_with_output("Detect field presence across documents", || {
let docs = vec![
"host: a\nport: 80\ndebug: true\n",
"host: b\nport: 443\n",
"host: c\nport: 8080\ndebug: false\ntls: true\n",
];
let mut all_keys = std::collections::BTreeMap::<String, usize>::new();
for doc in &docs {
let v: Value = from_str(doc).unwrap();
if let Some(map) = v.as_mapping() {
for key in map.keys() {
*all_keys.entry(key.clone()).or_insert(0) += 1;
}
}
}
let total = docs.len();
all_keys
.iter()
.map(|(key, count)| {
let status = if *count == total {
"required"
} else {
"optional"
};
format!("{key:<10} {status} ({count}/{total} docs)")
})
.collect()
});
support::task_with_output("Type consistency across documents", || {
let docs = vec![
"port: 80\nname: app1\n",
"port: 443\nname: app2\n",
"port: \"8080\"\nname: app3\n", ];
let mut types = std::collections::BTreeMap::<String, Vec<&str>>::new();
for doc in &docs {
let v: Value = from_str(doc).unwrap();
if let Some(map) = v.as_mapping() {
for (key, val) in map.iter() {
types.entry(key.clone()).or_default().push(type_name(val));
}
}
}
types
.iter()
.map(|(key, type_list)| {
let unique: std::collections::BTreeSet<&&str> = type_list.iter().collect();
let consistent = unique.len() == 1;
let types_str = type_list.join(", ");
if consistent {
format!("{key:<10} consistent ({types_str})")
} else {
format!("{key:<10} MISMATCH! ({types_str})")
}
})
.collect()
});
support::summary(3);
}