use anyhow::{Context, Result};
use clap::{Args, Parser, Subcommand};
use motto::prelude::*;
use std::path::PathBuf;
use tracing::{Level, info};
use tracing_subscriber::FmtSubscriber;
#[derive(Parser)]
#[command(name = "motto")]
#[command(author, version, about = "Generate multi-platform SDKs from Rust schema", long_about = None)]
struct Cli {
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init(InitArgs),
Generate(GenerateArgs),
Check(CheckArgs),
Lock(LockArgs),
Watch(WatchArgs),
}
#[derive(Args)]
struct InitArgs {
#[arg(short, long)]
path: Option<PathBuf>,
#[arg(short, long, default_value = "minimal")]
template: String,
}
#[derive(Args)]
struct GenerateArgs {
#[arg(short, long, default_value = "src/schema.rs")]
schema: PathBuf,
#[arg(short, long, default_value = "generated")]
output: PathBuf,
#[arg(short, long, default_value = "all")]
targets: String,
#[arg(short, long)]
force: bool,
#[arg(long)]
wasm: bool,
#[arg(long)]
native: bool,
}
#[derive(Args)]
struct CheckArgs {
#[arg(short, long, default_value = "src/schema.rs")]
schema: PathBuf,
#[arg(short, long, default_value = "motto.lock")]
lock: PathBuf,
#[arg(long)]
strict: bool,
}
#[derive(Args)]
struct LockArgs {
#[arg(short, long, default_value = "src/schema.rs")]
schema: PathBuf,
#[arg(short, long, default_value = "motto.lock")]
lock: PathBuf,
#[arg(short, long, default_value = "patch")]
bump: String,
}
#[derive(Args)]
struct WatchArgs {
#[arg(short, long, default_value = "src/schema.rs")]
schema: PathBuf,
#[arg(short, long, default_value = "generated")]
output: PathBuf,
#[arg(short, long, default_value = "all")]
targets: String,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let level = if cli.verbose {
Level::DEBUG
} else {
Level::INFO
};
let subscriber = FmtSubscriber::builder()
.with_max_level(level)
.with_target(false)
.finish();
tracing::subscriber::set_global_default(subscriber).context("Failed to set up logging")?;
match cli.command {
Commands::Init(args) => cmd_init(args),
Commands::Generate(args) => cmd_generate(args),
Commands::Check(args) => cmd_check(args),
Commands::Lock(args) => cmd_lock(args),
Commands::Watch(args) => cmd_watch(args),
}
}
fn cmd_init(args: InitArgs) -> Result<()> {
let path = args.path.unwrap_or_else(|| PathBuf::from("."));
info!(
"Initializing motto project in {:?} with template '{}'",
path, args.template
);
std::fs::create_dir_all(path.join("src"))?;
std::fs::create_dir_all(path.join("generated"))?;
let schema_content = include_str!("../templates/example_schema.rs");
std::fs::write(path.join("src/schema.rs"), schema_content)?;
let lock_content = motto::ir::lock::MottoLock::new().to_string()?;
std::fs::write(path.join("motto.lock"), lock_content)?;
info!("✓ Created motto project structure");
info!(" - src/schema.rs: Your schema definitions");
info!(" - motto.lock: Version tracking file");
info!(" - generated/: Output directory for SDK code");
info!("\nRun 'motto generate' to create your first SDK");
Ok(())
}
fn cmd_generate(args: GenerateArgs) -> Result<()> {
info!("Generating SDKs from {:?}", args.schema);
let parser = SchemaParser::new();
let schema = parser.parse_file(&args.schema)?;
let fingerprint = SchemaFingerprint::compute(&schema);
info!("Schema fingerprint: {}", fingerprint.short());
let ir_gen = IrGenerator::new();
let manifest = ir_gen.generate(&schema)?;
let targets: Vec<&str> = if args.targets == "all" {
#[allow(clippy::vec_init_then_push)]
let t = {
let mut t = Vec::new();
#[cfg(feature = "emitter-typescript")]
t.push("typescript");
#[cfg(feature = "emitter-swift")]
t.push("swift");
#[cfg(feature = "emitter-kotlin")]
t.push("kotlin");
#[cfg(feature = "emitter-unity")]
t.push("unity");
t.push("rust");
t
};
t
} else {
args.targets.split(',').map(|s| s.trim()).collect()
};
std::fs::create_dir_all(&args.output)?;
for target in targets {
info!("Generating {} SDK...", target);
let config = motto::emitters::EmitterConfig {
output_dir: args.output.clone(),
wasm_bindings: args.wasm,
native_bindings: args.native,
manifest: manifest.clone(),
};
match target {
#[cfg(feature = "emitter-typescript")]
"typescript" => motto::emitters::typescript::emit(&config)?,
#[cfg(feature = "emitter-swift")]
"swift" => motto::emitters::swift::emit(&config)?,
#[cfg(feature = "emitter-kotlin")]
"kotlin" => motto::emitters::kotlin::emit(&config)?,
#[cfg(feature = "emitter-unity")]
"unity" => motto::emitters::unity::emit(&config)?,
"rust" => motto::emitters::rust::emit(&config)?,
_ => anyhow::bail!(
"Unknown target: {}. Make sure the corresponding emitter feature is enabled.",
target
),
}
}
info!("✓ SDK generation complete!");
info!(" Output: {:?}", args.output);
Ok(())
}
fn cmd_check(args: CheckArgs) -> Result<()> {
info!("Checking schema compatibility...");
let parser = SchemaParser::new();
let schema = parser.parse_file(&args.schema)?;
let fingerprint = SchemaFingerprint::compute(&schema);
let lock_content = std::fs::read_to_string(&args.lock).context("Failed to read motto.lock")?;
let lock = motto::ir::lock::MottoLock::parse_str(&lock_content)?;
if fingerprint.hash() == lock.fingerprint() {
info!(
"✓ Schema matches motto.lock (fingerprint: {})",
fingerprint.short()
);
Ok(())
} else {
let msg = format!(
"Schema fingerprint mismatch!\n Current: {}\n Locked: {}\n\nRun 'motto lock' to update",
fingerprint.short(),
&lock.fingerprint()[..16]
);
if args.strict {
anyhow::bail!(msg);
} else {
eprintln!("⚠ {}", msg);
Ok(())
}
}
}
fn cmd_lock(args: LockArgs) -> Result<()> {
info!("Updating motto.lock...");
let parser = SchemaParser::new();
let schema = parser.parse_file(&args.schema)?;
let fingerprint = SchemaFingerprint::compute(&schema);
let mut lock = if args.lock.exists() {
let content = std::fs::read_to_string(&args.lock)?;
motto::ir::lock::MottoLock::parse_str(&content)?
} else {
motto::ir::lock::MottoLock::new()
};
match args.bump.as_str() {
"major" => lock.bump_major(),
"minor" => lock.bump_minor(),
"patch" => lock.bump_patch(),
_ => anyhow::bail!("Invalid bump type: {}", args.bump),
}
lock.set_fingerprint(fingerprint.hash());
std::fs::write(&args.lock, lock.to_string()?)?;
info!("✓ Updated motto.lock");
info!(" Version: {}", lock.version());
info!(" Fingerprint: {}", fingerprint.short());
Ok(())
}
fn cmd_watch(args: WatchArgs) -> Result<()> {
info!("Watching {:?} for changes...", args.schema);
info!("Press Ctrl+C to stop");
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::sync::mpsc::channel;
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(
move |res| {
if let Ok(event) = res {
let _ = tx.send(event);
}
},
Config::default(),
)?;
watcher.watch(&args.schema, RecursiveMode::NonRecursive)?;
loop {
match rx.recv() {
Ok(_event) => {
info!("Schema changed, regenerating...");
let gen_args = GenerateArgs {
schema: args.schema.clone(),
output: args.output.clone(),
targets: args.targets.clone(),
force: true,
wasm: false,
native: false,
};
if let Err(e) = cmd_generate(gen_args) {
eprintln!("Generation error: {}", e);
}
}
Err(e) => {
eprintln!("Watch error: {}", e);
break;
}
}
}
Ok(())
}