use super::sources::{ AtProtoSource, GitSource, HttpSource, JsonFileSource, LocalSource, SlicesSource, Source, SourceType, }; use miette::{Result, miette}; use std::path::PathBuf; #[derive(Debug, Clone)] pub struct Config { pub output: OutputConfig, pub sources: Vec, } #[derive(Debug, Clone)] pub struct OutputConfig { pub lexicons_dir: PathBuf, pub codegen_dir: PathBuf, // TODO: root_module causes issues when set to anything other than "crate", needs rework // pub root_module: Option, pub cargo_toml_path: Option, } impl Config { pub fn from_kdl(text: &str) -> Result { let doc = text .parse::() .map_err(|e| miette!("Failed to parse KDL: {}", e))?; let mut output: Option = None; let mut sources = Vec::new(); for node in doc.nodes() { match node.name().value() { "output" => { if output.is_some() { return Err(miette!("Multiple output blocks found")); } output = Some(parse_output(node)?); } "source" => { sources.push(parse_source(node)?); } other => { return Err(miette!("Unknown config node: {}", other)); } } } let output = output.ok_or_else(|| miette!("Missing output block"))?; Ok(Config { output, sources }) } } fn parse_output(node: &kdl::KdlNode) -> Result { let children = node .children() .ok_or_else(|| miette!("output block has no children"))?; let mut lexicons_dir: Option = None; let mut codegen_dir: Option = None; let mut cargo_toml_path: Option = None; for child in children.nodes() { match child.name().value() { "lexicons" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("lexicons expects a string value"))?; lexicons_dir = Some(PathBuf::from(val)); } "codegen" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("codegen expects a string value"))?; codegen_dir = Some(PathBuf::from(val)); } // TODO: root-module causes issues, disabled for now // "root-module" => { // let val = child // .entries() // .get(0) // .and_then(|e| e.value().as_string()) // .ok_or_else(|| miette!("root-module expects a string value"))?; // root_module = Some(val.to_string()); // } "cargo-toml" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("cargo-toml expects a string value"))?; cargo_toml_path = Some(PathBuf::from(val)); } other => { return Err(miette!("Unknown output field: {}", other)); } } } Ok(OutputConfig { lexicons_dir: lexicons_dir.ok_or_else(|| miette!("Missing lexicons directory"))?, codegen_dir: codegen_dir.ok_or_else(|| miette!("Missing codegen directory"))?, cargo_toml_path, }) } fn parse_source(node: &kdl::KdlNode) -> Result { let name = node .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("source expects a name as first argument"))? .to_string(); let type_str = node .get("type") .and_then(|v| v.as_string()) .ok_or_else(|| miette!("source {} missing type attribute", name))?; let priority = node .get("priority") .and_then(|v| v.as_integer()) .map(|i| i as i32); let children = node .children() .ok_or_else(|| miette!("source {} has no children", name))?; let source_type = match type_str { "atproto" => parse_atproto_source(children)?, "git" => parse_git_source(children)?, "http" => parse_http_source(children)?, "jsonfile" => parse_jsonfile_source(children)?, "local" => parse_local_source(children)?, "slices" => parse_slices_source(children)?, other => return Err(miette!("Unknown source type: {}", other)), }; Ok(Source { name, source_type, explicit_priority: priority, }) } fn parse_atproto_source(children: &kdl::KdlDocument) -> Result { let mut endpoint: Option = None; let mut slice: Option = None; for child in children.nodes() { match child.name().value() { "endpoint" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("endpoint expects a string value"))?; endpoint = Some(val.to_string()); } "slice" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("slice expects a string value"))?; slice = Some(val.to_string()); } other => { return Err(miette!("Unknown atproto source field: {}", other)); } } } Ok(SourceType::AtProto(AtProtoSource { endpoint: endpoint.ok_or_else(|| miette!("Missing endpoint"))?, slice, })) } fn parse_git_source(children: &kdl::KdlDocument) -> Result { let mut repo: Option = None; let mut git_ref: Option = None; let mut pattern: Option = None; for child in children.nodes() { match child.name().value() { "repo" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("repo expects a string value"))?; repo = Some(val.to_string()); } "ref" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("ref expects a string value"))?; git_ref = Some(val.to_string()); } "pattern" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("pattern expects a string value"))?; pattern = Some(val.to_string()); } other => { return Err(miette!("Unknown git source field: {}", other)); } } } Ok(SourceType::Git(GitSource { repo: repo.ok_or_else(|| miette!("Missing repo"))?, git_ref, pattern: pattern.unwrap_or_else(|| "**/*.json".to_string()), })) } fn parse_http_source(children: &kdl::KdlDocument) -> Result { let mut url: Option = None; for child in children.nodes() { match child.name().value() { "url" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("url expects a string value"))?; url = Some(val.to_string()); } other => { return Err(miette!("Unknown http source field: {}", other)); } } } Ok(SourceType::Http(HttpSource { url: url.ok_or_else(|| miette!("Missing url"))?, })) } fn parse_jsonfile_source(children: &kdl::KdlDocument) -> Result { let mut path: Option = None; for child in children.nodes() { match child.name().value() { "path" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("path expects a string value"))?; path = Some(PathBuf::from(val)); } other => { return Err(miette!("Unknown jsonfile source field: {}", other)); } } } Ok(SourceType::JsonFile(JsonFileSource { path: path.ok_or_else(|| miette!("Missing path"))?, })) } fn parse_slices_source(children: &kdl::KdlDocument) -> Result { let mut slice: Option = None; for child in children.nodes() { match child.name().value() { "slice" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("slice expects a string value"))?; slice = Some(val.to_string()); } other => { return Err(miette!("Unknown slices source field: {}", other)); } } } Ok(SourceType::Slices(SlicesSource { slice: slice.ok_or_else(|| miette!("Missing slice"))?, })) } fn parse_local_source(children: &kdl::KdlDocument) -> Result { let mut path: Option = None; let mut pattern: Option = None; for child in children.nodes() { match child.name().value() { "path" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("path expects a string value"))?; path = Some(PathBuf::from(val)); } "pattern" => { let val = child .entries() .get(0) .and_then(|e| e.value().as_string()) .ok_or_else(|| miette!("pattern expects a string value"))?; pattern = Some(val.to_string()); } other => { return Err(miette!("Unknown local source field: {}", other)); } } } Ok(SourceType::Local(LocalSource { path: path.ok_or_else(|| miette!("Missing path"))?, pattern, })) }