tern_cli/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! The CLI for the [`tern`][tern-docs] migration library.
//!
//! This exports the [`App`] type and [`ContextOptions`], which help turn a
//! project using `tern` into a CLI.
//!
//! The `App` is the CLI. `ContextOptions` exists to connect a generic context
//! to the CLI since it is the CLI that supplies the database URL, surely
//! required of the context, but not anything else the context might need to
//! initialize.
//!
//! [tern-docs]: https://docs.rs/crate/tern/latest
use clap::Parser;
use tern_core::error::TernResult;
use tern_core::future::Future;
use tern_core::migration::MigrationContext;
use tern_core::runner::Runner;

mod cli;
mod commands;

/// A type that can build a particular context given a database url.
/// This is needed because the context is arbitrary, yet the CLI options have
/// the database URL, which is certainly required to build it.
pub trait ContextOptions {
    type Ctx: MigrationContext;

    /// Establish a connection with this context.
    fn connect(&self, db_url: &str) -> impl Future<Output = TernResult<Self::Ctx>>;
}

/// The CLI app to run.  This wraps the functionality of the context that `Opts`
/// creates.
///
/// ## Usage
///
/// To connect to the given database, the CLI needs a database url, which can be
/// provided via the environment variable `DATABASE_URL` or using the option
/// `-D/--database-url` available to a command/subcommand.
///
/// ```terminal
/// > $ my-app --help
/// Usage: my-app <COMMAND>
///
/// Commands:
///   migrate  Operations on the set of migration files
///   history  Operations on the table storing the history of these migrations
///   help     Print this message or the help of the given subcommand(s)
/// ```
pub struct App<Opts> {
    opts: Opts,
    cli: cli::Tern,
}

impl<Opts> App<Opts>
where
    Opts: ContextOptions,
{
    pub fn new(opts: Opts) -> Self {
        let cli = cli::Tern::parse();
        Self { opts, cli }
    }

    pub async fn run(&self) -> anyhow::Result<()> {
        match &self.cli.commands {
            cli::TernCommands::History(history) => match &history.commands {
                cli::HistoryCommands::Init { connect_opts } => {
                    let db_url = connect_opts.required_db_url()?.to_string();
                    let context = self.opts.connect(&db_url).await?;
                    let mut runner = Runner::new(context);
                    runner.init_history().await?;

                    Ok(())
                }
                cli::HistoryCommands::Drop { connect_opts } => {
                    let db_url = connect_opts.required_db_url()?.to_string();
                    let context = self.opts.connect(&db_url).await?;
                    let mut runner = Runner::new(context);
                    runner.drop_history().await?;

                    Ok(())
                }
                cli::HistoryCommands::SoftApply {
                    from_version,
                    to_version,
                    connect_opts,
                } => {
                    let db_url = connect_opts.required_db_url()?.to_string();
                    let context = self.opts.connect(&db_url).await?;
                    let mut runner = Runner::new(context);
                    let report = runner.soft_apply(*from_version, *to_version).await?;
                    log::info!("{report:#?}");

                    Ok(())
                }
            },
            cli::TernCommands::Migrate(migrate) => match &migrate.commands {
                cli::MigrateCommands::ApplyAll {
                    dryrun,
                    connect_opts,
                } => {
                    let db_url = connect_opts.required_db_url()?.to_string();
                    let context = self.opts.connect(&db_url).await?;
                    let mut runner = Runner::new(context);
                    let report = if *dryrun {
                        runner.dryrun().await?
                    } else {
                        runner.apply_all().await?
                    };
                    log::info!("{report:#?}");

                    Ok(())
                }
                cli::MigrateCommands::ListApplied { connect_opts } => {
                    let db_url = connect_opts.required_db_url()?.to_string();
                    let context = self.opts.connect(&db_url).await?;
                    let mut runner = Runner::new(context);
                    let report = runner.list_applied().await?;
                    log::info!("{report:#?}");

                    Ok(())
                }
                cli::MigrateCommands::New {
                    description,
                    no_tx,
                    migration_type,
                    source,
                } => commands::new(
                    description.to_string(),
                    *no_tx,
                    *migration_type,
                    source.path.clone(),
                ),
            },
        }
    }
}