mod column_style;
mod css;
mod error;
mod json;
mod notice;
mod plugin;
mod plugins;
mod step;
mod validate;
mod version;
mod view;
mod viewer;
mod workspace;
pub use error::MigrateError;
pub use notice::Notice;
pub use plugin::PluginMigration;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub use step::{Apply, Step, StepCx};
use step::{Entry, Layer, Run, panels};
pub use validate::{Form, ValidateError, validate};
pub use version::Version;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Kind {
View,
Viewer,
Panel,
Workspace,
}
impl std::str::FromStr for Kind {
type Err = String;
fn from_str(kind: &str) -> Result<Self, Self::Err> {
match kind {
"view" => Ok(Kind::View),
"viewer" => Ok(Kind::Viewer),
"panel" => Ok(Kind::Panel),
"workspace" => Ok(Kind::Workspace),
_ => Err(format!(
"Unknown config kind `{kind}`; expected `view`, `viewer`, `panel` or `workspace`"
)),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Migrated {
pub value: Value,
pub notices: Vec<Notice>,
pub steps: Vec<&'static str>,
}
pub struct Migrator {
plugins: Vec<Box<dyn PluginMigration>>,
}
impl Default for Migrator {
fn default() -> Self {
Self::new()
}
}
impl Migrator {
pub fn new() -> Self {
Self {
plugins: plugins::first_party(),
}
}
pub fn with_plugin(mut self, plugin: impl PluginMigration + 'static) -> Self {
let owned = self
.plugins
.iter()
.flat_map(|x| x.plugins().iter().copied())
.collect::<Vec<_>>();
if let Some(name) = plugin.plugins().iter().find(|x| owned.contains(x)) {
panic!("plugin `{name}` already has a migration");
}
self.plugins.push(Box::new(plugin));
self
}
pub fn migrate(
&self,
kind: Kind,
from: Option<&str>,
value: Value,
) -> Result<Migrated, MigrateError> {
let mut value = value;
if !value.is_object() {
return Err(MigrateError::NotAnObject);
}
let schema = self.schema_version();
let from = match value.get("version").and_then(Value::as_str).or(from) {
Some(version) => check_version(version, schema)?,
None => Version::new(2, 0, 0),
};
let entries = self.entries();
let known_plugins = self.plugin_names();
let mut run = Run {
kind,
from,
schema,
known_plugins: &known_plugins,
notices: vec![],
steps: vec![],
};
run.apply(&entries, &mut value)?;
stamp(kind, &schema.to_string(), &mut value);
validate(kind, Form::Update, &value)?;
Ok(Migrated {
value,
notices: run.notices,
steps: run.steps,
})
}
pub fn step_ids(&self) -> Vec<&'static str> {
self.entries().iter().map(|x| x.step.id).collect()
}
pub fn newest_step_version(&self) -> Version {
self.entries()
.iter()
.map(|x| x.step.at)
.max()
.unwrap_or_else(Version::crate_version)
}
pub fn schema_version(&self) -> Version {
self.newest_step_version().max(Version::crate_version())
}
fn plugin_names(&self) -> Vec<&'static str> {
let mut names = plugins::REGISTRATION_ORDER.to_vec();
for name in self.plugins.iter().flat_map(|x| x.plugins().iter()) {
if !names.contains(name) {
names.push(name);
}
}
names
}
fn entries(&self) -> Vec<Entry<'_>> {
let fixed = [
(Layer::View, view::STEPS),
(Layer::Viewer, viewer::STEPS),
(Layer::ColumnStyle, column_style::STEPS),
(Layer::Workspace, workspace::STEPS),
];
let mut entries = fixed
.into_iter()
.flat_map(|(layer, steps)| {
steps.iter().map(move |step| Entry {
layer,
plugins: &[],
step: *step,
})
})
.collect::<Vec<_>>();
for plugin in &self.plugins {
entries.extend(plugin.steps().into_iter().map(|step| Entry {
layer: Layer::Plugin,
plugins: plugin.plugins(),
step,
}));
}
entries.sort_by_key(|x| (x.step.at, x.layer));
entries
}
}
pub(crate) fn check_version(version: &str, schema: Version) -> Result<Version, MigrateError> {
let parsed = Version::parse(version).ok_or_else(|| MigrateError::InvalidVersion {
version: version.to_owned(),
})?;
if parsed < Version::new(2, 0, 0) {
Err(MigrateError::Unsupported {
version: version.to_owned(),
})
} else if parsed > schema {
Err(MigrateError::FromFuture {
version: version.to_owned(),
})
} else {
Ok(parsed)
}
}
fn stamp(kind: Kind, version: &str, value: &mut Value) {
let version = Value::from(version);
match kind {
Kind::View => {},
Kind::Viewer | Kind::Panel => {
value["version"] = version;
},
Kind::Workspace => {
for pointer in panels(value) {
if let Some(Value::Object(panel)) = value.pointer_mut(&pointer)
&& panel.contains_key("version")
{
panel.insert("version".to_owned(), version.clone());
}
}
value["version"] = version;
},
}
}
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn migrate(kind: &str, from: Option<String>, value: JsValue) -> Result<JsValue, JsError> {
let kind: Kind = kind.parse().map_err(|e: String| JsError::new(&e))?;
let value: serde_json::Value = serde_wasm_bindgen::from_value(value)?;
let migrated = Migrator::new().migrate(kind, from.as_deref(), value)?;
let result = serde_json::json!({
"value": migrated.value,
"notices": migrated.notices,
"steps": migrated.steps,
});
Ok(result.serialize(&serde_wasm_bindgen::Serializer::json_compatible())?)
}
#[wasm_bindgen]
pub fn validate_config(kind: &str, value: JsValue, strict: Option<bool>) -> Result<(), JsError> {
let kind: Kind = kind.parse().map_err(|e: String| JsError::new(&e))?;
let form = if strict.unwrap_or(false) {
Form::Complete
} else {
Form::Update
};
let value: serde_json::Value = serde_wasm_bindgen::from_value(value)?;
validate(kind, form, &value).map_err(|e| JsError::new(&e.to_string()))
}