millennium 1.0.0-beta.3

Create consistent, light, & secure apps that work on all platforms, using HTML, CSS, and JavaScript
Documentation
// Copyright 2022 pyke.io
//           2019-2021 Tauri Programme within The Commons Conservancy
//                     [https://tauri.studio/]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#![allow(clippy::tabs_in_doc_comments)]

//! Types and functions related to CLI arguments.

use std::collections::HashMap;

use clap::{Arg, ArgMatches, ErrorKind};
use serde::Serialize;
use serde_json::Value;

use crate::{
	utils::config::{CliArg, CliConfig},
	PackageInfo
};

#[macro_use]
mod macros;

mod clapfix {
	//! Compatibility between `clap` 3.0 and 3.1+ without deprecation errors.
	#![allow(deprecated)]

	pub type ClapCommand<'help> = clap::App<'help>;

	pub trait ErrorExt {
		fn kind(&self) -> clap::ErrorKind;
	}

	impl ErrorExt for clap::Error {
		fn kind(&self) -> clap::ErrorKind {
			self.kind
		}
	}
}

use clapfix::{ClapCommand as App, ErrorExt};

/// The resolution of a argument match.
#[derive(Default, Debug, Serialize)]
#[non_exhaustive]
pub struct ArgData {
	/// - [`Value::Bool`] if it's a flag,
	/// - [`Value::Array`] if it's multiple,
	/// - [`Value::String`] if it has value,
	/// - [`Value::Null`] otherwise.
	pub value: Value,
	/// The number of occurrences of the argument.
	/// e.g. `./app --arg 1 --arg 2 --arg 2 3 4` results in three occurrences.
	pub occurrences: u64
}

/// The matched subcommand.
#[derive(Default, Debug, Serialize)]
#[non_exhaustive]
pub struct SubcommandMatches {
	/// The subcommand name.
	pub name: String,
	/// The subcommand argument matches.
	pub matches: Matches
}

/// The argument matches of a command.
#[derive(Default, Debug, Serialize)]
#[non_exhaustive]
pub struct Matches {
	/// Data structure mapping each found arg with its resolution.
	pub args: HashMap<String, ArgData>,
	/// The matched subcommand if found.
	pub subcommand: Option<Box<SubcommandMatches>>
}

impl Matches {
	/// Set a arg match.
	pub(crate) fn set_arg(&mut self, name: String, value: ArgData) {
		self.args.insert(name, value);
	}

	/// Sets the subcommand matches.
	pub(crate) fn set_subcommand(&mut self, name: String, matches: Matches) {
		self.subcommand = Some(Box::new(SubcommandMatches { name, matches }));
	}
}

/// Gets the argument matches of the CLI definition.
///
/// This is a low-level API. If the application has been built, use
/// [`App::get_cli_matches`](`crate::App#method.get_cli_matches`) instead.
///
/// # Examples
///
/// ```rust,no_run
/// use millennium::api::cli::get_matches;
/// millennium::Builder::default().setup(|app| {
/// 	let matches =
/// 		get_matches(app.config().millennium.cli.as_ref().unwrap(), app.package_info())?;
/// 	Ok(())
/// });
/// ```
pub fn get_matches(cli: &CliConfig, package_info: &PackageInfo) -> crate::api::Result<Matches> {
	let about = cli.description().unwrap_or(&package_info.description.to_string()).to_string();
	let version = &*package_info.version.to_string();
	let app = get_app(package_info, version, &package_info.name, Some(&about), cli);
	match app.try_get_matches() {
		Ok(matches) => Ok(get_matches_internal(cli, &matches)),
		Err(e) => match ErrorExt::kind(&e) {
			ErrorKind::DisplayHelp => {
				let mut matches = Matches::default();
				let help_text = e.to_string();
				matches.args.insert(
					"help".to_string(),
					ArgData {
						value: Value::String(help_text),
						occurrences: 0
					}
				);
				Ok(matches)
			}
			ErrorKind::DisplayVersion => {
				let mut matches = Matches::default();
				matches.args.insert("version".to_string(), Default::default());
				Ok(matches)
			}
			_ => Err(e.into())
		}
	}
}

fn get_matches_internal(config: &CliConfig, matches: &ArgMatches) -> Matches {
	let mut cli_matches = Matches::default();
	map_matches(config, matches, &mut cli_matches);

	if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() {
		let mut subcommand_cli_matches = Matches::default();
		map_matches(config.subcommands().unwrap().get(subcommand_name).unwrap(), subcommand_matches, &mut subcommand_cli_matches);
		cli_matches.set_subcommand(subcommand_name.to_string(), subcommand_cli_matches);
	}

	cli_matches
}

fn map_matches(config: &CliConfig, matches: &ArgMatches, cli_matches: &mut Matches) {
	if let Some(args) = config.args() {
		for arg in args {
			let occurrences = matches.occurrences_of(arg.name.clone());
			let value = if occurrences == 0 || !arg.takes_value {
				Value::Bool(occurrences > 0)
			} else if arg.multiple {
				matches
					.values_of(arg.name.clone())
					.map(|v| {
						let mut values = Vec::new();
						for value in v {
							values.push(Value::String(value.to_string()));
						}
						Value::Array(values)
					})
					.unwrap_or(Value::Null)
			} else {
				matches
					.value_of(arg.name.clone())
					.map(|v| Value::String(v.to_string()))
					.unwrap_or(Value::Null)
			};

			cli_matches.set_arg(arg.name.clone(), ArgData { value, occurrences });
		}
	}
}

fn get_app<'a>(package_info: &'a PackageInfo, version: &'a str, command_name: &'a str, about: Option<&'a String>, config: &'a CliConfig) -> App<'a> {
	let mut app = App::new(command_name).author(package_info.authors).version(version);

	if let Some(about) = about {
		app = app.about(&**about);
	}
	if let Some(long_description) = config.long_description() {
		app = app.long_about(&**long_description);
	}
	if let Some(before_help) = config.before_help() {
		app = app.before_help(&**before_help);
	}
	if let Some(after_help) = config.after_help() {
		app = app.after_help(&**after_help);
	}

	if let Some(args) = config.args() {
		for arg in args {
			let arg_name = arg.name.as_ref();
			app = app.arg(get_arg(arg_name, arg));
		}
	}

	if let Some(subcommands) = config.subcommands() {
		for (subcommand_name, subcommand) in subcommands {
			let clap_subcommand = get_app(package_info, version, subcommand_name, subcommand.description(), subcommand);
			app = app.subcommand(clap_subcommand);
		}
	}

	app
}

fn get_arg<'a>(arg_name: &'a str, arg: &'a CliArg) -> Arg<'a> {
	let mut clap_arg = Arg::new(arg_name);

	if arg.index.is_none() {
		clap_arg = clap_arg.long(arg_name);
		if let Some(short) = arg.short {
			clap_arg = clap_arg.short(short);
		}
	}

	clap_arg = bind_string_arg!(arg, clap_arg, description, help);
	clap_arg = bind_string_arg!(arg, clap_arg, long_description, long_help);
	clap_arg = clap_arg.takes_value(arg.takes_value);
	clap_arg = clap_arg.multiple_values(arg.multiple);
	clap_arg = clap_arg.multiple_occurrences(arg.multiple_occurrences);
	clap_arg = bind_value_arg!(arg, clap_arg, number_of_values);
	clap_arg = bind_string_slice_arg!(arg, clap_arg, possible_values);
	clap_arg = bind_value_arg!(arg, clap_arg, min_values);
	clap_arg = bind_value_arg!(arg, clap_arg, max_values);
	clap_arg = clap_arg.required(arg.required);
	clap_arg = bind_string_arg!(arg, clap_arg, required_unless_present, required_unless_present);
	clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_all);
	clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_any);
	clap_arg = bind_string_arg!(arg, clap_arg, conflicts_with, conflicts_with);
	if let Some(value) = &arg.conflicts_with_all {
		let v: Vec<&str> = value.iter().map(|x| &**x).collect();
		clap_arg = clap_arg.conflicts_with_all(&v);
	}
	clap_arg = bind_string_arg!(arg, clap_arg, requires, requires);
	if let Some(value) = &arg.requires_all {
		let v: Vec<&str> = value.iter().map(|x| &**x).collect();
		clap_arg = clap_arg.requires_all(&v);
	}
	clap_arg = bind_if_arg!(arg, clap_arg, requires_if);
	clap_arg = bind_if_arg!(arg, clap_arg, required_if_eq);
	clap_arg = bind_value_arg!(arg, clap_arg, require_equals);
	clap_arg = bind_value_arg!(arg, clap_arg, index);

	clap_arg
}