examples_example_cli/
main.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use clap::{Parser, Subcommand};
6
7use example_component::{ApiResult, ExampleComponent, TodoItem};
8
9// Writing a CLI means using the `clap` library to define your CLI commands.
10// The [clap documentation](https://docs.rs/clap/latest/clap/) is decent for this.
11// Also, the other app-services CLI code can provide good examples
12
13// Top-level clap CLI.  Each field corresponds to a CLI argument
14#[derive(Debug, Parser)]
15#[command(about, long_about = None)]
16struct Cli {
17    /// Enable verbose logging
18    // Notes:
19    //   * Docstrings show up in the CLI help
20    //   * `short` means that this is associated with `-v`
21    //   * `long` means that this is associated with `--verbose`
22    //   * `action` means that this flag will set the boolean to `true`.
23    #[arg(short, long, action)]
24    verbose: bool,
25
26    // Subcommands can be used to create git-like CLIs where the argument begins a new subcommand
27    // and each subcommand can have different args
28    #[command(subcommand)]
29    command: Commands,
30}
31
32#[derive(Debug, Subcommand)]
33enum Commands {
34    /// Manage todo lists
35    Lists {
36        // Yes, sub-sub-commands are possible
37        #[command(subcommand)]
38        lists_command: Option<ListsCommands>,
39    },
40    /// Manage todo items
41    Items {
42        /// List to use
43        list: String,
44        #[command(subcommand)]
45        items_command: Option<ItemsCommands>,
46    },
47}
48
49#[derive(Debug, Subcommand)]
50enum ListsCommands {
51    /// List lists
52    List,
53    /// Create a new list
54    Create {
55        // Name is a position argument, since it doesn't have the `#[arg]` attribute.
56        name: String,
57    },
58    /// delete a list
59    Delete { name: String },
60}
61
62#[derive(Debug, Subcommand)]
63enum ItemsCommands {
64    /// List todos
65    List,
66    /// Create a new todo
67    Add {
68        // Name for the item
69        name: String,
70        #[arg(short, long)]
71        github_issue: Option<String>,
72    },
73    /// Update a todo
74    Update {
75        // Name of the item to update
76        name: String,
77        #[arg(short, long)]
78        description: Option<String>,
79        #[arg(short, long)]
80        url: Option<String>,
81        #[arg(short, long, action)]
82        toggle: bool,
83    },
84    /// Delete a todo
85    Delete {
86        // Name of the item to update
87        name: String,
88    },
89}
90
91fn main() -> ApiResult<()> {
92    let cli = Cli::parse();
93    init_logging(&cli);
94    // Applications must initialize viaduct for the HTTP client to work.
95    // This example uses the `reqwest` backend because it's easy to setup.
96    viaduct_reqwest::use_reqwest_backend();
97    let component = build_example_component()?;
98    println!();
99    match cli.command {
100        Commands::Lists {
101            lists_command: command,
102        } => {
103            let command = command.unwrap_or(ListsCommands::List);
104            handle_lists(component, command)
105        }
106        Commands::Items {
107            list,
108            items_command: command,
109        } => {
110            let command = command.unwrap_or(ItemsCommands::List);
111            handle_todos(component, list, command)
112        }
113    }
114}
115
116fn init_logging(cli: &Cli) {
117    // The env_logger crate is a simple way to setup logging.
118    //
119    // This will enable trace-level logging if `-v` is present and `info-level` otherwise.
120    let log_filter = if cli.verbose {
121        "example_component=trace"
122    } else {
123        "example_component=info"
124    };
125    env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", log_filter));
126}
127
128fn build_example_component() -> ApiResult<ExampleComponent> {
129    // Use `cli_support` to get paths to store stuff in.  These will always be relative to
130    // `[WORKSPACE_ROOT]/.cli-data`
131    let db_path = cli_support::cli_data_path("example-component.db");
132    ExampleComponent::new(&db_path)
133}
134
135fn handle_lists(component: ExampleComponent, subcommand: ListsCommands) -> ApiResult<()> {
136    match subcommand {
137        ListsCommands::List => {
138            let lists = component.get_lists()?;
139            if lists.is_empty() {
140                println!("No lists created");
141            } else {
142                for list in lists {
143                    println!("{}", list);
144                }
145            }
146        }
147        ListsCommands::Create { name } => {
148            component.create_list(&name)?;
149            println!("Created list: {name}");
150        }
151        ListsCommands::Delete { name } => {
152            component.delete_list(&name)?;
153            println!("Deleted list: {name}");
154        }
155    }
156    Ok(())
157}
158
159fn handle_todos(
160    component: ExampleComponent,
161    list: String,
162    subcommand: ItemsCommands,
163) -> ApiResult<()> {
164    match subcommand {
165        ItemsCommands::List => {
166            let items = component.get_list_items(&list)?;
167            if items.is_empty() {
168                println!("No items created");
169            } else {
170                println!("{:-^79}", format!(" {list} "));
171                println!(
172                    "{:<9} {:<29} {:<29} {:>9}",
173                    "name", "description", "url", "completed"
174                );
175                for saved in items {
176                    println!(
177                        "{:<9} {:<29} {:<29} {:>9}",
178                        clamp_string(&saved.item.name, 9),
179                        clamp_string(&saved.item.description, 29),
180                        clamp_string(&saved.item.url, 29),
181                        if saved.item.completed { "X" } else { "" },
182                    )
183                }
184            }
185        }
186        ItemsCommands::Add { name, github_issue } => {
187            match github_issue {
188                None => {
189                    component.add_item(
190                        &list,
191                        TodoItem {
192                            name: name.clone(),
193                            ..TodoItem::default()
194                        },
195                    )?;
196                    println!("Created item: {name}");
197                }
198                Some(github_issue) => {
199                    component.add_item_from_gh_issue(&list, &name, &github_issue)?;
200                    println!("Created item: {name} (from GH-{github_issue})");
201                }
202            };
203        }
204        ItemsCommands::Update {
205            name,
206            description,
207            url,
208            toggle,
209        } => {
210            let mut saved = component.get_list_item(&list, &name)?;
211            if let Some(description) = description {
212                saved.item.description = description;
213            }
214            if let Some(url) = url {
215                saved.item.url = url;
216            }
217            if toggle {
218                saved.item.completed = !saved.item.completed;
219            }
220            component.update_item(&saved)?;
221            println!("Updated item: {name}");
222        }
223        ItemsCommands::Delete { name } => {
224            let saved = component.get_list_item(&list, &name)?;
225            component.delete_item(saved)?;
226            println!("Deleted item: {name}");
227        }
228    }
229    Ok(())
230}
231
232fn clamp_string(val: &str, max_width: usize) -> String {
233    if val.len() > max_width {
234        format!("{}...", &val[0..max_width - 3])
235    } else {
236        val.to_string()
237    }
238}