online-dsl-forge 0.3.3

A bounded parser and runtime for in-memory DSL expressions.
Documentation
use std::env;
use std::io::{self, Read};
use std::process::ExitCode;

use online_dsl_forge::{
  CompileOptions, EvalLimits, MapRuntime, compile_expression, evaluate, format_expression,
  parse_expression,
};

fn main() -> ExitCode {
  match run(env::args().skip(1).collect()) {
    Ok(()) => ExitCode::SUCCESS,
    Err(error) => {
      eprintln!("error: {error}");
      ExitCode::from(1)
    }
  }
}

fn run(args: Vec<String>) -> Result<(), String> {
  let Some(command) = args.first().map(String::as_str) else {
    return Err(usage());
  };
  let rest = &args[1..];
  match command {
    "check" => {
      let expression = expression_from_args(rest)?;
      parse_expression(&expression).map_err(|error| error.to_string())?;
      println!("ok");
      Ok(())
    }
    "ast" => {
      let expression = expression_from_args(rest)?;
      let ast = parse_expression(&expression).map_err(|error| error.to_string())?;
      let json = serde_json::to_string_pretty(&ast).map_err(|error| error.to_string())?;
      println!("{json}");
      Ok(())
    }
    "fmt" => {
      let expression = expression_from_args(rest)?;
      let ast = parse_expression(&expression).map_err(|error| error.to_string())?;
      println!("{}", format_expression(&ast));
      Ok(())
    }
    "eval" => eval_command(rest),
    "help" | "--help" | "-h" => {
      println!("{}", usage());
      Ok(())
    }
    other => Err(format!("unknown command {other}\n\n{}", usage())),
  }
}

fn eval_command(args: &[String]) -> Result<(), String> {
  let mut expression_parts = Vec::new();
  let mut bindings = "{}".to_string();
  let mut index = 0;
  while index < args.len() {
    match args[index].as_str() {
      "--bindings" => {
        index += 1;
        bindings = args
          .get(index)
          .cloned()
          .ok_or_else(|| "--bindings requires a JSON value".to_string())?;
      }
      "--bindings-file" => {
        index += 1;
        let path = args
          .get(index)
          .ok_or_else(|| "--bindings-file requires a path".to_string())?;
        bindings = std::fs::read_to_string(path).map_err(|error| error.to_string())?;
      }
      value => expression_parts.push(value.to_string()),
    }
    index += 1;
  }

  let expression = if expression_parts.is_empty() {
    read_stdin()?
  } else {
    expression_parts.join(" ")
  };
  let bindings_json =
    serde_json::from_str::<serde_json::Value>(&bindings).map_err(|error| error.to_string())?;
  let runtime = MapRuntime::from_json_bindings(bindings_json).map_err(|error| error.to_string())?;
  let ast = parse_expression(&expression).map_err(|error| error.to_string())?;
  let compiled = compile_expression(&ast, &runtime.schema(), CompileOptions::default())
    .map_err(|error| error.to_string())?;
  let value =
    evaluate(&compiled, &runtime, EvalLimits::default()).map_err(|error| error.to_string())?;
  let json = serde_json::Value::from(value);
  println!(
    "{}",
    serde_json::to_string_pretty(&json).map_err(|error| error.to_string())?
  );
  Ok(())
}

fn expression_from_args(args: &[String]) -> Result<String, String> {
  if args.is_empty() {
    read_stdin()
  } else {
    Ok(args.join(" "))
  }
}

fn read_stdin() -> Result<String, String> {
  let mut input = String::new();
  io::stdin()
    .read_to_string(&mut input)
    .map_err(|error| error.to_string())?;
  Ok(input)
}

fn usage() -> String {
  "usage:
  online-dsl-forgectl check EXPR
  online-dsl-forgectl ast EXPR
  online-dsl-forgectl fmt EXPR
  online-dsl-forgectl eval EXPR --bindings JSON
  online-dsl-forgectl eval EXPR --bindings-file PATH"
    .to_string()
}