//! Integration with `clap` //! //! CLI argument structs are defined in this module. Execution logic for each //! command goes in a submodule. use std::{path::PathBuf, str::FromStr}; use clap::{Parser, Subcommand}; use crate::error; mod migrate_db; mod serve; /// Command line arguments #[derive(Parser)] #[clap( about, version = crate::version(), )] pub(crate) struct Args { #[clap(subcommand)] pub(crate) command: Command, } #[derive(Subcommand)] pub(crate) enum Command { /// Run the server. Serve(ServeArgs), /// Commands for interacting with the database. #[clap(subcommand)] Db(DbCommand), } /// Wrapper for the `--config` arg. /// /// This exists to centralize the `mut_arg` code that sets the help value based /// on runtime information. #[derive(clap::Args)] #[clap(mut_arg("config", |x| { let help = "Set the path to the configuration file"; x.help(help).long_help(format!( "{}\n\nIf this option is specified, the provided value is used \ as-is.\n\nIf this option is not specified, then the XDG Base \ Directory Specification is followed, searching for the path `{}` \ in the configuration directories. ", help, crate::config::DEFAULT_PATH.display(), )) }))] pub(crate) struct ConfigArg { /// Path to the configuration file #[clap(long, short)] pub(crate) config: Option, } #[derive(clap::Args)] pub(crate) struct ServeArgs { #[clap(flatten)] pub(crate) config: ConfigArg, } #[derive(Subcommand)] pub(crate) enum DbCommand { /// Migrate database from one server implementation to another. /// /// This command is not protected against symlink-swapping attacks. Do not /// use it when any subdirectories or parents of the `--in`, `--out`, or /// `--inplace` directories may be written by an untrusted user during /// execution. Migrate(MigrateDbArgs), } #[derive(clap::Args)] pub(crate) struct MigrateDbArgs { #[clap(flatten)] config: ConfigArg, /// Target server implementation to migrate database to. /// /// If migrating to the current version of grapevine, specify the version /// as 'grapevine'. /// /// If migrating to a released version of conduit, specified the version /// of conduit as `conduit-{version}` (example: `conduit-0.8.0`). If /// migrating to an unreleased conduit build, instead specify the raw /// database version as `conduit-db-{version}` (example: `conduit-db-13`). /// The raw database version can be found by looking at the /// `latest_database_version` variable in `src/database/mod.rs`. /// /// The server implementation used for the current database will be /// detected automatically, and does not need to be specified. #[clap(long)] pub(crate) to: DbMigrationTarget, /// Path to read database from. #[clap(long = "in", short)] pub(crate) in_path: PathBuf, /// Path to write migrated database to. #[clap(long = "out", short)] pub(crate) out_path: PathBuf, } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum DbMigrationTarget { /// The latest grapevine db version /// /// Example: /// /// ``` /// assert_eq!("grapevine".parse(), Ok(DbMigrationTarget::Grapevine)) /// ``` Grapevine, /// A conduit-compatible db version. /// /// This may either be specified as a released version number or directly /// as a database version. The raw database version must be used when /// migrating to a conduit deployment built from an unreleased commit /// on the `next` branch. Conduit(ConduitDbVersion), } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum ConduitDbVersion { /// A conduit release version number /// /// Example: /// /// ``` /// assert_eq!( /// "conduit-0.8.0".parse(), /// Ok(DbMigrationTarget::Conduit(ConduitDbVersion::Release("0.8.0"))) /// ); /// ``` Release(String), /// A raw database version /// /// This corresponds directly to a /// [`crate::service::globals::DbVersion::Conduit`] version. /// /// Example: /// /// ``` /// assert_eq!( /// "conduit-db-13".parse(), /// Ok(DbMigrationTarget::Conduit(ConduitDbVersion::Db(13))) /// ); /// ``` Db(u64), } #[derive(thiserror::Error, Debug)] #[error("invalid db migration target version")] pub(crate) struct DbMigrationTargetParseError; impl FromStr for DbMigrationTarget { type Err = DbMigrationTargetParseError; fn from_str(s: &str) -> Result { if s == "grapevine" { Ok(DbMigrationTarget::Grapevine) } else if let Some(version) = s.strip_prefix("conduit-db-") { let version = version.parse().map_err(|_| DbMigrationTargetParseError)?; Ok(DbMigrationTarget::Conduit(ConduitDbVersion::Db(version))) } else if let Some(version) = s.strip_prefix("conduit-") { Ok(DbMigrationTarget::Conduit(ConduitDbVersion::Release( version.to_owned(), ))) } else { Err(DbMigrationTargetParseError) } } } impl Args { pub(crate) async fn run(self) -> Result<(), error::Main> { match self.command { Command::Serve(args) => serve::run(args).await?, Command::Db(DbCommand::Migrate(args)) => { migrate_db::run(args).await?; } } Ok(()) } }