use crate::InputSpec;
use std::collections::BTreeMap;
#[derive(Debug, Default)]
pub struct Parsed {
pub description: String,
pub inputs: BTreeMap<String, InputSpec>,
pub outputs: BTreeMap<String, String>,
pub branding_icon: Option<String>,
pub branding_color: Option<String>,
}
pub fn parse(src: &str) -> Parsed {
let mut out = Parsed::default();
let mut section: Option<&str> = None;
let mut current_key: Option<String> = None;
for raw in src.lines() {
let line = raw.trim_end();
if line.starts_with("description:") {
out.description = unquote(line.splitn(2, ':').nth(1).unwrap_or("").trim());
continue;
}
if line.starts_with("inputs:") {
section = Some("inputs");
current_key = None;
continue;
}
if line.starts_with("outputs:") {
section = Some("outputs");
current_key = None;
continue;
}
if line.starts_with("runs:") || line.starts_with("name:") {
section = None;
current_key = None;
continue;
}
if line.starts_with("branding:") {
let after = line.splitn(2, ':').nth(1).unwrap_or("").trim();
if after.starts_with('{') {
let inner = after.trim_start_matches('{').trim_end_matches('}').trim();
for pair in inner.split(',') {
let mut kv = pair.splitn(2, ':');
let k = kv.next().unwrap_or("").trim();
let v = kv.next().unwrap_or("").trim();
let v = unquote(v);
if k == "icon" { out.branding_icon = Some(v); }
else if k == "color" { out.branding_color = Some(v); }
}
section = None;
} else {
section = Some("branding");
}
current_key = None;
continue;
}
if let Some(sec) = section {
if let Some(rest) = line.strip_prefix(" ") {
if sec == "branding" {
if let Some(v) = rest.strip_prefix("icon:") {
out.branding_icon = Some(unquote(v.trim()));
} else if let Some(v) = rest.strip_prefix("color:") {
out.branding_color = Some(unquote(v.trim()));
}
continue;
}
if !rest.starts_with(' ') && rest.ends_with(':') {
let key = rest.trim_end_matches(':').to_string();
current_key = Some(key.clone());
if sec == "inputs" {
out.inputs.insert(key, InputSpec::default());
} else {
out.outputs.insert(key, String::new());
}
continue;
}
if let Some(ck) = ¤t_key {
let attr = rest.trim();
if sec == "inputs" {
if let Some(spec) = out.inputs.get_mut(ck) {
if let Some(val) = attr.strip_prefix("required:") {
spec.required = val.trim().contains("true");
} else if let Some(val) = attr.strip_prefix("default:") {
spec.default = Some(unquote(val.trim()));
} else if let Some(val) = attr.strip_prefix("description:") {
spec.description = Some(unquote(val.trim()));
}
}
} else if sec == "outputs" {
if let Some(val) = attr.strip_prefix("description:") {
out.outputs.insert(ck.clone(), unquote(val.trim()));
}
}
}
}
}
}
out
}
pub fn unquote(s: &str) -> String {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_flow_branding() {
let src = "name: 'x'\nbranding: { icon: 'refresh-cw', color: 'blue' }\nruns:\n";
let p = parse(src);
assert_eq!(p.branding_icon.as_deref(), Some("refresh-cw"));
assert_eq!(p.branding_color.as_deref(), Some("blue"));
}
#[test]
fn parses_block_branding() {
let src = "name: 'x'\nbranding:\n icon: 'tag'\n color: 'gray-dark'\nruns:\n";
let p = parse(src);
assert_eq!(p.branding_icon.as_deref(), Some("tag"));
assert_eq!(p.branding_color.as_deref(), Some("gray-dark"));
}
#[test]
fn parses_unquoted_block_branding() {
let src = "name: x\nbranding:\n icon: tag\n color: gray-dark\nruns:\n";
let p = parse(src);
assert_eq!(p.branding_icon.as_deref(), Some("tag"));
assert_eq!(p.branding_color.as_deref(), Some("gray-dark"));
}
#[test]
fn absent_branding_yields_none() {
let src = "name: 'x'\ndescription: 'd'\nruns:\n";
let p = parse(src);
assert!(p.branding_icon.is_none());
assert!(p.branding_color.is_none());
}
}