use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
mod app_spec;
mod commands;
mod error;
mod managed_block;
mod output;
mod rc;
mod scaffold_target;
mod template;
#[cfg(test)]
mod test_support;
use error::CliResult;
use scaffold_target::LONG_VERSION_TEXT;
const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
fn parse_variable_kind(value: &str) -> Result<String, String> {
match value {
"scalar" => Ok("scalar".to_string()),
"list" => Ok("list".to_string()),
_ => Err("valid values: scalar, list".to_string()),
}
}
const EXAMPLES: &str = "\x1b[1mExamples:\x1b[0m
solverforge new my-optimizer
solverforge generate entity shift --planning-variable employee_idx
solverforge generate constraint no_overlap --pair --hard
solverforge server
solverforge info
solverforge check
solverforge test
solverforge routes
solverforge config show";
#[derive(Parser)]
#[command(
name = "solverforge",
about = "CLI for scaffolding and managing SolverForge projects",
version = CLI_VERSION,
long_version = LONG_VERSION_TEXT,
infer_subcommands = true,
after_help = EXAMPLES,
)]
struct Cli {
#[arg(long, short, global = true)]
quiet: bool,
#[arg(long, short, global = true)]
verbose: bool,
#[arg(long, global = true)]
no_color: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
#[command(after_help = "Examples:\n solverforge new my-optimizer")]
New {
name: String,
#[arg(long)]
skip_git: bool,
#[arg(long)]
skip_readme: bool,
},
#[command(
after_help = "Examples:\n solverforge generate entity shift --planning-variable employee_idx\n solverforge generate fact employee\n solverforge generate constraint no_overlap --pair --hard\n solverforge generate solution schedule --score HardSoftScore\n solverforge generate data\n solverforge generate data --size large\n solverforge generate data --mode stub"
)]
Generate {
#[command(subcommand)]
resource: GenerateResource,
},
#[command(
after_help = "Examples:\n solverforge destroy entity shift\n solverforge destroy constraint no_overlap"
)]
Destroy {
#[arg(long, short)]
yes: bool,
#[command(subcommand)]
resource: DestroyResource,
},
#[command(
after_help = "Examples:\n solverforge server\n solverforge server --port 8080\n solverforge server --debug"
)]
Server {
#[arg(long, short, default_value = "7860")]
port: u16,
#[arg(long)]
debug: bool,
},
#[command(after_help = "Examples:\n solverforge info")]
Info,
#[command(after_help = "Examples:\n solverforge check")]
Check,
#[command(
after_help = "Examples:\n solverforge test\n solverforge test -- --nocapture\n solverforge test integration"
)]
Test {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
extra_args: Vec<String>,
},
#[command(after_help = "Examples:\n solverforge routes")]
Routes,
#[command(
after_help = "Examples:\n solverforge config show\n solverforge config set termination.seconds_spent_limit 60"
)]
Config {
#[command(subcommand)]
subcommand: ConfigSubcommand,
},
#[command(
after_help = "Examples:\n solverforge completions bash >> ~/.bashrc\n solverforge completions zsh >> ~/.zshrc\n solverforge completions fish > ~/.config/fish/completions/solverforge.fish"
)]
Completions {
shell: Shell,
},
}
#[derive(Subcommand)]
enum ConfigSubcommand {
Show,
Set {
key: String,
value: String,
},
}
#[derive(Subcommand)]
enum GenerateResource {
#[command(
after_help = "Examples:\n solverforge generate constraint max_hours --unary --hard\n solverforge generate constraint no_overlap --pair\n solverforge generate constraint required_skill --join --hard"
)]
Constraint {
name: String,
#[arg(long, conflicts_with = "soft")]
hard: bool,
#[arg(long, conflicts_with = "hard")]
soft: bool,
#[arg(long, conflicts_with_all = ["pair", "join", "balance", "reward"])]
unary: bool,
#[arg(long, conflicts_with_all = ["unary", "join", "balance", "reward"])]
pair: bool,
#[arg(long, conflicts_with_all = ["unary", "pair", "balance", "reward"])]
join: bool,
#[arg(long, conflicts_with_all = ["unary", "pair", "join", "reward"])]
balance: bool,
#[arg(long, conflicts_with_all = ["unary", "pair", "join", "balance"])]
reward: bool,
#[arg(long, short)]
force: bool,
#[arg(long)]
pretend: bool,
},
#[command(
after_help = "Examples:\n solverforge generate entity shift --planning-variable employee_idx\n solverforge generate entity task"
)]
Entity {
name: String,
#[arg(long = "planning-variable", value_name = "FIELD")]
planning_variable: Option<String>,
#[arg(long = "field", value_name = "NAME:TYPE")]
fields: Vec<String>,
#[arg(long, short)]
force: bool,
#[arg(long)]
pretend: bool,
},
#[command(
after_help = "Examples:\n solverforge generate fact employee\n solverforge generate fact location --field \"lat:f64\" --field \"lng:f64\""
)]
Fact {
name: String,
#[arg(long = "field", value_name = "NAME:TYPE")]
fields: Vec<String>,
#[arg(long, short)]
force: bool,
#[arg(long)]
pretend: bool,
},
#[command(
after_help = "Examples:\n solverforge generate solution schedule --score HardSoftScore"
)]
Solution {
name: String,
#[arg(long, value_name = "SCORE_TYPE", default_value = "HardSoftScore")]
score: String,
},
#[command(
after_help = "Examples:\n solverforge generate variable employee_idx --entity Shift --kind scalar --range employees --allows-unassigned\n solverforge generate variable stops --entity Route --kind list --elements visits"
)]
Variable {
field: String,
#[arg(long, value_name = "ENTITY_TYPE")]
entity: String,
#[arg(long, value_parser = parse_variable_kind)]
kind: String,
#[arg(long, value_name = "FACT_COLLECTION")]
range: Option<String>,
#[arg(long, value_name = "FACT_COLLECTION")]
elements: Option<String>,
#[arg(long, default_value_t = false)]
allows_unassigned: bool,
},
#[command(after_help = "Examples:\n solverforge generate score HardSoftDecimalScore")]
Score {
score_type: String,
},
#[command(
after_help = "Examples:\n solverforge generate data\n solverforge generate data --size large\n solverforge generate data --mode stub"
)]
Data {
#[arg(long, value_parser = ["sample", "stub"], default_value = "sample")]
mode: String,
#[arg(long, value_parser = ["small", "standard", "large"])]
size: Option<String>,
},
}
#[derive(Subcommand)]
enum DestroyResource {
Solution,
Entity {
name: String,
},
Variable {
field: String,
#[arg(long, value_name = "ENTITY_TYPE")]
entity: String,
},
Fact {
name: String,
},
Constraint {
name: String,
},
}
fn main() {
let rc = rc::load_rc().unwrap_or_default();
let cli = Cli::parse();
if cli.quiet || rc.quiet {
output::set_verbosity(0);
} else if cli.verbose {
output::set_verbosity(2);
}
if cli.no_color || rc.no_color || std::env::var("NO_COLOR").is_ok() {
output::set_no_color(true);
}
let result: CliResult = match cli.command {
Command::New {
name,
skip_git,
skip_readme,
} => commands::new::run(&name, skip_git, skip_readme, cli.quiet),
Command::Generate {
resource:
GenerateResource::Constraint {
name,
hard: _,
soft,
unary,
pair,
join,
balance,
reward,
force,
pretend,
},
} => commands::generate_constraint::run(
&name, soft, unary, pair, join, balance, reward, force, pretend,
),
Command::Generate {
resource:
GenerateResource::Entity {
name,
planning_variable,
fields,
force,
pretend,
},
} => commands::generate_domain::run_entity(
&name,
planning_variable.as_deref(),
&fields,
force,
pretend,
),
Command::Generate {
resource:
GenerateResource::Fact {
name,
fields,
force,
pretend,
},
} => commands::generate_domain::run_fact(&name, &fields, force, pretend),
Command::Generate {
resource: GenerateResource::Solution { name, score },
} => commands::generate_domain::run_solution(&name, &score),
Command::Generate {
resource:
GenerateResource::Variable {
field,
entity,
kind,
range,
elements,
allows_unassigned,
},
} => commands::generate_domain::run_variable(
&field,
&entity,
&kind,
range.as_deref(),
elements.as_deref(),
allows_unassigned,
),
Command::Generate {
resource: GenerateResource::Score { score_type },
} => commands::generate_domain::run_score(&score_type),
Command::Generate {
resource: GenerateResource::Data { mode, size },
} => commands::generate_domain::run_data(&mode, size.as_deref()),
Command::Destroy {
yes,
resource: DestroyResource::Solution,
} => commands::destroy::run_solution(yes),
Command::Destroy {
yes,
resource: DestroyResource::Entity { name },
} => commands::destroy::run_entity(&name, yes),
Command::Destroy {
yes,
resource: DestroyResource::Variable { field, entity },
} => commands::destroy::run_variable(&field, &entity, yes),
Command::Destroy {
yes,
resource: DestroyResource::Fact { name },
} => commands::destroy::run_fact(&name, yes),
Command::Destroy {
yes,
resource: DestroyResource::Constraint { name },
} => commands::destroy::run_constraint(&name, yes),
Command::Server { port, debug } => commands::server::run(port, debug),
Command::Info => commands::info::run(),
Command::Check => commands::check::run(),
Command::Test { extra_args } => commands::test::run(&extra_args),
Command::Routes => commands::routes::run(),
Command::Config {
subcommand: ConfigSubcommand::Show,
} => commands::config::run_show(),
Command::Config {
subcommand: ConfigSubcommand::Set { key, value },
} => commands::config::run_set(&key, &value),
Command::Completions { shell } => {
let mut cmd = Cli::command();
clap_complete::generate(shell, &mut cmd, "solverforge", &mut std::io::stdout());
Ok(())
}
};
if let Err(e) = result {
output::print_error(&e.to_string());
std::process::exit(1);
}
}