use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use aion_awl::Span;
use clap::Subcommand;
#[derive(Debug, Subcommand)]
pub(crate) enum AwlCommand {
Check {
file: PathBuf,
},
Fmt {
file: PathBuf,
},
Emit {
file: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
},
Schema {
file: PathBuf,
#[arg(long)]
r#type: Option<String>,
},
Lsp,
}
pub(crate) fn run(command: &AwlCommand) -> ExitCode {
match command {
AwlCommand::Check { file } => check_command(file),
AwlCommand::Fmt { file } => fmt_command(file),
AwlCommand::Emit { file, output } => emit_command(file, output.as_deref()),
AwlCommand::Schema { file, r#type } => schema_command(file, r#type.as_deref()),
AwlCommand::Lsp => match aion_awl_lsp::run_stdio() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("error: AWL language server failed: {error}");
ExitCode::FAILURE
}
},
}
}
fn check_command(file: &Path) -> ExitCode {
let Some(source) = read_source(file) else {
return ExitCode::FAILURE;
};
match check_source(file, &source) {
Ok(steps) => {
let noun = if steps == 1 { "step" } else { "steps" };
println!("ok: {} ({steps} {noun})", file.display());
ExitCode::SUCCESS
}
Err(diagnostics) => report(&diagnostics),
}
}
fn fmt_command(file: &Path) -> ExitCode {
let Some(source) = read_source(file) else {
return ExitCode::FAILURE;
};
match format_source(file, &source) {
Ok(formatted) => {
if let Err(error) = fs::write(file, formatted) {
eprintln!("error: failed to write {}: {error}", file.display());
return ExitCode::FAILURE;
}
println!("formatted: {}", file.display());
ExitCode::SUCCESS
}
Err(diagnostics) => report(&diagnostics),
}
}
fn emit_command(file: &Path, output: Option<&Path>) -> ExitCode {
let Some(source) = read_source(file) else {
return ExitCode::FAILURE;
};
match emit_artifact_source(file, &source) {
Ok(artifact) => {
if let Some(output) = output {
if let Err(error) = fs::write(output, &artifact.source) {
eprintln!("error: failed to write {}: {error}", output.display());
return ExitCode::FAILURE;
}
if let Err(error) = write_entry_sidecar(output, &artifact) {
eprintln!("error: failed to write generated entry metadata: {error}");
return ExitCode::FAILURE;
}
println!("emitted: {}", output.display());
} else {
print!("{}", artifact.source);
}
ExitCode::SUCCESS
}
Err(diagnostics) => report(&diagnostics),
}
}
fn schema_command(file: &Path, type_name: Option<&str>) -> ExitCode {
let Some(source) = read_source(file) else {
return ExitCode::FAILURE;
};
match schema_source(file, &source, type_name) {
Ok(schema) => {
print!("{schema}");
ExitCode::SUCCESS
}
Err(diagnostics) => report(&diagnostics),
}
}
fn check_source(file: &Path, source: &str) -> Result<usize, Vec<String>> {
let document = aion_awl::parse(source)
.map_err(|error| vec![diagnostic(file, error.span, &error.message)])?;
let errors = aion_awl::check_in(&document, document_root(file));
if errors.is_empty() {
Ok(document.steps.len())
} else {
Err(errors
.iter()
.map(|error| diagnostic(file, error.span, &error.message))
.collect())
}
}
fn format_source(file: &Path, source: &str) -> Result<String, Vec<String>> {
let document = aion_awl::parse(source)
.map_err(|error| vec![diagnostic(file, error.span, &error.message)])?;
Ok(aion_awl::print(&document))
}
#[cfg(test)]
fn emit_source(file: &Path, source: &str) -> Result<String, Vec<String>> {
Ok(emit_artifact_source(file, source)?.source)
}
fn emit_artifact_source(
file: &Path,
source: &str,
) -> Result<aion_awl::EmittedArtifact, Vec<String>> {
let document = aion_awl::parse(source)
.map_err(|error| vec![diagnostic(file, error.span, &error.message)])?;
let root = document_root(file);
let errors = aion_awl::check_in(&document, root);
if !errors.is_empty() {
return Err(errors
.iter()
.map(|error| diagnostic(file, error.span, &error.message))
.collect());
}
aion_awl::emit_artifact_in(&document, root)
.map_err(|error| vec![diagnostic(file, error.span, &error.message)])
}
fn write_entry_sidecar(
output: &Path,
artifact: &aion_awl::EmittedArtifact,
) -> Result<(), Box<dyn std::error::Error>> {
let path = output.with_extension("awl.json");
if artifact.synthesized_workflows.is_empty() {
match fs::remove_file(path) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
return Ok(());
}
fs::write(
path,
serde_json::to_vec_pretty(&artifact.project_metadata())?,
)?;
Ok(())
}
fn document_root(file: &Path) -> &Path {
match file.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent,
_ => Path::new("."),
}
}
fn schema_source(
file: &Path,
source: &str,
requested_type: Option<&str>,
) -> Result<String, Vec<String>> {
let document = aion_awl::parse(source)
.map_err(|error| vec![diagnostic(file, error.span, &error.message)])?;
let root = document_root(file);
let errors = aion_awl::check_in(&document, root);
if !errors.is_empty() {
return Err(errors
.iter()
.map(|error| diagnostic(file, error.span, &error.message))
.collect());
}
let schema = requested_type
.map_or_else(
|| aion_awl::schema_for_workflow_in(&document, root),
|name| aion_awl::schema_for_type_in(&document, root, name),
)
.map_err(|error| vec![diagnostic(file, error.span(), &error.to_string())])?;
serde_json::to_string_pretty(&schema)
.map(|json| format!("{json}\n"))
.map_err(|error| vec![diagnostic(file, document.span, &error.to_string())])
}
fn diagnostic(file: &Path, span: Span, message: &str) -> String {
format!(
"{}:{}:{}: error: {message}",
file.display(),
span.line,
span.column
)
}
fn report(diagnostics: &[String]) -> ExitCode {
for line in diagnostics {
eprintln!("{line}");
}
ExitCode::FAILURE
}
fn read_source(file: &Path) -> Option<String> {
match fs::read_to_string(file) {
Ok(source) => Some(source),
Err(error) => {
eprintln!("error: failed to read {}: {error}", file.display());
None
}
}
}
#[cfg(test)]
#[path = "awl_tests.rs"]
mod tests;