1mod column_style;
35mod css;
36mod error;
37mod json;
38mod notice;
39mod plugin;
40mod plugins;
41mod step;
42mod validate;
43mod version;
44mod view;
45mod viewer;
46mod workspace;
47
48pub use error::MigrateError;
49pub use notice::Notice;
50pub use plugin::PluginMigration;
51use serde::{Deserialize, Serialize};
52use serde_json::Value;
53pub use step::{Apply, Step, StepCx};
54use step::{Entry, Layer, Run, panels};
55pub use validate::{Form, ValidateError, validate};
56pub use version::Version;
57
58#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
60#[serde(rename_all = "lowercase")]
61pub enum Kind {
62 View,
64
65 Viewer,
67
68 Panel,
71
72 Workspace,
74}
75
76impl std::str::FromStr for Kind {
77 type Err = String;
78
79 fn from_str(kind: &str) -> Result<Self, Self::Err> {
80 match kind {
81 "view" => Ok(Kind::View),
82 "viewer" => Ok(Kind::Viewer),
83 "panel" => Ok(Kind::Panel),
84 "workspace" => Ok(Kind::Workspace),
85 _ => Err(format!(
86 "Unknown config kind `{kind}`; expected `view`, `viewer`, `panel` or `workspace`"
87 )),
88 }
89 }
90}
91
92#[derive(Clone, Debug, PartialEq)]
94pub struct Migrated {
95 pub value: Value,
97
98 pub notices: Vec<Notice>,
100
101 pub steps: Vec<&'static str>,
103}
104
105pub struct Migrator {
107 plugins: Vec<Box<dyn PluginMigration>>,
108}
109
110impl Default for Migrator {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116impl Migrator {
117 pub fn new() -> Self {
119 Self {
120 plugins: plugins::first_party(),
121 }
122 }
123
124 pub fn with_plugin(mut self, plugin: impl PluginMigration + 'static) -> Self {
130 let owned = self
131 .plugins
132 .iter()
133 .flat_map(|x| x.plugins().iter().copied())
134 .collect::<Vec<_>>();
135
136 if let Some(name) = plugin.plugins().iter().find(|x| owned.contains(x)) {
137 panic!("plugin `{name}` already has a migration");
138 }
139
140 self.plugins.push(Box::new(plugin));
141 self
142 }
143
144 pub fn migrate(
157 &self,
158 kind: Kind,
159 from: Option<&str>,
160 value: Value,
161 ) -> Result<Migrated, MigrateError> {
162 let mut value = value;
163 if !value.is_object() {
164 return Err(MigrateError::NotAnObject);
165 }
166
167 let schema = self.schema_version();
168 let from = match value.get("version").and_then(Value::as_str).or(from) {
169 Some(version) => check_version(version, schema)?,
170 None => Version::new(2, 0, 0),
171 };
172
173 let entries = self.entries();
174 let known_plugins = self.plugin_names();
175 let mut run = Run {
176 kind,
177 from,
178 schema,
179 known_plugins: &known_plugins,
180 notices: vec![],
181 steps: vec![],
182 };
183
184 run.apply(&entries, &mut value)?;
185 stamp(kind, &schema.to_string(), &mut value);
186 validate(kind, Form::Update, &value)?;
187 Ok(Migrated {
188 value,
189 notices: run.notices,
190 steps: run.steps,
191 })
192 }
193
194 pub fn step_ids(&self) -> Vec<&'static str> {
196 self.entries().iter().map(|x| x.step.id).collect()
197 }
198
199 pub fn newest_step_version(&self) -> Version {
201 self.entries()
202 .iter()
203 .map(|x| x.step.at)
204 .max()
205 .unwrap_or_else(Version::crate_version)
206 }
207
208 pub fn schema_version(&self) -> Version {
210 self.newest_step_version().max(Version::crate_version())
211 }
212
213 fn plugin_names(&self) -> Vec<&'static str> {
214 let mut names = plugins::REGISTRATION_ORDER.to_vec();
215 for name in self.plugins.iter().flat_map(|x| x.plugins().iter()) {
216 if !names.contains(name) {
217 names.push(name);
218 }
219 }
220
221 names
222 }
223
224 fn entries(&self) -> Vec<Entry<'_>> {
225 let fixed = [
226 (Layer::View, view::STEPS),
227 (Layer::Viewer, viewer::STEPS),
228 (Layer::ColumnStyle, column_style::STEPS),
229 (Layer::Workspace, workspace::STEPS),
230 ];
231
232 let mut entries = fixed
233 .into_iter()
234 .flat_map(|(layer, steps)| {
235 steps.iter().map(move |step| Entry {
236 layer,
237 plugins: &[],
238 step: *step,
239 })
240 })
241 .collect::<Vec<_>>();
242
243 for plugin in &self.plugins {
244 entries.extend(plugin.steps().into_iter().map(|step| Entry {
245 layer: Layer::Plugin,
246 plugins: plugin.plugins(),
247 step,
248 }));
249 }
250
251 entries.sort_by_key(|x| (x.step.at, x.layer));
252 entries
253 }
254}
255
256pub(crate) fn check_version(version: &str, schema: Version) -> Result<Version, MigrateError> {
257 let parsed = Version::parse(version).ok_or_else(|| MigrateError::InvalidVersion {
258 version: version.to_owned(),
259 })?;
260
261 if parsed < Version::new(2, 0, 0) {
262 Err(MigrateError::Unsupported {
263 version: version.to_owned(),
264 })
265 } else if parsed > schema {
266 Err(MigrateError::FromFuture {
267 version: version.to_owned(),
268 })
269 } else {
270 Ok(parsed)
271 }
272}
273
274fn stamp(kind: Kind, version: &str, value: &mut Value) {
275 let version = Value::from(version);
276 match kind {
277 Kind::View => {},
278 Kind::Viewer | Kind::Panel => {
279 value["version"] = version;
280 },
281 Kind::Workspace => {
282 for pointer in panels(value) {
283 if let Some(Value::Object(panel)) = value.pointer_mut(&pointer)
284 && panel.contains_key("version")
285 {
286 panel.insert("version".to_owned(), version.clone());
287 }
288 }
289
290 value["version"] = version;
291 },
292 }
293}
294
295use wasm_bindgen::prelude::*;
296
297#[wasm_bindgen]
300pub fn migrate(kind: &str, from: Option<String>, value: JsValue) -> Result<JsValue, JsError> {
301 let kind: Kind = kind.parse().map_err(|e: String| JsError::new(&e))?;
302 let value: serde_json::Value = serde_wasm_bindgen::from_value(value)?;
303 let migrated = Migrator::new().migrate(kind, from.as_deref(), value)?;
304 let result = serde_json::json!({
305 "value": migrated.value,
306 "notices": migrated.notices,
307 "steps": migrated.steps,
308 });
309
310 Ok(result.serialize(&serde_wasm_bindgen::Serializer::json_compatible())?)
311}
312
313#[wasm_bindgen]
316pub fn validate_config(kind: &str, value: JsValue, strict: Option<bool>) -> Result<(), JsError> {
317 let kind: Kind = kind.parse().map_err(|e: String| JsError::new(&e))?;
318 let form = if strict.unwrap_or(false) {
319 Form::Complete
320 } else {
321 Form::Update
322 };
323
324 let value: serde_json::Value = serde_wasm_bindgen::from_value(value)?;
325 validate(kind, form, &value).map_err(|e| JsError::new(&e.to_string()))
326}