use std::{ env, fs, path::{Path, PathBuf}, process::ExitCode, }; use decodal::{ Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span, format_diagnostic_with, }; fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("{error}"); ExitCode::FAILURE } } } fn run() -> Result<(), String> { let mut args = env::args().skip(1); let first = args.next().unwrap_or_else(|| String::from("--help")); match first.as_str() { "--help" | "-h" => { print_help(); Ok(()) } "--version" | "-V" => { println!("Decodal {}", decodal::version()); Ok(()) } "check" => { let path = args.next().unwrap_or_else(|| String::from("-")); let data = materialize_path(&path)?; drop(data); println!("ok"); Ok(()) } "eval" | "materialize" => { let path = args.next().unwrap_or_else(|| String::from("-")); let data = materialize_path(&path)?; print_data(&data, 0); println!(); Ok(()) } path => { let data = materialize_path(path)?; print_data(&data, 0); println!(); Ok(()) } } } fn materialize_path(path: &str) -> Result { let root = read_root_source(path).map_err(format_raw_diagnostic)?; let root_name = root.name.clone(); let mut engine = Engine::new(FsLoader); let module = engine .add_root_source(root.key, root.name, &root.source) .map_err(|error| format_diagnostic_with_root(&error, &root_name))?; let value = engine .eval_module(module) .map_err(|error| engine.format_diagnostic(&error))?; engine .materialize(&value) .map_err(|error| engine.format_diagnostic(&error)) } fn read_root_source(path: &str) -> Result { if path == "-" { use std::io::Read; let mut source = String::new(); std::io::stdin() .read_to_string(&mut source) .map_err(|error| { Diagnostic::new( DiagnosticKind::Import, Span::default(), format!("failed to read stdin: {error}"), ) })?; Ok(LoadedSource { key: String::from(""), name: String::from(""), source, }) } else { load_path(Path::new(path)) } } #[derive(Debug, Clone, Copy)] struct FsLoader; impl SourceLoader for FsLoader { fn load( &mut self, current_key: Option<&str>, specifier: &str, ) -> Result { let path = Path::new(specifier); let path = if path.is_absolute() { PathBuf::from(path) } else if let Some(current_key) = current_key.filter(|key| *key != "") { Path::new(current_key) .parent() .unwrap_or_else(|| Path::new(".")) .join(path) } else { PathBuf::from(path) }; load_path(&path) } } fn load_path(path: &Path) -> Result { let canonical = path.canonicalize().map_err(|error| { Diagnostic::new( DiagnosticKind::Import, Span::default(), format!("failed to resolve `{}`: {error}", path.display()), ) })?; let source = fs::read_to_string(&canonical).map_err(|error| { Diagnostic::new( DiagnosticKind::Import, Span::default(), format!("failed to read `{}`: {error}", canonical.display()), ) })?; let key = canonical.to_string_lossy().into_owned(); Ok(LoadedSource { name: key.clone(), key, source, }) } fn print_help() { println!("Decodal - Deferred Constraint Data Language"); println!(); println!("Usage:"); println!(" decodal Materialize a DCDL file"); println!(" decodal eval Materialize a DCDL file"); println!(" decodal check Evaluate and materialize without printing data"); println!(" decodal - Read DCDL source from stdin"); } fn format_diagnostic_with_root(error: &Diagnostic, root_name: &str) -> String { format_diagnostic_with(error, |source| (source == SourceId(0)).then_some(root_name)) } fn format_raw_diagnostic(error: Diagnostic) -> String { format_diagnostic_with(&error, |_| None) } fn print_data(data: &Data, indent: usize) { match data { Data::String(value) => print_string(value), Data::Int(value) => print!("{value}"), Data::Float(value) => print!("{value}"), Data::Bool(value) => print!("{value}"), Data::Array(items) => { print!("["); if !items.is_empty() { println!(); for (index, item) in items.iter().enumerate() { print_indent(indent + 2); print_data(item, indent + 2); if index + 1 != items.len() { print!(","); } println!(); } print_indent(indent); } print!("]"); } Data::Object(fields) => { print!("{{"); if !fields.is_empty() { println!(); for (index, field) in fields.iter().enumerate() { print_indent(indent + 2); print_string(&field.name); print!(": "); print_data(&field.value, indent + 2); if index + 1 != fields.len() { print!(","); } println!(); } print_indent(indent); } print!("}}"); } } } fn print_indent(indent: usize) { for _ in 0..indent { print!(" "); } } fn print_string(value: &str) { print!("\""); for ch in value.chars() { match ch { '"' => print!("\\\""), '\\' => print!("\\\\"), '\n' => print!("\\n"), '\r' => print!("\\r"), '\t' => print!("\\t"), ch => print!("{ch}"), } } print!("\""); }