223 lines
6.4 KiB
Rust
223 lines
6.4 KiB
Rust
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<Data, String> {
|
|
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<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 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!("\"");
|
|
}
|