slumber_cli/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(clippy::all)]
3
4//! Command line interface for Slumber.
5//!
6//! **This crate is not semver compliant**. The version is locked to the root
7//! `slumber` crate version. If you choose to depend directly on this crate, you
8//! do so at your own risk of breakage.
9
10mod commands;
11mod completions;
12
13use crate::commands::{
14    collections::CollectionsCommand, db::DbCommand, generate::GenerateCommand,
15    history::HistoryCommand, import::ImportCommand, new::NewCommand,
16    request::RequestCommand, show::ShowCommand,
17};
18use clap::{CommandFactory, Parser};
19use clap_complete::CompleteEnv;
20use slumber_core::collection::CollectionFile;
21use std::{path::PathBuf, process::ExitCode};
22
23const COMMAND_NAME: &str = "slumber";
24
25/// Configurable HTTP client with both TUI and CLI interfaces
26///
27/// If subcommand is omitted, start the TUI.
28///
29/// <https://slumber.lucaspickering.me/book/>
30#[derive(Debug, Parser)]
31#[clap(author, version, about, name = COMMAND_NAME)]
32pub struct Args {
33    #[command(flatten)]
34    pub global: GlobalArgs,
35    #[command(subcommand)]
36    pub subcommand: Option<CliCommand>,
37}
38
39impl Args {
40    /// Check if we're in shell completion mode, which is set via the `COMPLETE`
41    /// env var. If so, this will print completions then exit the process
42    pub fn complete() {
43        CompleteEnv::with_factory(Args::command).complete();
44    }
45
46    /// Alias for [clap::Parser::parse]
47    pub fn parse() -> Self {
48        <Self as Parser>::parse()
49    }
50}
51
52/// Arguments that are available to all subcommands and the TUI
53#[derive(Debug, Parser)]
54pub struct GlobalArgs {
55    /// Collection file, which defines profiles, recipes, etc. If omitted,
56    /// check the current and all parent directories for the following files
57    /// (in this order): slumber.yml, slumber.yaml, .slumber.yml,
58    /// .slumber.yaml. If a directory is passed, apply the same search
59    /// logic from the given directory rather than the current.
60    #[clap(long, short)]
61    pub file: Option<PathBuf>,
62}
63
64impl GlobalArgs {
65    /// Get the path to the active collection file. Return an error if there is
66    /// no collection file present, or if the user specified an invalid file.
67    fn collection_path(&self) -> anyhow::Result<PathBuf> {
68        CollectionFile::try_path(None, self.file.clone())
69    }
70}
71
72/// A CLI subcommand
73#[derive(Clone, Debug, clap::Subcommand)]
74pub enum CliCommand {
75    Collections(CollectionsCommand),
76    Db(DbCommand),
77    Generate(GenerateCommand),
78    History(HistoryCommand),
79    Import(ImportCommand),
80    New(NewCommand),
81    Request(RequestCommand),
82    Show(ShowCommand),
83}
84
85impl CliCommand {
86    /// Execute this CLI subcommand
87    pub async fn execute(self, global: GlobalArgs) -> anyhow::Result<ExitCode> {
88        match self {
89            Self::Collections(command) => command.execute(global).await,
90            Self::Db(command) => command.execute(global).await,
91            Self::Generate(command) => command.execute(global).await,
92            Self::History(command) => command.execute(global).await,
93            Self::Import(command) => command.execute(global).await,
94            Self::New(command) => command.execute(global).await,
95            Self::Request(command) => command.execute(global).await,
96            Self::Show(command) => command.execute(global).await,
97        }
98    }
99}
100
101/// An executable subcommand. This trait isn't strictly necessary because we do
102/// static dispatch via the command enum, but it's helpful to enforce a
103/// consistent interface for each subcommand.
104trait Subcommand {
105    /// Execute the subcommand
106    async fn execute(self, global: GlobalArgs) -> anyhow::Result<ExitCode>;
107}