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` helps to connect a generic context to
7//! the CLI if it is the CLI that is supplying the database connection string.
8//!
9//! [tern-docs]: https://docs.rs/crate/tern/latest
10use clap::Parser;
11use tern_core::error::TernResult;
12use tern_core::future::Future;
13use tern_core::migration::MigrationContext;
14use tern_core::runner::{Report, Runner};
15
16mod cli;
17mod commands;
18
19/// A type that can build a particular context given a database url.
20pub trait ContextOptions {
21    type Ctx: MigrationContext;
22
23    /// Establish a connection with this context.
24    fn connect(&self, db_url: &str) -> impl Future<Output = TernResult<Self::Ctx>>;
25}
26
27/// The CLI app to run.
28///
29/// ## Usage
30///
31/// Either build from [`ContextOptions`] and supply the database connection
32/// string with the CLI and `-D`, `--database-url`, or environment variable
33/// `DATABASE_URL`, or build `App` directly from a `MigrationContext`.
34///
35/// ```terminal
36/// > $ my-app --help
37/// Usage: my-app <COMMAND>
38///
39/// Commands:
40///   migrate  Operations on the set of migration files
41///   history  Operations on the table storing the history of these migrations
42///   help     Print this message or the help of the given subcommand(s)
43/// ```
44pub struct App<T> {
45    inner: T,
46    cli: cli::Tern,
47}
48
49impl<T> App<T> {
50    pub fn new(inner: T) -> Self {
51        let cli = cli::Tern::parse();
52        Self { inner, cli }
53    }
54
55    /// Run a CLI that has a `T: ContextOptions`, using the context that these
56    /// options can build.
57    pub async fn run(&self) -> anyhow::Result<Option<Report>>
58    where
59        T: ContextOptions,
60    {
61        match &self.cli.commands {
62            cli::TernCommands::History(history) => match &history.commands {
63                cli::HistoryCommands::Init { connect_opts } => {
64                    let db_url = connect_opts.required_db_url()?.to_string();
65                    let context = self.inner.connect(&db_url).await?;
66                    let mut runner = Runner::new(context);
67                    runner.init_history().await?;
68
69                    Ok(None)
70                }
71                cli::HistoryCommands::Drop { connect_opts } => {
72                    let db_url = connect_opts.required_db_url()?.to_string();
73                    let context = self.inner.connect(&db_url).await?;
74                    let mut runner = Runner::new(context);
75                    runner.drop_history().await?;
76
77                    Ok(None)
78                }
79                cli::HistoryCommands::SoftApply { .. } => Err(anyhow::anyhow!(
80                    "Deprecated: use `migrate soft-apply` instead"
81                )),
82            },
83            cli::TernCommands::Migrate(migrate) => match &migrate.commands {
84                cli::MigrateCommands::Apply {
85                    dryrun,
86                    target_version,
87                    connect_opts,
88                } => {
89                    let db_url = connect_opts.required_db_url()?.to_string();
90                    let context = self.inner.connect(&db_url).await?;
91                    let mut runner = Runner::new(context);
92                    let report = runner.run_apply(*target_version, *dryrun).await?;
93
94                    Ok(Some(report))
95                }
96                cli::MigrateCommands::ApplyAll {
97                    dryrun,
98                    connect_opts,
99                } => {
100                    let db_url = connect_opts.required_db_url()?.to_string();
101                    let context = self.inner.connect(&db_url).await?;
102                    let mut runner = Runner::new(context);
103                    let report = runner.run_apply_all(*dryrun).await?;
104
105                    Ok(Some(report))
106                }
107                cli::MigrateCommands::SoftApply {
108                    dryrun,
109                    target_version,
110                    connect_opts,
111                } => {
112                    let db_url = connect_opts.required_db_url()?.to_string();
113                    let context = self.inner.connect(&db_url).await?;
114                    let mut runner = Runner::new(context);
115                    let report = runner.run_soft_apply(*target_version, *dryrun).await?;
116
117                    Ok(Some(report))
118                }
119                cli::MigrateCommands::ListApplied { connect_opts } => {
120                    let db_url = connect_opts.required_db_url()?.to_string();
121                    let context = self.inner.connect(&db_url).await?;
122                    let mut runner = Runner::new(context);
123                    let report = runner.list_applied().await?;
124
125                    Ok(Some(report))
126                }
127                cli::MigrateCommands::New {
128                    description,
129                    no_tx,
130                    migration_type,
131                    source,
132                } => {
133                    commands::new(
134                        description.to_string(),
135                        *no_tx,
136                        *migration_type,
137                        source.path.clone(),
138                    )?;
139
140                    Ok(None)
141                }
142            },
143        }
144    }
145
146    /// Run the CLI having already built a `MigrationContext` and initialized the
147    /// `App` from it instead of builder options.
148    pub async fn run_with_context(self) -> anyhow::Result<Option<Report>>
149    where
150        T: MigrationContext,
151    {
152        let mut runner = Runner::new(self.inner);
153        let cli = self.cli;
154
155        match cli.commands {
156            cli::TernCommands::History(history) => match &history.commands {
157                cli::HistoryCommands::Init { .. } => {
158                    runner.init_history().await?;
159
160                    Ok(None)
161                }
162                cli::HistoryCommands::Drop { .. } => {
163                    runner.drop_history().await?;
164
165                    Ok(None)
166                }
167                cli::HistoryCommands::SoftApply { .. } => Err(anyhow::anyhow!(
168                    "Deprecated: use `migrate soft-apply` instead"
169                )),
170            },
171            cli::TernCommands::Migrate(migrate) => match migrate.commands {
172                cli::MigrateCommands::Apply {
173                    dryrun,
174                    target_version,
175                    ..
176                } => {
177                    let report = runner.run_apply(target_version, dryrun).await?;
178
179                    Ok(Some(report))
180                }
181                cli::MigrateCommands::ApplyAll { dryrun, .. } => {
182                    let report = runner.run_apply_all(dryrun).await?;
183
184                    Ok(Some(report))
185                }
186                cli::MigrateCommands::SoftApply {
187                    dryrun,
188                    target_version,
189                    ..
190                } => {
191                    let report = runner.run_soft_apply(target_version, dryrun).await?;
192
193                    Ok(Some(report))
194                }
195                cli::MigrateCommands::ListApplied { .. } => {
196                    let report = runner.list_applied().await?;
197
198                    Ok(Some(report))
199                }
200                cli::MigrateCommands::New {
201                    description,
202                    no_tx,
203                    migration_type,
204                    source,
205                } => {
206                    commands::new(
207                        description.to_string(),
208                        no_tx,
209                        migration_type,
210                        source.path.clone(),
211                    )?;
212
213                    Ok(None)
214                }
215            },
216        }
217    }
218}