Skip to main content

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