use clap::{Parser, Subcommand};
use masl_compiler::{CompileError, compile};
use masl_diagnostics::diagnostic::Diagnostic;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(
name = "maslc",
about = "MASL compiler — Maduka Authorization Specification Language",
version
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Build {
path: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
},
Check {
path: PathBuf,
},
Fmt {
path: PathBuf,
#[arg(long)]
check: bool,
#[arg(long)]
diff: bool,
},
Lint {
path: PathBuf,
#[arg(long)]
config: Option<PathBuf>,
},
Lsp {
#[arg(long)]
port: Option<u16>,
},
Explain {
code: String,
},
Doc {
path: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
},
}
fn print_diagnostics(diags: &[Diagnostic], source: &str, filepath: &str) {
for diag in diags {
let severity_str = match diag.severity {
masl_diagnostics::severity::Severity::Error => "error",
masl_diagnostics::severity::Severity::Warning => "warning",
masl_diagnostics::severity::Severity::Note => "note",
masl_diagnostics::severity::Severity::Help => "help",
};
println!("{}[{}]: {}", severity_str, diag.code, diag.message);
println!(
" --> {}:{}:{}",
filepath, diag.primary.span.line, diag.primary.span.column
);
let lines: Vec<&str> = source.lines().collect();
if diag.primary.span.line > 0 && diag.primary.span.line <= lines.len() {
let line_idx = diag.primary.span.line - 1;
let line = lines[line_idx];
println!(" |");
println!("{:4} | {}", diag.primary.span.line, line);
println!(
" | {}{}",
" ".repeat(diag.primary.span.column - 1),
"^".repeat(
diag.primary
.span
.end
.saturating_sub(diag.primary.span.start)
.max(1)
)
);
}
if let Some(ref help) = diag.help {
println!(" = help: {}", help);
}
println!();
}
}
fn compile_file(file: &Path, output: 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(());
}
};
match compile(&source) {
Ok(bytes) => {
let out_path = if let Some(out) = output {
if out.is_dir() {
let mut p = out.to_path_buf();
let file_name = file.file_name().unwrap_or_default();
p.push(file_name);
p.set_extension("mdkc");
p
} else {
out.to_path_buf()
}
} else {
let mut p = file.to_path_buf();
p.set_extension("mdkc");
p
};
if let Err(e) = fs::write(&out_path, bytes) {
eprintln!("Error writing output {}: {}", out_path.display(), e);
return Err(());
}
println!("Successfully compiled {} to {}", file.display(), out_path.display());
Ok(())
}
Err(e) => {
let diags = match e {
CompileError::ParseFailed(d) => d,
CompileError::AnalysisFailed(d) => d,
CompileError::ValidationFailed(d) => d,
CompileError::LoweringFailed(d) => d,
CompileError::BytecodeFailed(d) => d,
};
print_diagnostics(&diags, &source, file.to_str().unwrap_or(""));
Err(())
}
}
}
fn check_file(file: &Path) -> Result<(), ()> {
let source = match fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file {}: {}", file.display(), e);
return Err(());
}
};
match compile(&source) {
Ok(_) => {
println!("{} is correct.", file.display());
Ok(())
}
Err(e) => {
let diags = match e {
CompileError::ParseFailed(d) => d,
CompileError::AnalysisFailed(d) => d,
CompileError::ValidationFailed(d) => d,
CompileError::LoweringFailed(d) => d,
CompileError::BytecodeFailed(d) => d,
};
print_diagnostics(&diags, &source, file.to_str().unwrap_or(""));
Err(())
}
}
}
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 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 type_parts: Vec<String> = rel.types.iter().map(|t| {
match &t.relation {
Some(r) => format!("`{}::{}`", t.target_name, r),
None => format!("`{}`", t.target_name),
}
}).collect();
md.push_str(&format!("| `{}` | {} |\n", rel_name, type_parts.join(" \\| ")));
}
md.push_str("\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 type_parts: Vec<String> = rel.types.iter().map(|t| {
match &t.relation {
Some(r) => format!("`{}::{}`", t.target_name, r),
None => format!("`{}`", t.target_name),
}
}).collect();
md.push_str(&format!("| `{}` | {} |\n", rel_name, type_parts.join(" \\| ")));
}
md.push_str("\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 {
fn format_expr_doc(expr: &masl_hir::document::Expr) -> String {
match expr {
masl_hir::document::Expr::Relation(r) => format!("`{}`", r),
masl_hir::document::Expr::Traversal { steps, .. } => format!("`{}`", steps.join("::")),
masl_hir::document::Expr::Not { operand, .. } => format!("`!{}`", format_expr_doc(operand)),
masl_hir::document::Expr::Union { operands, .. } => {
let parts: Vec<String> = operands.iter().map(format_expr_doc).collect();
parts.join(" \\| ")
}
masl_hir::document::Expr::Intersection { operands, .. } => {
let parts: Vec<String> = operands.iter().map(format_expr_doc).collect();
parts.join(" & ")
}
masl_hir::document::Expr::Difference { left, right, .. } => {
format!("{} - {}", format_expr_doc(left), format_expr_doc(right))
}
}
}
md.push_str(&format!("| `{}` | {} |\n", grant_name, format_expr_doc(&grant.expression)));
}
md.push_str("\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 {
fn format_expr_doc(expr: &masl_hir::document::Expr) -> String {
match expr {
masl_hir::document::Expr::Relation(r) => format!("`{}`", r),
masl_hir::document::Expr::Traversal { steps, .. } => format!("`{}`", steps.join("::")),
masl_hir::document::Expr::Not { operand, .. } => format!("`!{}`", format_expr_doc(operand)),
masl_hir::document::Expr::Union { operands, .. } => {
let parts: Vec<String> = operands.iter().map(format_expr_doc).collect();
parts.join(" \\| ")
}
masl_hir::document::Expr::Intersection { operands, .. } => {
let parts: Vec<String> = operands.iter().map(format_expr_doc).collect();
parts.join(" & ")
}
masl_hir::document::Expr::Difference { left, right, .. } => {
format!("{} - {}", format_expr_doc(left), format_expr_doc(right))
}
}
}
md.push_str(&format!("| `{}` | {} |\n", name, format_expr_doc(expr)));
}
md.push_str("\n");
}
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 main() {
let cli = Cli::parse();
match cli.command {
Command::Build { path, output } => {
if path.is_dir() {
if let Some(ref out) = output {
let _ = fs::create_dir_all(out);
}
let mut success = true;
let read_dir = match fs::read_dir(&path) {
Ok(rd) => rd,
Err(e) => {
eprintln!("Error reading directory {}: {}", path.display(), e);
std::process::exit(1);
}
};
for entry in read_dir {
let entry = entry.unwrap();
let file_path = entry.path();
if file_path.is_file() && file_path.extension().map_or(false, |ext| ext == "mdk") {
if compile_file(&file_path, output.as_deref()).is_err() {
success = false;
}
}
}
if !success {
std::process::exit(1);
}
} else {
if compile_file(&path, output.as_deref()).is_err() {
std::process::exit(1);
}
}
}
Command::Check { path } => {
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);
std::process::exit(1);
}
};
for entry in read_dir {
let entry = entry.unwrap();
let file_path = entry.path();
if file_path.is_file() && file_path.extension().map_or(false, |ext| ext == "mdk") {
if check_file(&file_path).is_err() {
success = false;
}
}
}
if !success {
std::process::exit(1);
}
} else {
if check_file(&path).is_err() {
std::process::exit(1);
}
}
}
Command::Explain { code } => match code.as_str() {
"E0100" => {
println!("Error[E0100]: Unexpected token or syntax error.");
println!(
"This occurs when the parser encounters tokens that do not match the expected PEG rules."
);
}
"E0201" => {
println!("Error[E0201]: Duplicate resource or subject declaration.");
println!(
"Each subject and resource must be declared only once per schema namespace."
);
}
"E0202" => {
println!("Error[E0202]: Circular relation hierarchy or alias dependency.");
println!("The compiler detected a path dependency cycle that could cause infinite loops at runtime.");
}
c => {
println!("No detailed explanation available for code: {}", c);
}
},
Command::Lsp { port } => {
if port.is_some() {
eprintln!("TCP transport not supported yet. Starting stdio...");
}
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
masl_lsp::start_lsp_server().await;
});
std::process::exit(0);
}
Command::Doc { path, output } => {
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);
std::process::exit(1);
}
};
for entry in read_dir {
let entry = entry.unwrap();
let file_path = entry.path();
if file_path.is_file() && file_path.extension().map_or(false, |ext| ext == "mdk") {
if doc_file(&file_path, output.as_deref()).is_err() {
success = false;
}
}
}
if !success {
std::process::exit(1);
}
} else {
if doc_file(&path, output.as_deref()).is_err() {
std::process::exit(1);
}
}
}
Command::Fmt { path, check, diff } => {
let source = match fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
match masl_parser::parse(&source) {
Ok(ast) => {
let formatted = masl_formatter::format(&ast);
if formatted == source {
if !check {
println!("Already formatted.");
}
std::process::exit(0);
}
if check {
println!("File is not formatted.");
std::process::exit(1);
}
if diff {
println!("--- original");
println!("+++ formatted");
for line in formatted.lines() {
println!("+{}", line);
}
std::process::exit(0);
}
if let Err(e) = fs::write(&path, formatted) {
eprintln!("Error writing file: {}", e);
std::process::exit(1);
}
println!("Successfully formatted {}", path.display());
}
Err(e) => {
print_diagnostics(&e, &source, path.to_str().unwrap_or(""));
std::process::exit(1);
}
}
}
Command::Lint { path, config: _config } => {
let source = match fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
match masl_parser::parse(&source) {
Ok(ast) => match masl_hir::analyze(ast) {
Ok(hir) => {
let lint_diags = masl_linter::lint(&hir);
if lint_diags.is_empty() {
println!("No lint warnings found.");
std::process::exit(0);
} else {
print_diagnostics(&lint_diags, &source, path.to_str().unwrap_or(""));
std::process::exit(1);
}
}
Err(e) => {
print_diagnostics(&e, &source, path.to_str().unwrap_or(""));
std::process::exit(1);
}
},
Err(e) => {
print_diagnostics(&e, &source, path.to_str().unwrap_or(""));
std::process::exit(1);
}
}
}
}
}