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::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<()> {
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(())
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(())
81                }
82                cli::HistoryCommands::SoftApply {
83                    from_version,
84                    to_version,
85                    connect_opts,
86                } => {
87                    let db_url = connect_opts.required_db_url()?.to_string();
88                    let context = self.opts.connect(&db_url).await?;
89                    let mut runner = Runner::new(context);
90                    let report = runner.soft_apply(*from_version, *to_version).await?;
91                    log::info!("{report:#?}");
92
93                    Ok(())
94                }
95            },
96            cli::TernCommands::Migrate(migrate) => match &migrate.commands {
97                cli::MigrateCommands::ApplyAll {
98                    dryrun,
99                    connect_opts,
100                } => {
101                    let db_url = connect_opts.required_db_url()?.to_string();
102                    let context = self.opts.connect(&db_url).await?;
103                    let mut runner = Runner::new(context);
104                    let report = if *dryrun {
105                        runner.dryrun().await?
106                    } else {
107                        runner.apply_all().await?
108                    };
109                    log::info!("{report:#?}");
110
111                    Ok(())
112                }
113                cli::MigrateCommands::ListApplied { connect_opts } => {
114                    let db_url = connect_opts.required_db_url()?.to_string();
115                    let context = self.opts.connect(&db_url).await?;
116                    let mut runner = Runner::new(context);
117                    let report = runner.list_applied().await?;
118                    log::info!("{report:#?}");
119
120                    Ok(())
121                }
122                cli::MigrateCommands::New {
123                    description,
124                    no_tx,
125                    migration_type,
126                    source,
127                } => commands::new(
128                    description.to_string(),
129                    *no_tx,
130                    *migration_type,
131                    source.path.clone(),
132                ),
133            },
134        }
135    }
136}