Skip to main content

dealer/
cli.rs

1use clap::{Args, CommandFactory, Parser, Subcommand};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub(crate) enum CommandKind {
5    Version,
6    Check {
7        project: String,
8    },
9    Fmt {
10        project: String,
11        check: bool,
12    },
13    Install {
14        package: Option<String>,
15        project: String,
16    },
17    Build {
18        project: String,
19        mode: BuildMode,
20    },
21    Run {
22        project: String,
23        mode: BuildMode,
24    },
25    Clean {
26        project: String,
27    },
28    Init {
29        kind: InitKind,
30        path: Option<String>,
31    },
32    Xtazy {
33        subcommand: XtazySubcommand,
34    },
35    SelfUpdate,
36    Doctor,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) enum BuildMode {
41    Dev,
42    Prod,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub(crate) enum InitKind {
47    App,
48    Package,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub(crate) enum XtazySubcommand {
53    Install { version: Option<String> },
54    Update,
55    AutoUpdate { action: Option<AutoUpdateAction> },
56    UseVersion { version: String },
57    Active,
58    List,
59    Remove { version: String },
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub(crate) enum AutoUpdateAction {
64    Off,
65    Status,
66}
67
68#[derive(Parser, Debug)]
69#[command(
70    name = "dealer",
71    about = "Xtazy project orchestrator",
72    disable_version_flag = true
73)]
74struct DealerCli {
75    #[command(subcommand)]
76    command: DealerCommand,
77}
78
79#[derive(Subcommand, Debug)]
80enum DealerCommand {
81    Check(ProjectArg),
82    Fmt(FmtArgs),
83    Install(InstallArgs),
84    Build(BuildArgs),
85    Run(BuildArgs),
86    Clean(ProjectArg),
87    Init(InitArgs),
88    Xtazy(XtazyArgs),
89    #[command(name = "self")]
90    SelfCommand(SelfArgs),
91    Doctor,
92}
93
94#[derive(Args, Debug)]
95struct ProjectArg {
96    #[arg(default_value = ".")]
97    project: String,
98}
99
100#[derive(Args, Debug)]
101struct FmtArgs {
102    #[arg(long)]
103    check: bool,
104    #[arg(default_value = ".")]
105    project: String,
106}
107
108#[derive(Args, Debug)]
109struct InstallArgs {
110    package: Option<String>,
111    project: Option<String>,
112}
113
114#[derive(Args, Debug)]
115struct BuildArgs {
116    #[arg(long, conflicts_with = "prod")]
117    dev: bool,
118    #[arg(long, conflicts_with = "dev")]
119    prod: bool,
120    #[arg(default_value = ".")]
121    project: String,
122}
123
124#[derive(Args, Debug)]
125struct InitArgs {
126    #[command(subcommand)]
127    kind: InitCommand,
128}
129
130#[derive(Subcommand, Debug)]
131enum InitCommand {
132    App(InitPath),
133    Package(InitPath),
134}
135
136#[derive(Args, Debug)]
137struct InitPath {
138    path: Option<String>,
139}
140
141#[derive(Args, Debug)]
142struct XtazyArgs {
143    #[command(subcommand)]
144    command: XtazyCommand,
145}
146
147#[derive(Subcommand, Debug)]
148enum XtazyCommand {
149    Install(VersionArg),
150    Update,
151    AutoUpdate(AutoUpdateArgs),
152    Use(RequiredVersionArg),
153    Active,
154    List,
155    Remove(RequiredVersionArg),
156}
157
158#[derive(Args, Debug)]
159struct VersionArg {
160    version: Option<String>,
161}
162
163#[derive(Args, Debug)]
164struct RequiredVersionArg {
165    version: String,
166}
167
168#[derive(Args, Debug)]
169struct AutoUpdateArgs {
170    #[command(subcommand)]
171    action: Option<AutoUpdateCommand>,
172}
173
174#[derive(Subcommand, Debug)]
175enum AutoUpdateCommand {
176    Off,
177    Status,
178}
179
180#[derive(Args, Debug)]
181struct SelfArgs {
182    #[command(subcommand)]
183    command: SelfCommand,
184}
185
186#[derive(Subcommand, Debug)]
187enum SelfCommand {
188    Update,
189}
190
191pub(crate) fn parse_args(args: &[String]) -> Result<CommandKind, String> {
192    if args.len() == 1 {
193        return Err(DealerCli::command().render_usage().to_string());
194    }
195
196    if args.get(1).map(String::as_str) == Some("--version")
197        || args.get(1).map(String::as_str) == Some("-V")
198    {
199        return Ok(CommandKind::Version);
200    }
201
202    // Preserve the historical dev shortcut: `dealer <project>` means check that project.
203    if args.len() == 2 && !args[1].starts_with('-') && !is_known_command(&args[1]) {
204        return Ok(CommandKind::Check {
205            project: args[1].clone(),
206        });
207    }
208
209    let cli = DealerCli::try_parse_from(args).map_err(|error| error.to_string())?;
210    Ok(command_from_cli(cli.command))
211}
212
213fn is_known_command(value: &str) -> bool {
214    matches!(
215        value,
216        "check"
217            | "fmt"
218            | "install"
219            | "build"
220            | "run"
221            | "clean"
222            | "init"
223            | "xtazy"
224            | "self"
225            | "doctor"
226    )
227}
228
229fn command_from_cli(command: DealerCommand) -> CommandKind {
230    match command {
231        DealerCommand::Check(args) => CommandKind::Check {
232            project: args.project,
233        },
234        DealerCommand::Fmt(args) => CommandKind::Fmt {
235            project: args.project,
236            check: args.check,
237        },
238        DealerCommand::Install(args) => install_command(args),
239        DealerCommand::Build(args) => CommandKind::Build {
240            project: args.project,
241            mode: mode_from_args(args.dev, args.prod),
242        },
243        DealerCommand::Run(args) => CommandKind::Run {
244            project: args.project,
245            mode: mode_from_args(args.dev, args.prod),
246        },
247        DealerCommand::Clean(args) => CommandKind::Clean {
248            project: args.project,
249        },
250        DealerCommand::Init(args) => init_command(args.kind),
251        DealerCommand::Xtazy(args) => CommandKind::Xtazy {
252            subcommand: xtazy_command(args.command),
253        },
254        DealerCommand::SelfCommand(args) => match args.command {
255            SelfCommand::Update => CommandKind::SelfUpdate,
256        },
257        DealerCommand::Doctor => CommandKind::Doctor,
258    }
259}
260
261fn install_command(args: InstallArgs) -> CommandKind {
262    match (args.package, args.project) {
263        (None, None) => CommandKind::Install {
264            package: None,
265            project: ".".to_string(),
266        },
267        (Some(package), None) if looks_like_project_path(&package) => CommandKind::Install {
268            package: None,
269            project: package,
270        },
271        (Some(package), None) => CommandKind::Install {
272            package: Some(package),
273            project: ".".to_string(),
274        },
275        (package, Some(project)) => CommandKind::Install { package, project },
276    }
277}
278
279fn looks_like_project_path(value: &str) -> bool {
280    value == "." || value.contains('/') || std::path::Path::new(value).is_dir()
281}
282
283fn mode_from_args(_dev: bool, prod: bool) -> BuildMode {
284    if prod {
285        BuildMode::Prod
286    } else {
287        BuildMode::Dev
288    }
289}
290
291fn init_command(command: InitCommand) -> CommandKind {
292    match command {
293        InitCommand::App(path) => CommandKind::Init {
294            kind: InitKind::App,
295            path: path.path,
296        },
297        InitCommand::Package(path) => CommandKind::Init {
298            kind: InitKind::Package,
299            path: path.path,
300        },
301    }
302}
303
304fn xtazy_command(command: XtazyCommand) -> XtazySubcommand {
305    match command {
306        XtazyCommand::Install(args) => XtazySubcommand::Install {
307            version: args.version,
308        },
309        XtazyCommand::Update => XtazySubcommand::Update,
310        XtazyCommand::AutoUpdate(args) => XtazySubcommand::AutoUpdate {
311            action: args.action.map(|action| match action {
312                AutoUpdateCommand::Off => AutoUpdateAction::Off,
313                AutoUpdateCommand::Status => AutoUpdateAction::Status,
314            }),
315        },
316        XtazyCommand::Use(args) => XtazySubcommand::UseVersion {
317            version: args.version,
318        },
319        XtazyCommand::Active => XtazySubcommand::Active,
320        XtazyCommand::List => XtazySubcommand::List,
321        XtazyCommand::Remove(args) => XtazySubcommand::Remove {
322            version: args.version,
323        },
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    fn args(values: &[&str]) -> Vec<String> {
332        values.iter().map(|value| value.to_string()).collect()
333    }
334
335    #[test]
336    fn parse_version_flag() {
337        let cmd = parse_args(&args(&["dealer", "--version"])).expect("version should parse");
338
339        assert_eq!(cmd, CommandKind::Version);
340    }
341
342    #[test]
343    fn parse_defaults_to_check_for_single_project_argument() {
344        let cmd =
345            parse_args(&args(&["dealer", "project"])).expect("single project arg should parse");
346
347        assert_eq!(
348            cmd,
349            CommandKind::Check {
350                project: "project".to_string()
351            }
352        );
353    }
354
355    #[test]
356    fn parse_check_defaults_to_current_directory() {
357        let cmd = parse_args(&args(&["dealer", "check"])).expect("check should parse");
358
359        assert_eq!(
360            cmd,
361            CommandKind::Check {
362                project: ".".to_string()
363            }
364        );
365    }
366
367    #[test]
368    fn parse_build_prod_command() {
369        let cmd = parse_args(&args(&["dealer", "build", "--prod", "project"]))
370            .expect("build command should parse");
371
372        assert_eq!(
373            cmd,
374            CommandKind::Build {
375                project: "project".to_string(),
376                mode: BuildMode::Prod
377            }
378        );
379    }
380
381    #[test]
382    fn parse_install_package_and_project() {
383        let cmd = parse_args(&args(&["dealer", "install", "foo", "project"]))
384            .expect("install command should parse");
385
386        assert_eq!(
387            cmd,
388            CommandKind::Install {
389                package: Some("foo".to_string()),
390                project: "project".to_string()
391            }
392        );
393    }
394
395    #[test]
396    fn parse_xtazy_auto_update_status() {
397        let cmd = parse_args(&args(&["dealer", "xtazy", "auto-update", "status"]))
398            .expect("auto-update status should parse");
399
400        assert_eq!(
401            cmd,
402            CommandKind::Xtazy {
403                subcommand: XtazySubcommand::AutoUpdate {
404                    action: Some(AutoUpdateAction::Status)
405                }
406            }
407        );
408    }
409
410    #[test]
411    fn parse_self_update() {
412        let cmd =
413            parse_args(&args(&["dealer", "self", "update"])).expect("self update should parse");
414
415        assert_eq!(cmd, CommandKind::SelfUpdate);
416    }
417}