rapid_cli/
cli.rs

1use crate::{
2	args::flag,
3	commands::{self, RapidCommand},
4};
5use clap::{command, crate_version, ArgMatches, Command};
6use colorful::Colorful;
7use std::{
8	env::{current_dir, current_exe},
9	path::PathBuf,
10	process::exit,
11};
12
13pub type App = Command;
14
15// This should 100% pull from a GCP storage bucket or something that gets updataed in CI when we trigger releases
16// TODO: eventually, we should use this to tell the user that they need to update their CLI version
17// (we could detect this by comparing this value with the crate_version!() macro value)
18pub const RAPID_LATEST_VERSION: &str = "v0.4.3";
19
20/// Returns what the current working directory of the user is
21pub fn current_directory() -> PathBuf {
22	current_dir().expect("Error: Could not determine the current wrking directory")
23}
24
25/// Returns where the installed binary is on the users machine
26pub fn binary_dir() -> PathBuf {
27	current_exe().expect("Error: Could not determine binary dir.")
28}
29
30/// TODO: config fields can be added here later on as needed
31pub struct Config {}
32
33pub struct RapidCLI {
34	// This config can be used for global env vars that can be passed on CLI init
35	pub config: Config,
36}
37
38impl RapidCLI {
39	pub fn new(config: Config) -> Self {
40		Self { config }
41	}
42	pub fn parse() -> App {
43		let usage = "rapid [SUBCAMMAND] [OPTIONS]";
44		command!()
45			.allow_external_subcommands(true)
46			.disable_colored_help(false)
47			.override_usage(usage)
48			.long_version(crate_version!())
49			.help_template(get_help_template())
50			.arg(flag("help", "List command(s)"))
51			.subcommands(RapidCLI::commands())
52	}
53
54	pub fn commands() -> Vec<Command> {
55		vec![
56			commands::new::New::cmd(),
57			commands::init::Init::cmd(),
58			commands::run::Run::cmd(),
59			commands::templates::Templates::cmd(),
60			commands::routes::Routes::cmd(),
61		]
62	}
63
64	pub fn execute_cammand(cmd: &str) -> Option<fn(&Config, &ArgMatches) -> Result<(), crate::cli::CliError<'static>>> {
65		let command_resolver = match cmd {
66			"new" => commands::new::New::execute,
67			"init" => commands::init::Init::execute,
68			"run" => commands::run::Run::execute,
69			"templates" => commands::templates::Templates::execute,
70			"routes" => commands::routes::Routes::execute,
71			_ => return None,
72		};
73
74		Some(command_resolver)
75	}
76
77	pub fn run(&self, args: ArgMatches) -> Result<(), CliError<'static>> {
78		if let Some((cmd, args)) = args.subcommand() {
79			// Since we did find a sub-command match, lets exeute the command
80			if let Some(cm) = RapidCLI::execute_cammand(cmd) {
81				let _ = cm(&self.config, args);
82			} else {
83				// Show the help command if the user inputted a invalid command
84				println!("{}", get_help_template());
85				exit(64); // exit 64 is a standard usage error with CLIs
86			}
87		} else {
88			// Show the help template if there was no command match found
89			println!("{}", get_help_template());
90			exit(64);
91		}
92
93		// This outputs only when a command succeeds (would be cool to capture analytics here at some point)
94		Ok(())
95	}
96}
97
98// TODO: update this to actually be a legit health template
99// Note: Do not change indentation of this or else it will break
100fn get_help_template() -> String {
101	format!(
102		"RAPID -- Build type-safe applications with Rust and Typescript
103
104Commands:
105  {init}	Initialize Rapid functionality in an existing app
106  {run}	Run Rapid applications with a single command
107  {new} 	Create a new rapid app
108
109Options:
110  -V --version	  Print version info and exit
111
112",
113		init = "init".bold(),
114		run = "run".bold(),
115		new = "new".bold()
116	)
117}
118
119#[derive(Debug)]
120pub struct CliError<'a> {
121	pub error: Option<&'a str>,
122	pub exit_code: i32,
123}
124
125impl<'a> CliError<'a> {
126	pub fn new(error: &'a str, code: i32) -> CliError<'a> {
127		CliError {
128			error: Some(error),
129			exit_code: code,
130		}
131	}
132}