use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
fn main() -> anyhow::Result<()> {
let ontology_path = "ontology/affi-cli.ttl";
let ontology = fs::read_to_string(ontology_path)?;
let verbs = parse_verbs(&ontology)?;
generate_verb_wrappers(&verbs)?;
generate_handlers_stub(&verbs)?;
generate_verbs_mod(&verbs)?;
println!("✅ Generated {} verb wrappers and handlers", verbs.len());
Ok(())
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct Verb {
name: String,
about: String,
arguments: Vec<Argument>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct Argument {
name: String,
rust_type: String,
required: bool,
}
fn parse_verbs(ontology: &str) -> anyhow::Result<BTreeMap<String, Verb>> {
let mut verbs = BTreeMap::new();
let lines: Vec<&str> = ontology.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
if line.contains("a cnv:Verb") {
if let Some(verb_name) = extract_verb_name(&lines, i) {
let about = extract_field(&lines, i, "cnv:verbAbout");
let mut args = Vec::new();
if let Some(args_section) = extract_arguments(&lines, i, ontology) {
args = parse_arguments(&lines, &args_section, ontology);
}
verbs.insert(
verb_name.clone(),
Verb {
name: verb_name,
about: about.unwrap_or_default(),
arguments: args,
},
);
}
}
i += 1;
}
Ok(verbs)
}
fn extract_verb_name(lines: &[&str], start_idx: usize) -> Option<String> {
for i in (start_idx.saturating_sub(10))..start_idx {
if let Some(name) = extract_field_at_line(lines[i], "cnv:hasVerbName") {
return Some(
name.trim_matches(|c: char| c.is_whitespace() || c == '"' || c == ';')
.to_string(),
);
}
}
None
}
fn extract_field(lines: &[&str], start_idx: usize, field: &str) -> Option<String> {
for i in start_idx..std::cmp::min(start_idx + 20, lines.len()) {
if let Some(val) = extract_field_at_line(lines[i], field) {
return Some(val);
}
}
None
}
fn extract_field_at_line(line: &str, field: &str) -> Option<String> {
if let Some(pos) = line.find(field) {
let rest = &line[pos + field.len()..];
if let Some(start) = rest.find('"') {
if let Some(end) = rest[start + 1..].find('"') {
return Some(rest[start + 1..start + 1 + end].to_string());
}
}
}
None
}
fn extract_arguments(lines: &[&str], start_idx: usize, _ontology: &str) -> Option<Vec<String>> {
for i in start_idx..std::cmp::min(start_idx + 10, lines.len()) {
if lines[i].contains("cnv:hasArguments") {
let mut args = Vec::new();
let line = lines[i];
for word in line.split_whitespace() {
if word.starts_with("affi:") && word.ends_with("Arg") || word.ends_with(",") {
let arg_name = word
.trim_end_matches(',')
.trim_end_matches('.')
.trim_start_matches("affi:");
if !arg_name.is_empty() && arg_name.ends_with("Arg") {
args.push(arg_name.to_string());
}
}
}
if !args.is_empty() {
return Some(args);
}
}
}
None
}
fn parse_arguments(lines: &[&str], arg_names: &[String], _ontology: &str) -> Vec<Argument> {
let mut arguments = Vec::new();
for arg_name in arg_names {
if let Some(idx) = lines
.iter()
.position(|l| l.contains(&format!("{} a cnv:Argument", arg_name)))
{
let arg_line_start = idx;
let rust_name = to_rust_identifier(
extract_field(lines, arg_line_start, "cnv:hasArgumentName")
.unwrap_or_default()
.as_str(),
);
let value_type = extract_field(lines, arg_line_start, "cnv:valueType")
.unwrap_or_else(|| "String".to_string());
let required = extract_field(lines, arg_line_start, "cnv:required")
.map(|v| v.contains("true"))
.unwrap_or(true);
let rust_type = type_to_rust(&value_type, required);
arguments.push(Argument {
name: rust_name,
rust_type,
required,
});
}
}
arguments
}
fn to_rust_identifier(name: &str) -> String {
name.replace('-', "_")
}
fn type_to_rust(typ: &str, required: bool) -> String {
let base = match typ.trim() {
"String" => "String".to_string(),
"bool" | "Boolean" => "bool".to_string(),
"u32" | "u64" => typ.to_string(),
"usize" => "usize".to_string(),
"Vec<String>" => "Vec<String>".to_string(),
other => other.to_string(),
};
if required {
base
} else {
format!("Option<{}>", base)
}
}
fn generate_verb_wrappers(verbs: &BTreeMap<String, Verb>) -> anyhow::Result<()> {
for (verb_name, verb) in verbs {
let mut arg_list = String::new();
let mut handler_args = String::new();
for arg in &verb.arguments {
let param = format!("{}: {}", arg.name, arg.rust_type);
arg_list.push_str(¶m);
arg_list.push_str(", ");
handler_args.push_str(&format!("{}, ", arg.name));
}
arg_list = arg_list.trim_end_matches(", ").to_string();
handler_args = handler_args.trim_end_matches(", ").to_string();
let code = format!(
r#"// Copyright (c) 2024 Sean Chatman
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Thin verb wrapper auto-generated by codegen_maximalist_verbs. The pack is
// authoritative for the CLI *interface* only; the body delegates to a stable
// consumer-implemented handler.
//! `receipt {}` verb (auto-generated).
use clap_noun_verb::Result;
use clap_noun_verb_macros::verb;
/// {}
#[verb("{}", "receipt")]
pub fn {}({}) -> Result<()> {{
crate::handlers::{}({})
}}
"#,
verb_name, verb.about, verb_name, verb_name, arg_list, verb_name, handler_args
);
let path = format!("src/verbs/{}.rs", verb_name.replace('-', "_"));
fs::write(&path, code)?;
println!(" Generated {}", path);
}
Ok(())
}
fn generate_handlers_stub(verbs: &BTreeMap<String, Verb>) -> anyhow::Result<()> {
let mut handler_code = r#"// Handlers for all verbs (auto-generated stubs).
// Implement these to add business logic for each verb.
use crate::types::Receipt;
use anyhow::Result;
"#
.to_string();
for (verb_name, verb) in verbs {
let mut args = String::new();
for arg in &verb.arguments {
args.push_str(&format!("{}: {}, ", arg.name, arg.rust_type));
}
args = args.trim_end_matches(", ").to_string();
handler_code.push_str(&format!(
"/// {}\npub fn {}({}) -> Result<()> {{\n todo!(\"Implement {} handler\")\n}}\n\n",
verb.about,
verb_name.replace('-', "_"),
args,
verb_name
));
}
let path = "src/handlers.rs";
if Path::new(path).exists() {
println!(" ℹ️ handlers.rs already exists. Stubs generated to STDERR for reference:");
eprintln!("{}", handler_code);
} else {
fs::write(path, handler_code)?;
println!(" Generated {}", path);
}
Ok(())
}
fn generate_verbs_mod(verbs: &BTreeMap<String, Verb>) -> anyhow::Result<()> {
let mut mod_code = r#"// Module declarations for all verbs (auto-generated).
// Each verb is a thin wrapper that delegates to crate::handlers::*.
"#
.to_string();
for verb_name in verbs.keys() {
mod_code.push_str(&format!("pub mod {};\n", verb_name.replace('-', "_")));
}
fs::write("src/verbs/mod.rs", mod_code)?;
println!(" Generated src/verbs/mod.rs");
Ok(())
}