tauri_store/
migration.rs

1use crate::error::{Error, Result};
2use crate::store::{StoreId, StoreState};
3use itertools::Itertools;
4use semver::Version;
5use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7use std::collections::HashMap;
8use tauri_store_utils::Semver;
9
10type MigrationFn = dyn Fn(&mut StoreState) -> Result<()> + Send + Sync;
11type BeforeEachMigrationFn = dyn Fn(MigrationContext) + Send + Sync;
12
13#[doc(hidden)]
14#[derive(Default)]
15pub struct Migrator {
16  migrations: HashMap<StoreId, Vec<Migration>>,
17  before_each: Option<Box<BeforeEachMigrationFn>>,
18  pub(crate) history: MigrationHistory,
19}
20
21impl Migrator {
22  pub fn add_migration(&mut self, id: StoreId, migration: Migration) {
23    self
24      .migrations
25      .entry(id)
26      .or_default()
27      .push(migration);
28  }
29
30  pub fn add_migrations<I>(&mut self, id: StoreId, migrations: I)
31  where
32    I: IntoIterator<Item = Migration>,
33  {
34    self
35      .migrations
36      .entry(id)
37      .or_default()
38      .extend(migrations);
39  }
40
41  pub fn migrate(&mut self, id: &StoreId, state: &mut StoreState) -> MigrationResult {
42    let mut migrations = self
43      .migrations
44      .get(id)
45      .map(Vec::as_slice)
46      .unwrap_or_default()
47      .iter()
48      .sorted()
49      .collect_vec();
50
51    if let Some(last) = self.history.get(id) {
52      migrations.retain(|migration| migration.version > *last);
53    }
54
55    if migrations.is_empty() {
56      return MigrationResult::new(0);
57    }
58
59    let mut iter = migrations.iter().peekable();
60    let mut previous = None;
61    let mut done = 0;
62
63    while let Some(migration) = iter.next() {
64      let current = &migration.version;
65      if let Some(before_each) = &self.before_each {
66        let next = iter.peek().map(|it| &it.version);
67        let context = MigrationContext { id, state, current, previous, next };
68        before_each(context);
69      }
70
71      if let Err(err) = (migration.inner)(state) {
72        return MigrationResult::with_error(done, err);
73      }
74
75      self.history.set(id, current);
76      previous = Some(current);
77      done += 1;
78    }
79
80    MigrationResult::new(done)
81  }
82
83  #[doc(hidden)]
84  pub fn on_before_each<F>(&mut self, f: F)
85  where
86    F: Fn(MigrationContext) + Send + Sync + 'static,
87  {
88    self.before_each = Some(Box::new(f));
89  }
90}
91
92/// A migration step.
93pub struct Migration {
94  inner: Box<MigrationFn>,
95  version: Version,
96}
97
98impl Migration {
99  /// Creates a new migration.
100  ///
101  /// # Panics
102  ///
103  /// Panics if the version is not a valid [semver](https://semver.org/).
104  #[allow(clippy::needless_pass_by_value)]
105  pub fn new<F>(version: impl Semver, up: F) -> Self
106  where
107    F: Fn(&mut StoreState) -> Result<()> + Send + Sync + 'static,
108  {
109    Self {
110      inner: Box::new(up),
111      version: version.semver(),
112    }
113  }
114
115  /// Version of the migration.
116  pub fn version(&self) -> &Version {
117    &self.version
118  }
119}
120
121impl PartialOrd for Migration {
122  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
123    Some(self.cmp(other))
124  }
125}
126
127impl Ord for Migration {
128  fn cmp(&self, other: &Self) -> Ordering {
129    self.version.cmp(&other.version)
130  }
131}
132
133impl PartialEq for Migration {
134  fn eq(&self, other: &Self) -> bool {
135    self.version == other.version
136  }
137}
138
139impl Eq for Migration {}
140
141/// Context for a migration step.
142#[derive(Debug)]
143pub struct MigrationContext<'a> {
144  pub id: &'a StoreId,
145  pub state: &'a StoreState,
146  pub current: &'a Version,
147  pub previous: Option<&'a Version>,
148  pub next: Option<&'a Version>,
149}
150
151#[derive(Clone, Debug, Default, Serialize, Deserialize)]
152pub(crate) struct MigrationHistory(HashMap<StoreId, Version>);
153
154impl MigrationHistory {
155  pub fn get(&self, id: &StoreId) -> Option<&Version> {
156    self.0.get(id)
157  }
158
159  pub fn set(&mut self, id: &StoreId, version: &Version) {
160    self.0.insert(id.clone(), version.clone());
161  }
162}
163
164// This way the meta can be updated even if some migrations fail.
165#[doc(hidden)]
166pub struct MigrationResult {
167  pub(crate) done: u32,
168  pub(crate) error: Option<Error>,
169}
170
171impl MigrationResult {
172  pub const fn new(done: u32) -> Self {
173    Self { done, error: None }
174  }
175
176  pub const fn with_error(done: u32, error: Error) -> Self {
177    Self { done, error: Some(error) }
178  }
179}