tern_cli/
lib.rs

1//! The CLI for the [`tern`][tern-docs] migration library.
2//!
3//! This exports the [`App`] type and [`ContextOptions`], which help turn a
4//! project using `tern` into a CLI.
5//!
6//! The `App` is the CLI. `ContextOptions` exists to connect a generic context
7//! to the CLI since it is the CLI that supplies the database URL, surely
8//! required of the context, but not anything else the context might need to
9//! initialize.
10//!
11//! [tern-docs]: https://docs.rs/crate/tern/latest
12use clap::Parser;
13use tern_core::error::TernResult;
14use tern_core::future::Future;
15use tern_core::migration::MigrationContext;
16use tern_core::runner::{Report, Runner};
17
18mod cli;
19mod commands;
20
21/// A type that can build a particular context given a database url.
22/// This is needed because the context is arbitrary, yet the CLI options have
23/// the database URL, which is certainly required to build it.
24pub trait ContextOptions {
25    type Ctx: MigrationContext;
26
27    /// Establish a connection with this context.
28    fn connect(&self, db_url: &str) -> impl Future<Output = TernResult<Self::Ctx>>;
29}
30
31/// The CLI app to run.  This wraps the functionality of the context that `Opts`
32/// creates.
33///
34/// ## Usage
35///
36/// To connect to the given database, the CLI needs a database url, which can be
37/// provided via the environment variable `DATABASE_URL` or using the option
38/// `-D/--database-url` available to a command/subcommand.
39///
40/// ```terminal
41/// > $ my-app --help
42/// Usage: my-app <COMMAND>
43///
44/// Commands:
45///   migrate  Operations on the set of migration files
46///   history  Operations on the table storing the history of these migrations
47///   help     Print this message or the help of the given subcommand(s)
48/// ```
49pub struct App<Opts> {
50    opts: Opts,
51    cli: cli::Tern,
52}
53
54impl<Opts> App<Opts>
55where
56    Opts: ContextOptions,
57{
58    pub fn new(opts: Opts) -> Self {
59        let cli = cli::Tern::parse();
60        Self { opts, cli }
61    }
62
63    pub async fn run(&self) -> anyhow::Result<Option<Report>> {
64        match &self.cli.commands {
65            cli::TernCommands::History(history) => match &history.commands {
66                cli::HistoryCommands::Init { connect_opts } => {
67                    let db_url = connect_opts.required_db_url()?.to_string();
68                    let context = self.opts.connect(&db_url).await?;
69                    let mut runner = Runner::new(context);
70                    runner.init_history().await?;
71
72                    Ok(None)
73                }
74                cli::HistoryCommands::Drop { connect_opts } => {
75                    let db_url = connect_opts.required_db_url()?.to_string();
76                    let context = self.opts.connect(&db_url).await?;
77                    let mut runner = Runner::new(context);
78                    runner.drop_history().await?;
79
80                    Ok(None)
81                }
82                cli::HistoryCommands::SoftApply { .. } => Err(anyhow::anyhow!(
83                    "Deprecated: use `migrate soft-apply` instead"
84                )),
85            },
86            cli::TernCommands::Migrate(migrate) => match &migrate.commands {
87                cli::MigrateCommands::Apply {
88                    dryrun,
89                    target_version,
90                    connect_opts,
91                } => {
92                    let db_url = connect_opts.required_db_url()?.to_string();
93                    let context = self.opts.connect(&db_url).await?;
94                    let mut runner = Runner::new(context);
95                    let report = runner.run_apply(*target_version, *dryrun).await?;
96
97                    Ok(Some(report))
98                }
99                cli::MigrateCommands::ApplyAll {
100                    dryrun,
101                    connect_opts,
102                } => {
103                    let db_url = connect_opts.required_db_url()?.to_string();
104                    let context = self.opts.connect(&db_url).await?;
105                    let mut runner = Runner::new(context);
106                    let report = runner.run_apply_all(*dryrun).await?;
107
108                    Ok(Some(report))
109                }
110                cli::MigrateCommands::SoftApply {
111                    dryrun,
112                    target_version,
113                    connect_opts,
114                } => {
115                    let db_url = connect_opts.required_db_url()?.to_string();
116                    let context = self.opts.connect(&db_url).await?;
117                    let mut runner = Runner::new(context);
118                    let report = runner.run_soft_apply(*target_version, *dryrun).await?;
119
120                    Ok(Some(report))
121                }
122                cli::MigrateCommands::ListApplied { connect_opts } => {
123                    let db_url = connect_opts.required_db_url()?.to_string();
124                    let context = self.opts.connect(&db_url).await?;
125                    let mut runner = Runner::new(context);
126                    let report = runner.list_applied().await?;
127
128                    Ok(Some(report))
129                }
130                cli::MigrateCommands::New {
131                    description,
132                    no_tx,
133                    migration_type,
134                    source,
135                } => {
136                    commands::new(
137                        description.to_string(),
138                        *no_tx,
139                        *migration_type,
140                        source.path.clone(),
141                    )?;
142
143                    Ok(None)
144                }
145            },
146        }
147    }
148}