use std::ffi::OsString;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use acativa::SchemaId;
use acativa::runtime::protocol::serve;
use acativa::store::{StoreDialect, StrategyStore, schema_sql};
const HELP: &str = "\
Acativa private runtime
Usage:
acativa schema
acativa serve --database PATH --workflow-schema ID --projection-schema ID --ephemeral-prefix PREFIX
acativa --help
schema exports the canonical empty-schema SQL; the host owns migration.
serve opens an existing migrated database and uses private NDJSON stdin/stdout.
All serve options are required. No effects are executed by this process.
";
enum Command {
Help,
Schema,
Serve {
database: PathBuf,
dialect: StoreDialect,
},
}
fn text(value: OsString) -> Result<String, ()> {
value.into_string().map_err(|_| ())
}
fn parse(mut args: impl Iterator<Item = OsString>) -> Result<Command, ()> {
let command = text(args.next().ok_or(())?)?;
match command.as_str() {
"--help" | "-h" | "help" if args.next().is_none() => Ok(Command::Help),
"schema" if args.next().is_none() => Ok(Command::Schema),
"serve" => {
let mut database = None;
let mut workflow_schema = None;
let mut projection_schema = None;
let mut ephemeral_prefix = None;
while let Some(option) = args.next() {
let option = text(option)?;
let value = args.next().filter(|value| !value.is_empty()).ok_or(())?;
match option.as_str() {
"--database" if database.is_none() => database = Some(PathBuf::from(value)),
"--workflow-schema" if workflow_schema.is_none() => {
workflow_schema = Some(SchemaId::new(text(value)?));
}
"--projection-schema" if projection_schema.is_none() => {
projection_schema = Some(SchemaId::new(text(value)?));
}
"--ephemeral-prefix" if ephemeral_prefix.is_none() => {
ephemeral_prefix = Some(text(value)?);
}
_ => return Err(()),
}
}
Ok(Command::Serve {
database: database.ok_or(())?,
dialect: StoreDialect {
state_root: String::new(),
workflow_schema: workflow_schema.ok_or(())?,
projection_schema: projection_schema.ok_or(())?,
ephemeral_definition_prefix: Some(ephemeral_prefix.ok_or(())?),
},
})
}
_ => Err(()),
}
}
fn run(command: Command) -> Result<(), &'static str> {
match command {
Command::Help => io::stdout()
.lock()
.write_all(HELP.as_bytes())
.map_err(|_| "acativa: output unavailable\n"),
Command::Schema => {
let sql = schema_sql().map_err(|_| "acativa: runtime refused\n")?;
io::stdout()
.lock()
.write_all(sql.as_bytes())
.map_err(|_| "acativa: output unavailable\n")
}
Command::Serve { database, dialect } => {
let store = StrategyStore::open_existing_database(&database, &dialect)
.map_err(|_| "acativa: runtime refused\n")?;
store.suppress_transition_intents();
serve(io::stdin().lock(), io::stdout().lock(), store)
.map_err(|_| "acativa: pipe unavailable\n")
}
}
}
fn main() -> ExitCode {
let result = parse(std::env::args_os().skip(1))
.map_err(|_| "acativa: invalid usage; use --help\n")
.and_then(run);
match result {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
let _ = io::stderr().lock().write_all(message.as_bytes());
ExitCode::FAILURE
}
}
}