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
//! Migration configuration is based on injecting an [MigrationConfigProvider], which can later be
//! used to retrieve [MigrationConfig].
//!
//! By default, the config is created with opinionated default values, which can then be overwritten
//! by values from `springtime.json` file under the `migration` key.

use config::{Config, File};
use serde::Deserialize;
use springtime::config::CONFIG_FILE;
use springtime::future::{BoxFuture, FutureExt};
use springtime_di::component_registry::conditional::unregistered_component;
use springtime_di::instance_provider::ErrorPtr;
use springtime_di::{component_alias, injectable, Component};
use std::sync::Arc;

/// A [Deserialize] version of [Target](refinery_core::Target).
#[derive(Clone, Copy, Debug, Deserialize)]
pub enum Target {
    /// Latest version.
    Latest,
    /// User-provided version.
    Version(u32),
    /// Don't run migrations, just update the migration table to latest version.
    Fake,
    /// Don't run migrations, just update the migration table to user-provided version.
    FakeVersion(u32),
}

impl From<Target> for refinery_core::Target {
    fn from(value: Target) -> Self {
        match value {
            Target::Latest => refinery_core::Target::Latest,
            Target::Version(version) => refinery_core::Target::Version(version),
            Target::Fake => refinery_core::Target::Fake,
            Target::FakeVersion(version) => refinery_core::Target::FakeVersion(version),
        }
    }
}

/// Migration configuration.
#[non_exhaustive]
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
pub struct MigrationConfig {
    /// Should migrations run on application start.
    pub run_migrations_on_start: bool,
    /// The target version up to which migrate.
    pub target: Target,
    /// Group migrations in a single transaction.
    pub grouped: bool,
    /// Should abort migration process if divergent migrations are found i.e. applied migrations
    /// with the same version but different name or checksum from the ones on the filesystem.
    pub abort_divergent: bool,
    /// Should abort if missing migrations are found i.e. applied migrations that are not found on
    /// the filesystem, or migrations found on filesystem with a version inferior to the last one
    /// applied but not applied
    pub abort_missing: bool,
    /// Table name for migration data.
    pub migration_table_name: String,
}

impl Default for MigrationConfig {
    fn default() -> Self {
        Self {
            run_migrations_on_start: true,
            target: Target::Latest,
            grouped: false,
            abort_divergent: true,
            abort_missing: true,
            migration_table_name: "refinery_schema_history".to_string(),
        }
    }
}

impl MigrationConfig {
    fn init_from_config() -> Result<Self, ErrorPtr> {
        Config::builder()
            .add_source(File::with_name(CONFIG_FILE).required(false))
            .build()
            .and_then(|config| config.try_deserialize::<MigrationConfigWrapper>())
            .map(|config| config.migration)
            .map_err(|error| Arc::new(error) as ErrorPtr)
    }
}

/// Provider for [MigrationConfig]. The primary instance of the provider will be used to retrieve
/// migration configuration.
#[injectable]
pub trait MigrationConfigProvider {
    /// Provide current config.
    fn config(&self) -> BoxFuture<'_, Result<&MigrationConfig, ErrorPtr>>;
}

#[derive(Component)]
#[component(priority = -128, condition = "unregistered_component::<dyn MigrationConfigProvider + Send + Sync>", constructor = "DefaultMigrationConfigProvider::new")]
struct DefaultMigrationConfigProvider {
    // cached init result
    #[component(ignore)]
    config: Result<MigrationConfig, ErrorPtr>,
}

#[component_alias]
impl MigrationConfigProvider for DefaultMigrationConfigProvider {
    fn config(&self) -> BoxFuture<'_, Result<&MigrationConfig, ErrorPtr>> {
        async {
            match &self.config {
                Ok(config) => Ok(config),
                Err(error) => Err(error.clone()),
            }
        }
        .boxed()
    }
}

impl DefaultMigrationConfigProvider {
    fn new() -> BoxFuture<'static, Result<Self, ErrorPtr>> {
        async {
            Ok(Self {
                config: MigrationConfig::init_from_config(),
            })
        }
        .boxed()
    }
}

#[derive(Deserialize, Default)]
#[serde(default)]
struct MigrationConfigWrapper {
    migration: MigrationConfig,
}