use std::collections::HashMap;
use super::FakeHost;
use super::Installed;
use super::table;
use crate::abi::Level;
use crate::abi::Str;
use crate::migrations::MigrationError;
impl FakeHost {
#[must_use]
pub fn other_settings(mut self, id: &str, text: &str) -> Self {
self.other_settings.insert(id.to_owned(), text.to_owned());
self
}
#[must_use]
pub fn settings(mut self, text: &str) -> Self {
self.settings = Some(text.to_owned());
self
}
#[must_use]
pub fn data_dir(mut self, path: &str) -> Self {
path.clone_into(&mut self.data_dir);
self
}
#[must_use]
pub fn log_level(mut self, level: Level) -> Self {
self.log_level = level;
self
}
}
#[expect(
clippy::unused_self,
reason = "the fake's state is process-wide; `self` proves it is installed"
)]
impl Installed {
pub fn set_settings(&self, text: &str) {
table::with((), |s| s.host.settings = Some(text.to_owned()));
}
pub fn set_server_synced(&self, synced: Option<&str>) {
table::with((), |s| s.host.server_synced = synced.map(str::to_owned));
}
#[must_use]
pub fn reported_synced(&self) -> Vec<String> {
table::with(Vec::new(), |s| s.synced.clone())
}
#[must_use]
pub fn settings_defaults(&self) -> Vec<String> {
table::with(Vec::new(), |s| s.defaults.clone())
}
#[must_use]
pub fn config_schema(&self) -> Option<crate::config::Schema> {
table::with(None, |s| s.config_schema.clone())
.and_then(|text| crate::config::Schema::parse(&text).ok())
}
pub fn check_settings(&self, text: &str) -> Result<(), SettingsRefused> {
let Some((callback, user)) = table::with(None, |s| s.config_check) else {
return Ok(());
};
let mut why = Str::EMPTY;
let status = unsafe {
callback(
Str::new(text),
&raw mut why,
core::ptr::with_exposed_provenance_mut(user),
)
};
if status.is_ok() {
return Ok(());
}
let reason = unsafe { why.as_str() }.unwrap_or("refused").to_owned();
Err(SettingsRefused { reason })
}
#[must_use]
pub fn migrations(&self) -> Vec<String> {
table::with(Vec::new(), |s| {
s.migrations
.as_ref()
.map(|m| m.list.iter().map(|(id, _)| id.clone()).collect())
.unwrap_or_default()
})
}
pub fn migrate_settings(&self, old: &str) -> Result<String, MigrationError> {
use crate::abi::MigrationStep as S;
let migrations = table::with(None, |s| s.migrations.clone());
let others = table::with(HashMap::new(), |s| s.host.other_settings.clone());
let mut table: toml::Table = toml::from_str(old)?;
let list = migrations.as_ref().map_or(&[][..], |m| &m.list[..]);
for (index, (_, steps)) in list.iter().enumerate() {
for (step, (kind, from, to)) in steps.iter().enumerate() {
match *kind {
S::TAKE_SETTING => take(&mut table, from, to, &others)?,
S::RENAME_SETTING => {
if crate::config::value_at(&table, to).is_none()
&& let Some(value) = remove_path(&mut table, from)
{
set_path(&mut table, to, value)?;
}
}
S::REMOVE_SETTING => {
let _gone = remove_path(&mut table, from);
}
S::MAP_SETTING => {
let (Some(value), Some(map)) = (table.get(from), migrations.as_ref())
else {
continue;
};
let text = crate::migrations::write_value(value)?;
let new = map
.call(index, step, &text)
.map_err(|e| MigrationError::new(format!("setting {from}: {e}")))?;
table.insert(from.clone(), crate::migrations::parse_value(&new)?);
}
_ => {}
}
}
}
toml::to_string(&table).map_err(|e| MigrationError::new(e.to_string()))
}
pub fn migrate_store(&self, name: &str, old: &str) -> Result<(String, String), MigrationError> {
use crate::abi::MigrationStep as S;
let migrations = table::with(None, |s| s.migrations.clone());
let (mut name, mut text) = (name.to_owned(), old.to_owned());
let list = migrations.as_ref().map_or(&[][..], |m| &m.list[..]);
for (index, (_, steps)) in list.iter().enumerate() {
for (step, (kind, from, to)) in steps.iter().enumerate() {
match *kind {
S::RENAME_STORE if *from == name => name.clone_from(to),
S::MAP_STORE if *from == name => {
let Some(map) = migrations.as_ref() else {
continue;
};
text = map
.call(index, step, &text)
.map_err(|e| MigrationError::new(format!("store {from}: {e}")))?;
}
_ => {}
}
}
}
Ok((name, text))
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{reason}")]
pub struct SettingsRefused {
pub reason: String,
}
pub(crate) type FakeStep = (u32, String, String);
#[derive(Clone, Debug)]
pub(crate) struct FakeMigrations {
pub(crate) list: Vec<(String, Vec<FakeStep>)>,
pub(crate) map: crate::abi::MigrateFn,
pub(crate) user: usize,
}
impl FakeMigrations {
fn call(&self, migration: usize, step: usize, input: &str) -> Result<String, MigrationError> {
let (Ok(migration), Ok(step)) = (u32::try_from(migration), u32::try_from(step)) else {
return Err(MigrationError::new("too many steps"));
};
let mut out = Str::EMPTY;
let status = unsafe {
(self.map)(
core::ptr::with_exposed_provenance_mut(self.user),
migration,
step,
Str::new(input),
&raw mut out,
)
};
let text = unsafe { out.as_str() }.unwrap_or_default().to_owned();
if status.is_ok() {
Ok(text)
} else {
Err(MigrationError::new(text))
}
}
}
fn take(
table: &mut toml::Table,
from: &str,
to: &str,
others: &HashMap<String, String>,
) -> Result<(), MigrationError> {
let (id, key) = from
.split_once(':')
.ok_or_else(|| MigrationError::new(format!("setting {from}: not <mod id>:<key>")))?;
let Some(text) = others.get(id) else {
return Ok(());
};
let other: toml::Table = toml::from_str(text)?;
let mut parts = key.split('.');
let mut value = parts.next().and_then(|first| other.get(first));
for part in parts {
value = value.and_then(|v| v.get(part));
}
let Some(value) = value.cloned() else {
return Ok(());
};
set_path(table, to, value)
}
fn set_path(table: &mut toml::Table, key: &str, value: toml::Value) -> Result<(), MigrationError> {
let (path, leaf) = key.rsplit_once('.').map_or(("", key), |(p, l)| (p, l));
let mut at = table;
for part in path.split('.').filter(|p| !p.is_empty()) {
at = at
.entry(part)
.or_insert_with(|| toml::Value::Table(toml::Table::new()))
.as_table_mut()
.ok_or_else(|| MigrationError::new(format!("setting {key}: {part} is not a table")))?;
}
at.insert(leaf.to_owned(), value);
Ok(())
}
fn remove_path(table: &mut toml::Table, key: &str) -> Option<toml::Value> {
let (path, leaf) = key.rsplit_once('.').map_or(("", key), |(p, l)| (p, l));
let mut at = table;
for part in path.split('.').filter(|p| !p.is_empty()) {
at = at.get_mut(part)?.as_table_mut()?;
}
at.remove(leaf)
}