use std::cell::RefCell;
use crate::abi::{self, MigrationStep, Status, Str};
use crate::content::Guid;
use crate::{ItemId, Load, Off};
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{0}")]
pub struct MigrationError(String);
impl MigrationError {
#[must_use]
pub fn new(why: impl Into<String>) -> Self {
Self(why.into())
}
}
impl From<&str> for MigrationError {
fn from(why: &str) -> Self {
Self(why.to_owned())
}
}
impl From<String> for MigrationError {
fn from(why: String) -> Self {
Self(why)
}
}
impl From<toml::de::Error> for MigrationError {
fn from(error: toml::de::Error) -> Self {
Self(crate::config::reason(None, &error))
}
}
type Map = Box<dyn Fn(&str) -> Result<String, MigrationError> + Send + Sync>;
struct Step {
kind: u32,
from: String,
to: String,
from_guid: Guid,
to_guid: Guid,
map: Option<Map>,
}
impl core::fmt::Debug for Step {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Step")
.field("kind", &self.kind)
.field("from", &self.from)
.field("to", &self.to)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct Migration {
id: String,
steps: Vec<Step>,
}
impl Migration {
fn push(mut self, kind: u32, from: &str, to: &str, map: Option<Map>) -> Self {
self.steps.push(Step {
kind,
from: from.to_owned(),
to: to.to_owned(),
from_guid: Guid([0; 16]),
to_guid: Guid([0; 16]),
map,
});
self
}
#[must_use]
pub fn rename_setting(self, from: &str, to: &str) -> Self {
self.push(MigrationStep::RENAME_SETTING, from, to, None)
}
#[must_use]
pub fn remove_setting(self, key: &str) -> Self {
self.push(MigrationStep::REMOVE_SETTING, key, "", None)
}
#[must_use]
pub fn map_setting(
self,
key: &str,
map: impl Fn(toml::Value) -> Result<toml::Value, MigrationError> + Send + Sync + 'static,
) -> Self {
let map: Map = Box::new(move |text| {
let value = parse_value(text)?;
write_value(&map(value)?)
});
self.push(MigrationStep::MAP_SETTING, key, "", Some(map))
}
#[must_use]
pub fn take_setting(self, from: &str, key: &str, to: &str) -> Self {
let from = format!("{from}:{key}");
self.push(MigrationStep::TAKE_SETTING, &from, to, None)
}
#[must_use]
pub fn rename_store(self, from: &str, to: &str) -> Self {
self.push(MigrationStep::RENAME_STORE, from, to, None)
}
#[must_use]
pub fn map_store(
self,
name: &str,
map: impl Fn(&mut toml::Table) -> Result<(), MigrationError> + Send + Sync + 'static,
) -> Self {
let map: Map = Box::new(move |text| {
let mut table: toml::Table = toml::from_str(text)
.map_err(|e| MigrationError(crate::config::reason(Some(text), &e)))?;
map(&mut table)?;
toml::to_string(&table).map_err(|e| MigrationError(e.to_string()))
});
self.push(MigrationStep::MAP_STORE, name, "", Some(map))
}
#[must_use]
pub fn rename_content(mut self, from: Guid, to: Guid) -> Self {
self.steps.push(Step {
kind: MigrationStep::RENAME_CONTENT,
from: from.to_string(),
to: to.to_string(),
from_guid: from,
to_guid: to,
map: None,
});
self
}
}
#[derive(Debug, Default)]
pub struct Migrations {
list: Vec<Migration>,
}
impl Migrations {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add(mut self, id: &str, build: impl FnOnce(Migration) -> Migration) -> Self {
self.list.push(build(Migration {
id: id.to_owned(),
steps: Vec::new(),
}));
self
}
#[must_use]
pub fn ids(&self) -> Vec<&str> {
self.list.iter().map(|m| m.id.as_str()).collect()
}
fn check(&self) -> Result<(), String> {
for (index, migration) in self.list.iter().enumerate() {
if migration.id.is_empty() || migration.id.chars().any(char::is_control) {
return Err(format!("migration {index}: an id is plain text"));
}
if self.list[..index].iter().any(|m| m.id == migration.id) {
return Err(format!("migration {}: the id is used twice", migration.id));
}
for step in &migration.steps {
let renames = matches!(
step.kind,
MigrationStep::RENAME_SETTING
| MigrationStep::RENAME_STORE
| MigrationStep::TAKE_SETTING
);
if step.kind == MigrationStep::TAKE_SETTING
&& step
.from
.split_once(':')
.is_none_or(|(id, key)| !abi::is_valid_former_id(id) || key.is_empty())
{
return Err(format!(
"migration {}: a setting is taken from <mod id>:<key>, not {}",
migration.id, step.from
));
}
if step.from.is_empty() || (renames && step.to.is_empty()) {
return Err(format!("migration {}: a step names nothing", migration.id));
}
if step.kind == MigrationStep::RENAME_CONTENT && step.from_guid == step.to_guid {
return Err(format!(
"migration {}: content {} renamed to itself",
migration.id, step.from
));
}
}
}
Ok(())
}
}
pub(crate) fn parse_value(text: &str) -> Result<toml::Value, MigrationError> {
let text = format!("v = {text}");
let mut table: toml::Table = toml::from_str(&text)
.map_err(|e| MigrationError(crate::config::reason(Some(&text), &e)))?;
table.remove("v").ok_or_else(|| "no value".into())
}
pub(crate) fn write_value(value: &toml::Value) -> Result<String, MigrationError> {
let text = value.to_string();
parse_value(&text)?;
Ok(text)
}
struct Kept {
list: Vec<Migration>,
}
static KEPT: crate::fresh::Fresh<Kept> = crate::fresh::Fresh::new();
thread_local! {
static OUT: RefCell<String> = const { RefCell::new(String::new()) };
}
pub(crate) fn old_guids(guid: Guid) -> Vec<Guid> {
KEPT.get()
.map_or_else(Vec::new, |kept| renamed_to(&kept.list, guid))
}
fn renamed_to(list: &[Migration], guid: Guid) -> Vec<Guid> {
let mut found: Vec<Guid> = Vec::new();
let mut at = 0;
let mut wanted = guid;
loop {
for (from, to) in renames(list) {
if to == wanted && from != guid && !found.contains(&from) {
found.push(from);
}
}
let Some(&next) = found.get(at) else {
return found;
};
wanted = next;
at += 1;
}
}
fn renames(list: &[Migration]) -> impl Iterator<Item = (Guid, Guid)> + '_ {
list.iter()
.flat_map(|m| &m.steps)
.filter(|s| s.kind == MigrationStep::RENAME_CONTENT)
.map(|s| (s.from_guid, s.to_guid))
}
#[must_use]
pub fn current_id(id: ItemId) -> ItemId {
KEPT.get().map_or(id, |kept| renamed_from(&kept.list, id))
}
fn renamed_from(list: &[Migration], id: ItemId) -> ItemId {
let mut current = id;
for _ in renames(list) {
match renames(list).find(|(from, _)| ItemId::from_guid(*from) == current) {
Some((_, to)) if ItemId::from_guid(to) != current && ItemId::from_guid(to) != id => {
current = ItemId::from_guid(to);
}
_ => break,
}
}
current
}
unsafe extern "system" fn run_map(
_user: *mut core::ffi::c_void,
migration: u32,
step: u32,
input: Str,
out: *mut Str,
) -> Status {
let Some(kept) = KEPT.get() else {
return Status::MISSING;
};
let map = kept
.list
.get(migration as usize)
.and_then(|m| m.steps.get(step as usize))
.and_then(|s| s.map.as_ref());
let Some(map) = map else {
return Status::MISSING;
};
let Some(input) = (unsafe { input.as_str() }) else {
return Status::BAD_ARGS;
};
let (status, text) = crate::host::guard(
"panic in a migration",
|| (Status::FAULTED, "the migration panicked".to_owned()),
|| match map(input) {
Ok(text) => (Status::OK, text),
Err(reason) => (Status::REFUSED, reason.0),
},
);
OUT.with_borrow_mut(|held| {
*held = text;
if !out.is_null() {
unsafe { out.write(Str::new(held)) };
}
});
status
}
impl Load<'_> {
pub fn migrate(&mut self, migrations: Migrations) -> Result<(), Off> {
migrations
.check()
.map_err(|reason| Off::new(format!("migrations: {reason}")))?;
if KEPT
.set(Kept {
list: migrations.list,
})
.is_err()
{
return Err(Off::new("migrations: declared twice"));
}
let Some(kept) = KEPT.get() else {
return Err(Off::new("migrations: not kept"));
};
let steps: Vec<Vec<MigrationStep>> = kept
.list
.iter()
.map(|m| {
m.steps
.iter()
.map(|s| MigrationStep {
kind: s.kind,
from: Str::new(&s.from),
to: Str::new(&s.to),
from_guid: s.from_guid.0,
to_guid: s.to_guid.0,
})
.collect()
})
.collect();
let list: Vec<abi::Migration> = kept
.list
.iter()
.zip(&steps)
.map(|(m, steps)| abi::Migration {
id: Str::new(&m.id),
steps: steps.as_ptr(),
count: u32::try_from(steps.len()).unwrap_or(u32::MAX),
})
.collect();
let set = abi::MigrationSet {
header: abi::Header::of::<abi::MigrationSet>(),
migrations: list.as_ptr(),
count: u32::try_from(list.len()).unwrap_or(u32::MAX),
map: Some(run_map),
user: core::ptr::null_mut(),
};
let host = self.host();
let status = (host.api().migrate)(host.handle(), &raw const set);
crate::load::ok(status, (), || {
"migrations: refused; the log says why".to_owned()
})
}
}
#[cfg(test)]
mod tests {
use super::{
KEPT, Kept, Migrations, parse_value, renamed_from, renamed_to, run_map, write_value,
};
use crate::ItemId;
use crate::abi::{Status, Str};
use crate::content::Guid;
fn map(input: &str) -> Str {
let mut out = Str::new("");
let status = unsafe { run_map(core::ptr::null_mut(), 0, 0, Str::new(input), &raw mut out) };
assert_eq!(status, Status::OK);
out
}
#[test]
fn a_map_on_another_thread_leaves_this_threads_text_alone() {
let list = Migrations::new()
.add("1", |m| {
m.map_setting("n", |v| {
Ok(toml::Value::Integer(v.as_integer().unwrap_or(0) + 1))
})
})
.list;
assert!(KEPT.set(Kept { list }).is_ok());
let here = map("1");
let there = std::thread::spawn(|| {
unsafe { map("41").as_str() }.map(str::to_owned)
})
.join()
.unwrap();
assert_eq!(there.as_deref(), Some("42"));
assert_eq!(unsafe { here.as_str() }, Some("2"));
}
#[test]
fn values_round_trip_as_text() {
for text in [
"5",
"true",
"\"text\"",
"[1, 2]",
"1.5",
"{ a = 1 }",
"[{ a = 1 }, { b = \"x\" }]",
] {
let value = parse_value(text).unwrap();
assert_eq!(write_value(&value).unwrap(), text);
}
assert!(parse_value("not toml [").is_err());
}
#[test]
fn renames_follow_their_chain_both_ways() {
let (a, b, c) = (Guid::named("t:a"), Guid::named("t:b"), Guid::named("t:c"));
let list = Migrations::new()
.add("2", |m| m.rename_content(a, b))
.add("3", |m| m.rename_content(b, c))
.list;
assert_eq!(
renamed_from(&list, ItemId::from_guid(a)),
ItemId::from_guid(c)
);
assert_eq!(
renamed_from(&list, ItemId::from_guid(b)),
ItemId::from_guid(c)
);
assert_eq!(
renamed_from(&list, ItemId::from_guid(c)),
ItemId::from_guid(c)
);
assert_eq!(renamed_to(&list, c), [b, a]);
let cycle = Migrations::new()
.add("2", |m| m.rename_content(a, b))
.add("3", |m| m.rename_content(b, a))
.list;
assert_eq!(
renamed_from(&cycle, ItemId::from_guid(a)),
ItemId::from_guid(b)
);
assert_eq!(renamed_to(&cycle, a), [b]);
}
#[test]
fn a_declaration_is_checked() {
assert!(
Migrations::new()
.add("2", |m| m.rename_setting("a", "b"))
.check()
.is_ok()
);
assert!(
Migrations::new()
.add("2", |m| m)
.add("2", |m| m)
.check()
.is_err()
);
assert!(Migrations::new().add("", |m| m).check().is_err());
assert!(
Migrations::new()
.add("2", |m| m.rename_setting("a", ""))
.check()
.is_err()
);
assert!(
Migrations::new()
.add("2", |m| m.take_setting(
"comfort",
"multiplier",
"comfort.multiplier"
))
.check()
.is_ok()
);
for (from, key, to) in [
("Bad Id", "a", "a"),
("comfort", "", "a"),
("comfort", "a", ""),
] {
assert!(
Migrations::new()
.add("2", |m| m.take_setting(from, key, to))
.check()
.is_err(),
"{from}:{key} to {to}"
);
}
let guid = Guid::parse("8a3f6e21-5c4b-4d7e-9f10-2b7c5e6a9d31");
assert!(
Migrations::new()
.add("2", |m| m.rename_content(guid, guid))
.check()
.is_err()
);
}
#[test]
fn a_map_that_fails_to_read_a_value_never_names_it() {
#[derive(Debug, serde::Deserialize)]
#[expect(dead_code, reason = "only read to fail")]
struct Pin {
pin: u32,
}
let error =
super::MigrationError::from(toml::from_str::<Pin>("pin = \"hunter2\"").unwrap_err());
assert_eq!(error.to_string(), "key pin: expected u32");
}
#[test]
fn a_value_that_is_no_toml_says_why_without_itself() {
let why = parse_value("\"hunter2").unwrap_err().to_string();
assert!(
why.starts_with("line 1: ") && !why.contains("hunter2"),
"{why}"
);
assert!(!why.contains("does not fit"), "{why}");
}
}