Skip to main content

perspective_migrate/
step.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use serde_json::{Map, Value};
14
15use crate::error::MigrateError;
16use crate::json::escape;
17use crate::notice::Notice;
18use crate::plugins::resolve_plugin;
19use crate::version::Version;
20use crate::{Kind, check_version};
21
22/// Rewrites one config object from the shape before [`Step::at`] to the shape
23/// after it.
24pub type Apply = fn(&mut Map<String, Value>, &mut StepCx) -> Result<(), MigrateError>;
25
26/// One schema change: `apply` runs on every object it concerns when the
27/// source version predates `at`.
28#[derive(Clone, Copy)]
29pub struct Step {
30    /// The first release that shipped the new shape.
31    pub at: Version,
32
33    /// A stable identifier, reported in [`crate::Migrated::steps`].
34    pub id: &'static str,
35
36    /// The rewrite, which must only touch keys present in its input.
37    pub apply: Apply,
38}
39
40/// The context a [`Step`] runs in.
41pub struct StepCx<'a> {
42    kind: Kind,
43    from: Version,
44    step: &'static str,
45    pointer: &'a str,
46    known_plugins: &'a [&'static str],
47    notices: &'a mut Vec<Notice>,
48}
49
50impl StepCx<'_> {
51    /// The kind of document being migrated.
52    pub fn kind(&self) -> Kind {
53        self.kind
54    }
55
56    /// The version the document was written by.
57    pub fn from(&self) -> Version {
58        self.from
59    }
60
61    /// Record that `value`, found at `path` below this object, was removed.
62    pub fn dropped(&mut self, path: &[&str], value: Value) {
63        let pointer = self.pointer_to(path);
64        self.notices.push(Notice::Dropped {
65            step: self.step,
66            pointer,
67            value,
68        });
69    }
70
71    /// Record that the value at `path` below this object was approximated.
72    pub fn approximated(&mut self, path: &[&str], reason: &'static str) {
73        let pointer = self.pointer_to(path);
74        self.notices.push(Notice::Approximated {
75            step: self.step,
76            pointer,
77            reason,
78        });
79    }
80
81    /// A [`MigrateError::Malformed`] for the value at `path` below this object.
82    pub fn malformed(&self, path: &[&str], expected: &'static str) -> MigrateError {
83        MigrateError::Malformed {
84            step: self.step,
85            pointer: self.pointer_to(path),
86            expected,
87        }
88    }
89
90    /// A [`MigrateError::Conflict`] between `keys` of this object.
91    pub fn conflict(&self, keys: &[&str]) -> MigrateError {
92        MigrateError::Conflict {
93            step: self.step,
94            pointer: self.pointer.to_owned(),
95            keys: keys.iter().map(|x| (*x).to_owned()).collect(),
96        }
97    }
98
99    /// A [`MigrateError::MissingTable`] for the workspace panel `panel`.
100    pub fn missing_table(&self) -> MigrateError {
101        MigrateError::MissingTable {
102            step: self.step,
103            pointer: self.pointer.to_owned(),
104        }
105    }
106
107    pub(crate) fn resolve_plugin(&self, name: &str) -> Option<&'static str> {
108        resolve_plugin(name, self.known_plugins)
109    }
110
111    fn pointer_to(&self, path: &[&str]) -> String {
112        path.iter().fold(self.pointer.to_owned(), |acc, seg| {
113            format!("{acc}/{}", escape(seg))
114        })
115    }
116}
117
118#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
119pub(crate) enum Layer {
120    View,
121    Viewer,
122    ColumnStyle,
123    Plugin,
124    Workspace,
125}
126
127pub(crate) struct Entry<'a> {
128    pub layer: Layer,
129    pub plugins: &'a [&'static str],
130    pub step: Step,
131}
132
133pub(crate) struct Run<'a> {
134    pub kind: Kind,
135    pub from: Version,
136    pub schema: Version,
137    pub known_plugins: &'a [&'static str],
138    pub notices: Vec<Notice>,
139    pub steps: Vec<&'static str>,
140}
141
142impl Run<'_> {
143    pub fn apply(&mut self, entries: &[Entry], root: &mut Value) -> Result<(), MigrateError> {
144        for entry in entries {
145            let notices = self.notices.len();
146            let mut changed = false;
147            for pointer in targets(self.kind, entry.layer, root) {
148                let Some(Value::Object(target)) = root.pointer_mut(&pointer) else {
149                    continue;
150                };
151
152                let from = match target.get("version").and_then(Value::as_str) {
153                    Some(version) => check_version(version, self.schema)?,
154                    None => self.from,
155                };
156
157                if from >= entry.step.at
158                    || (entry.layer == Layer::Plugin && !self.owns(entry.plugins, target))
159                {
160                    continue;
161                }
162
163                let before = target.clone();
164                let mut cx = StepCx {
165                    kind: self.kind_at(&pointer),
166                    from,
167                    step: entry.step.id,
168                    pointer: &pointer,
169                    known_plugins: self.known_plugins,
170                    notices: &mut self.notices,
171                };
172
173                (entry.step.apply)(target, &mut cx)?;
174                changed |= *target != before;
175            }
176
177            if changed || self.notices.len() > notices {
178                self.steps.push(entry.step.id);
179            }
180        }
181
182        Ok(())
183    }
184
185    fn kind_at(&self, pointer: &str) -> Kind {
186        if self.kind == Kind::Workspace
187            && (pointer.starts_with("/panels/") || pointer.starts_with("/viewers/"))
188        {
189            Kind::Panel
190        } else {
191            self.kind
192        }
193    }
194
195    fn owns(&self, plugins: &[&'static str], target: &Map<String, Value>) -> bool {
196        target
197            .get("plugin")
198            .and_then(Value::as_str)
199            .and_then(|name| resolve_plugin(name, self.known_plugins))
200            .is_some_and(|name| plugins.contains(&name))
201    }
202}
203
204fn targets(kind: Kind, layer: Layer, root: &Value) -> Vec<String> {
205    match (kind, layer) {
206        (Kind::View, Layer::View) => vec![String::new()],
207        (Kind::View, _) => vec![],
208        (Kind::Viewer, Layer::View) => {
209            let mut out = vec![String::new()];
210            let mut pointer = String::from("/table");
211            while root.pointer(&pointer).is_some_and(Value::is_object) {
212                out.push(pointer.clone());
213                pointer.push_str("/table");
214            }
215
216            out
217        },
218        (Kind::Viewer, Layer::Workspace) => vec![],
219        (Kind::Viewer, _) => vec![String::new()],
220        (Kind::Panel, Layer::Workspace) => vec![],
221        (Kind::Panel, _) => vec![String::new()],
222        (Kind::Workspace, Layer::Workspace) => vec![String::new()],
223        (Kind::Workspace, Layer::View) => {
224            let mut out = panels(root);
225            out.extend(children(root, "derived_tables"));
226            out
227        },
228        (Kind::Workspace, _) => panels(root),
229    }
230}
231
232pub(crate) fn panels(root: &Value) -> Vec<String> {
233    let mut out = children(root, "panels");
234    out.extend(children(root, "viewers"));
235    out
236}
237
238fn children(root: &Value, key: &str) -> Vec<String> {
239    root.get(key)
240        .and_then(Value::as_object)
241        .map(|map| {
242            map.keys()
243                .map(|name| format!("/{key}/{}", escape(name)))
244                .collect()
245        })
246        .unwrap_or_default()
247}