use std::path::PathBuf;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "fslite",
// The `Action::Help` variant below would otherwise collide with
// clap's auto-generated `help` subcommand (which prints the same
// content as the `--help` flag). Disabling that subcommand leaves
// `--help`/`-h` working (which users reach for by default) while
// letting `fslite help [<verb>]` print the per-verb table.
disable_help_subcommand = true
)]
pub struct Cli {
#[arg(long, global = true, conflicts_with_all = ["memory", "server", "filesystem"])]
pub db: Option<PathBuf>,
#[arg(long, global = true, conflicts_with_all = ["db", "server", "filesystem"])]
pub memory: bool,
#[arg(long, global = true, conflicts_with_all = ["db", "memory", "filesystem"])]
pub server: Option<String>,
#[arg(
long,
global = true,
env = "FSLITE_TOKEN",
hide_env_values = true,
requires = "server"
)]
pub token: Option<String>,
#[arg(long, global = true, conflicts_with_all = ["db", "memory", "server"])]
pub filesystem: Option<String>,
#[arg(long, global = true)]
pub workspace: Option<String>,
#[arg(long, global = true)]
pub create_workspace: bool,
#[arg(long, global = true)]
pub repl: bool,
#[arg(long, global = true)]
pub json: bool,
#[command(subcommand)]
pub action: Option<Action>,
}
#[derive(Subcommand)]
pub enum Action {
Create {
name: String,
#[arg(short = 'f', long)]
file: PathBuf,
#[arg(short = 'w', long = "workspace-name")]
workspace_name: Option<String>,
},
Delete {
name: String,
#[arg(short = 'y', long)]
yes: bool,
},
Use {
name: String,
#[arg(short = 'w', long = "workspace-name")]
workspace_name: String,
},
Help {
verb: Option<String>,
},
#[command(external_subcommand)]
Verb(Vec<String>),
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Cli {
let mut full = vec!["fslite"];
full.extend_from_slice(args);
Cli::try_parse_from(full).unwrap_or_else(|err| panic!("failed to parse {args:?}: {err}"))
}
#[test]
fn legacy_create_workspace_flag_has_no_positional_action() {
let cli = parse(&["--db", "X", "--create-workspace"]);
assert!(cli.create_workspace);
assert!(cli.action.is_none());
}
#[test]
fn legacy_repl_flag_has_no_positional_action() {
let cli = parse(&["--db", "X", "--workspace", "ID", "--repl"]);
assert!(cli.repl);
assert_eq!(cli.workspace.as_deref(), Some("ID"));
assert!(cli.action.is_none());
}
#[test]
fn legacy_verb_with_plain_args_is_captured_verbatim() {
let cli = parse(&["--db", "X", "--workspace", "ID", "mkdir", "/docs"]);
match cli.action {
Some(Action::Verb(words)) => assert_eq!(words, vec!["mkdir", "/docs"]),
other => panic!(
"expected Action::Verb, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn legacy_json_flag_before_verb_still_applies_globally() {
let cli = parse(&["--db", "X", "--workspace", "ID", "--json", "usage"]);
assert!(cli.json);
match cli.action {
Some(Action::Verb(words)) => assert_eq!(words, vec!["usage"]),
other => panic!(
"expected Action::Verb, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn verb_args_that_look_like_flags_pass_through_untouched() {
let cli = parse(&[
"--db",
"X",
"--workspace",
"ID",
"write",
"/docs/a.txt",
"--text=hello cli",
]);
match cli.action {
Some(Action::Verb(words)) => {
assert_eq!(words, vec!["write", "/docs/a.txt", "--text=hello cli"]);
}
other => panic!(
"expected Action::Verb, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn verb_args_containing_an_unrelated_long_flag_name_pass_through_untouched() {
let cli = parse(&["mkdir", "--parents", "/a/b/c"]);
match cli.action {
Some(Action::Verb(words)) => assert_eq!(words, vec!["mkdir", "--parents", "/a/b/c"]),
other => panic!(
"expected Action::Verb, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn help_subcommand_matches_bare_help() {
let cli = parse(&["help"]);
match cli.action {
Some(Action::Help { verb }) => assert!(verb.is_none(), "got verb={verb:?}"),
other => panic!(
"expected Action::Help, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn help_subcommand_with_verb_captures_verb_name() {
let cli = parse(&["help", "write"]);
match cli.action {
Some(Action::Help { verb }) => assert_eq!(verb.as_deref(), Some("write")),
other => panic!(
"expected Action::Help, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn help_subcommand_consumes_unknown_verb_rather_than_externalizing() {
let cli = parse(&["help", "nonexistent"]);
match cli.action {
Some(Action::Help { verb }) => assert_eq!(verb.as_deref(), Some("nonexistent")),
other => panic!(
"expected Action::Help, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn create_subcommand_parses_name_file_and_workspace_name() {
let cli = parse(&["create", "main", "-f", "main.db", "-w", "primary"]);
match cli.action {
Some(Action::Create {
name,
file,
workspace_name,
}) => {
assert_eq!(name, "main");
assert_eq!(file, PathBuf::from("main.db"));
assert_eq!(workspace_name.as_deref(), Some("primary"));
}
other => panic!(
"expected Action::Create, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn use_subcommand_local_workspace_name_does_not_leak_into_global_workspace_flag() {
let cli = parse(&[
"--workspace",
"GLOBAL-VALUE",
"use",
"main",
"-w",
"LOCAL-VALUE",
]);
assert_eq!(cli.workspace.as_deref(), Some("GLOBAL-VALUE"));
match cli.action {
Some(Action::Use {
name,
workspace_name,
}) => {
assert_eq!(name, "main");
assert_eq!(workspace_name, "LOCAL-VALUE");
}
other => panic!(
"expected Action::Use, got {other:?}",
other = debug_action(&other)
),
}
}
#[test]
fn delete_subcommand_yes_flag() {
let cli = parse(&["delete", "main", "-y"]);
match cli.action {
Some(Action::Delete { name, yes }) => {
assert_eq!(name, "main");
assert!(yes);
}
other => panic!(
"expected Action::Delete, got {other:?}",
other = debug_action(&other)
),
}
}
fn debug_action(action: &Option<Action>) -> &'static str {
match action {
Some(Action::Create { .. }) => "Create",
Some(Action::Delete { .. }) => "Delete",
Some(Action::Use { .. }) => "Use",
Some(Action::Help { .. }) => "Help",
Some(Action::Verb(_)) => "Verb",
None => "None",
}
}
}