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
//! 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 with 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.
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(),
                ),
            },
        }
    }
}