use std::fs;
use std::path::{Path, PathBuf};
use std::process::exit;
use masl_hir::document::Expr;
use crate::diagnostics::print_diagnostics;
fn format_expr(expr: &Expr) -> String {
match expr {
Expr::Relation(r) => format!("`{}`", r),
Expr::Traversal { steps, .. } => format!("`{}`", steps.join("::")),
Expr::Not { operand, .. } => format!("`!{}`", format_expr(operand)),
Expr::Union { operands, .. } => operands
.iter()
.map(format_expr)
.collect::<Vec<_>>()
.join(" \\| "),
Expr::Intersection { operands, .. } => operands
.iter()
.map(format_expr)
.collect::<Vec<_>>()
.join(" & "),
Expr::Difference { left, right, .. } => {
format!("{} - {}", format_expr(left), format_expr(right))
}
}
}
pub fn doc_file(file: &Path, output_dir: Option<&Path>) -> Result<(), ()> {
let source = match fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file {}: {}", file.display(), e);
return Err(());
}
};
let ast = match masl_parser::parse(&source) {
Ok(a) => a,
Err(e) => {
print_diagnostics(&e, &source, file.to_str().unwrap_or(""));
return Err(());
}
};
let hir = match masl_hir::analyze(ast) {
Ok(h) => h,
Err(e) => {
print_diagnostics(&e, &source, file.to_str().unwrap_or(""));
return Err(());
}
};
let md = build_markdown(&hir);
let out_dir = output_dir.unwrap_or(&PathBuf::from("./docs")).to_path_buf();
if let Err(e) = fs::create_dir_all(&out_dir) {
eprintln!(
"Error creating output directory {}: {}",
out_dir.display(),
e
);
return Err(());
}
let out_file = out_dir.join(format!("{}.md", hir.namespace));
if let Err(e) = fs::write(&out_file, md) {
eprintln!(
"Error writing documentation file {}: {}",
out_file.display(),
e
);
return Err(());
}
println!(
"Successfully generated documentation at {}",
out_file.display()
);
Ok(())
}
fn build_markdown(hir: &masl_hir::document::HirDocument) -> String {
let mut md = String::new();
md.push_str(&format!("# Namespace: `{}`\n\n", hir.namespace));
md.push_str(&format!("**Schema Version:** `{}`\n\n", hir.schema_version));
md.push_str("Generated automatically by `maslc doc`.\n\n---\n\n");
if !hir.subjects.is_empty() {
md.push_str("## Subjects\n\n");
for (name, subject) in &hir.subjects {
md.push_str(&format!("### Subject: `{}`\n\n", name));
if !subject.relations.is_empty() {
md.push_str("| Relation | Allowed Types |\n");
md.push_str("| :--- | :--- |\n");
for (rel_name, rel) in &subject.relations {
let types = format_allowed_types(rel);
md.push_str(&format!("| `{}` | {} |\n", rel_name, types));
}
md.push('\n');
} else {
md.push_str("*No relations declared.*\n\n");
}
}
md.push_str("---\n\n");
}
if !hir.resources.is_empty() {
md.push_str("## Resources\n\n");
for (name, resource) in &hir.resources {
md.push_str(&format!("### Resource: `{}`\n\n", name));
md.push_str("#### Relations\n\n");
if !resource.relations.is_empty() {
md.push_str("| Relation | Allowed Types |\n");
md.push_str("| :--- | :--- |\n");
for (rel_name, rel) in &resource.relations {
let types = format_allowed_types(rel);
md.push_str(&format!("| `{}` | {} |\n", rel_name, types));
}
md.push('\n');
} else {
md.push_str("*No relations declared.*\n\n");
}
md.push_str("#### Grants (Permissions)\n\n");
if !resource.grants.is_empty() {
md.push_str("| Grant | Expression |\n");
md.push_str("| :--- | :--- |\n");
for (grant_name, grant) in &resource.grants {
md.push_str(&format!(
"| `{}` | {} |\n",
grant_name,
format_expr(&grant.expression)
));
}
md.push('\n');
} else {
md.push_str("*No grants declared.*\n\n");
}
}
md.push_str("---\n\n");
}
if !hir.aliases.is_empty() {
md.push_str("## Aliases\n\n");
md.push_str("| Alias | Expression |\n");
md.push_str("| :--- | :--- |\n");
for (name, expr) in &hir.aliases {
md.push_str(&format!("| `{}` | {} |\n", name, format_expr(expr)));
}
md.push('\n');
}
md
}
fn format_allowed_types(rel: &masl_hir::document::Relation) -> String {
rel.types
.iter()
.map(|t| match &t.relation {
Some(r) => format!("`{}::{}`", t.target_name, r),
None => format!("`{}`", t.target_name),
})
.collect::<Vec<_>>()
.join(" \\| ")
}
pub fn run(path: PathBuf, output: Option<PathBuf>) {
if path.is_dir() {
let mut success = true;
let read_dir = match fs::read_dir(&path) {
Ok(rd) => rd,
Err(e) => {
eprintln!("Error reading directory {}: {}", path.display(), e);
exit(1);
}
};
for entry in read_dir {
let file_path = entry.unwrap().path();
if file_path.is_file()
&& file_path.extension().is_some_and(|ext| ext == "mdk")
&& doc_file(&file_path, output.as_deref()).is_err()
{
success = false;
}
}
if !success {
exit(1);
}
} else if doc_file(&path, output.as_deref()).is_err() {
exit(1);
}
}