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}