use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use aion_awl::doc::{DocumentDoc, SchemaFiles};
use aion_awl::{Document, TypeBody};
use clap::ValueEnum;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum DocFormat {
Html,
Json,
Markdown,
Plain,
}
const ARCHIVE_EXTENSION: &str = "aion";
pub(crate) fn run(file: &Path, format: DocFormat, output: Option<&Path>) -> ExitCode {
let model = match load(file) {
Ok(model) => model,
Err(message) => {
eprintln!("{}: error: {message}", file.display());
return ExitCode::FAILURE;
}
};
let rendered = render(&model, format);
let rendered = match rendered {
Ok(rendered) => rendered,
Err(message) => {
eprintln!("{}: error: {message}", file.display());
return ExitCode::FAILURE;
}
};
let Some(path) = output else {
print!("{rendered}");
return ExitCode::SUCCESS;
};
match fs::write(path, rendered.as_bytes()) {
Ok(()) => {
println!("ok: {} -> {}", file.display(), path.display());
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("{}: error: {error}", path.display());
ExitCode::FAILURE
}
}
}
pub(crate) fn render(model: &DocumentDoc, format: DocFormat) -> Result<String, String> {
Ok(match format {
DocFormat::Html => aion_awl::doc::html::render(model),
DocFormat::Json => json(model)?,
DocFormat::Markdown => aion_awl::doc::markdown::render(model),
DocFormat::Plain => plain(model),
})
}
pub(crate) fn json(model: &DocumentDoc) -> Result<String, String> {
let mut rendered = serde_json::to_string_pretty(model)
.map_err(|error| format!("the documentation model could not serialise: {error}"))?;
rendered.push('\n');
Ok(rendered)
}
fn plain(model: &DocumentDoc) -> String {
use std::fmt::Write as _;
let mut out = format!("{} {}\n", model.family, model.name);
if let Some(prose) = &model.prose {
if let Some(narration) = &prose.narration {
let _ = writeln!(out, "\n{narration}");
}
out.push('\n');
for step in aion_awl::doc::plain::steps(&prose.graph) {
let indent = " ".repeat(step.depth);
let _ = writeln!(out, "{indent}{}. {}", step.number, step.name);
if let Some(sentence) = &step.documentation {
let _ = writeln!(out, "{indent} {sentence}");
}
for inside in &step.contains {
let _ = writeln!(out, "{indent} it {inside}");
}
for lead in &step.leads_to {
let _ = writeln!(out, "{indent} -> {lead}");
}
}
} else {
out.push_str("\nno authored document is recorded for this revision\n");
}
out
}
fn load(file: &Path) -> Result<DocumentDoc, String> {
if file.extension().is_some_and(|ext| ext == ARCHIVE_EXTENSION) {
return from_archive(file);
}
from_document(file)
}
fn from_document(file: &Path) -> Result<DocumentDoc, String> {
let source = fs::read_to_string(file).map_err(|error| error.to_string())?;
let parsed = aion_awl::parse(&source).map_err(|error| error.message.clone())?;
let root = file.parent().unwrap_or_else(|| Path::new("."));
let mut schemas: SchemaFiles = BTreeMap::new();
for path in imported_schema_paths(&parsed) {
let full: PathBuf = root.join(&path);
let text =
fs::read_to_string(&full).map_err(|error| format!("{}: {error}", full.display()))?;
schemas.insert(path, text);
}
aion_awl::doc::derive(&source, &schemas).map_err(|error| error.to_string())
}
fn from_archive(file: &Path) -> Result<DocumentDoc, String> {
let package =
aion_package::Package::load_from_path(file, aion_package::ExtractionLimits::unbounded())
.map_err(|error| error.to_string())?;
let workflow_type = package.manifest().entry_module.clone();
aion_awl::doc::from_package(&package, &workflow_type).map_err(|error| error.to_string())
}
fn imported_schema_paths(document: &Document) -> Vec<String> {
document
.types
.iter()
.filter_map(|declaration| match &declaration.body {
TypeBody::SchemaImport { path, .. } => Some(path.clone()),
_ => None,
})
.collect()
}
#[cfg(test)]
#[path = "awl_doc_tests.rs"]
mod awl_doc_tests;