use std::fs;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand};
use mical_cli_syntax::ast::{AstNode as _, SourceFile};
#[derive(Parser)]
#[command(name = "mical", version, about = "Mical configuration language tool")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Eval(EvalArgs),
#[command(hide = true)]
Dev(DevArgs),
}
#[derive(Args)]
struct EvalArgs {
file: PathBuf,
#[arg(short = 'o', long = "output-path")]
output_path: Option<PathBuf>,
#[arg(short = 'f', long = "format", default_value = "json")]
format: OutputFormat,
#[command(flatten)]
query: QueryArgs,
}
#[derive(Args)]
#[group(multiple = false)]
struct QueryArgs {
#[arg(long)]
get: Option<String>,
#[arg(long)]
prefix: Option<String>,
}
#[derive(Clone, Debug)]
enum OutputFormat {
Json,
}
impl std::str::FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"json" => Ok(OutputFormat::Json),
_ => Err(format!("unsupported format: '{s}' (supported: json)")),
}
}
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputFormat::Json => f.write_str("json"),
}
}
}
#[derive(Args)]
struct DevArgs {
file: PathBuf,
#[arg(long)]
cst: bool,
#[arg(long)]
ast: bool,
}
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.command {
Command::Eval(args) => cmd_eval(args),
Command::Dev(args) => cmd_dev(args),
}
}
fn cmd_eval(args: EvalArgs) -> ExitCode {
let source = match fs::read_to_string(&args.file) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot read '{}': {e}", args.file.display());
return ExitCode::FAILURE;
}
};
let (green, syntax_errors) = mical_cli_parser::parse(mical_cli_lexer::tokenize(&source));
let syntax_node = mical_cli_syntax::SyntaxNode::new_root(green);
let source_file = match SourceFile::cast(syntax_node) {
Some(sf) => sf,
None => {
eprintln!("error: failed to parse source file");
return ExitCode::FAILURE;
}
};
for err in &syntax_errors {
eprintln!("syntax error: {err}");
}
let (config, config_errors) = mical_cli_config::Config::from_source_file(source_file);
for err in &config_errors {
eprintln!("config error: {err}");
}
let json_output = match (&args.query.get, &args.query.prefix) {
(Some(key), None) => {
let values: Vec<_> = config.query(key).map(|v| v.to_json()).collect();
match values.len() {
0 => serde_json::Value::Null,
1 => values.into_iter().next().unwrap(),
_ => serde_json::Value::Array(values),
}
}
(None, Some(prefix)) => {
let mut map = serde_json::Map::new();
let mut last_key: Option<String> = None;
for (k, v) in config.query_prefix(prefix) {
let json_val = v.to_json();
match last_key.as_deref() {
Some(prev) if prev == k => {
let entry = map.get_mut(k).unwrap();
match entry {
serde_json::Value::Array(arr) => arr.push(json_val),
other => {
let prev_val = std::mem::replace(other, serde_json::Value::Null);
*other = serde_json::Value::Array(vec![prev_val, json_val]);
}
}
}
_ => {
map.insert(k.to_owned(), json_val);
}
}
last_key = Some(k.to_owned());
}
serde_json::Value::Object(map)
}
(None, None) => config.to_json(),
_ => unreachable!("clap ensures mutual exclusivity"),
};
let output_str = match args.format {
OutputFormat::Json => {
serde_json::to_string_pretty(&json_output).expect("JSON serialization failed")
}
};
match args.output_path {
Some(path) => {
if let Err(e) = fs::write(&path, format!("{output_str}\n")) {
eprintln!("error: cannot write to '{}': {e}", path.display());
return ExitCode::FAILURE;
}
}
None => {
println!("{output_str}");
}
}
if !syntax_errors.is_empty() || !config_errors.is_empty() {
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
fn cmd_dev(args: DevArgs) -> ExitCode {
let source = match fs::read_to_string(&args.file) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot read '{}': {e}", args.file.display());
return ExitCode::FAILURE;
}
};
let (green, syntax_errors) = mical_cli_parser::parse(mical_cli_lexer::tokenize(&source));
let syntax_node = mical_cli_syntax::SyntaxNode::new_root(green);
let print_both = !args.cst && !args.ast;
if args.cst || print_both {
println!("=== CST ===");
print_cst(&syntax_node, 0);
}
if args.ast || print_both {
if args.cst || print_both {
println!();
}
println!("=== AST ===");
match SourceFile::cast(syntax_node.clone()) {
Some(sf) => println!("{sf:#?}"),
None => eprintln!("error: failed to cast to SourceFile"),
}
}
if !syntax_errors.is_empty() {
println!();
println!("=== Syntax Errors ===");
for err in &syntax_errors {
println!(" {err}");
}
}
ExitCode::SUCCESS
}
fn print_cst(node: &mical_cli_syntax::SyntaxNode, indent: usize) {
let padding = " ".repeat(indent);
println!("{padding}{:?}@{:?}", node.kind(), node.text_range());
for child in node.children_with_tokens() {
match child {
mical_cli_syntax::SyntaxElement::Node(n) => print_cst(&n, indent + 1),
mical_cli_syntax::SyntaxElement::Token(t) => {
let padding = " ".repeat(indent + 1);
println!("{padding}{:?}@{:?} {:?}", t.kind(), t.text_range(), t.text());
}
}
}
}