Decodal/crates/decodal-cli/src/main.rs

225 lines
6.4 KiB
Rust

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<Data, Diagnostic> {
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<LoadedSource, Diagnostic> {
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("<stdin>"),
name: String::from("<stdin>"),
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<LoadedSource, Diagnostic> {
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 != "<stdin>") {
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<LoadedSource, Diagnostic> {
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 <file.dcdl> Materialize a DCDL file");
println!(" decodal eval <file.dcdl> Materialize a DCDL file");
println!(" decodal check <file.dcdl> 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!("\"");
}