use std::{ env, fs, path::{Path, PathBuf}, process::ExitCode, }; use decodal_core::{Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceLoader, Span}; fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(error) => { print_diagnostic(&error); ExitCode::FAILURE } } } fn run() -> Result<(), Diagnostic> { 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_core::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)?; let mut engine = Engine::new(FsLoader); let module = engine.add_root_source(root.key, root.name, &root.source)?; let value = engine.eval_module(module)?; engine.materialize(&value) } 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 print_diagnostic(error: &Diagnostic) { eprintln!( "error[{kind:?}] {source}:{start}..{end}: {message}", kind = error.kind, source = error.span.source.0, start = error.span.start, end = error.span.end, message = error.message, ); for label in &error.labels { eprintln!( " note {source}:{start}..{end}: {message}", source = label.span.source.0, start = label.span.start, end = label.span.end, message = label.message, ); } } 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!("\""); }