use serde_json::Value;
use crate::error::ConfigError;
use crate::path::get_value_at_path;
use crate::report::{AppliedMigration, ConfigReport};
use super::canonical::canonicalize_runtime_path;
use super::de::insert_path;
use super::migration::{ConfigMigration, ConfigMigrationKind};
use super::path::try_normalize_external_path;
pub(super) fn normalize_version_registration_path(path: &str) -> Result<String, ConfigError> {
let normalized =
try_normalize_external_path(path).map_err(|message| ConfigError::MetadataInvalid {
path: path.to_owned(),
message: format!("invalid configuration version path: {message}"),
})?;
if normalized.is_empty() {
return Err(ConfigError::MetadataInvalid {
path: path.to_owned(),
message: "configuration version path cannot be empty".to_owned(),
});
}
Ok(normalized)
}
pub(super) fn validate_config_migrations(
migrations: &[ConfigMigration],
) -> Result<(), ConfigError> {
for migration in migrations {
match &migration.kind {
ConfigMigrationKind::Rename { from, to } => {
let _ = normalize_migration_registration_path(from)?;
let _ = normalize_migration_registration_path(to)?;
}
ConfigMigrationKind::Remove { path } => {
let _ = normalize_migration_registration_path(path)?;
}
}
}
Ok(())
}
pub(super) fn apply_config_migrations(
merged: &mut Value,
version_path: &str,
current_version: u32,
migrations: &[ConfigMigration],
report: &mut ConfigReport,
) -> Result<(), ConfigError> {
let version_path = canonicalize_runtime_path(merged, version_path);
let mut working_version = read_config_version(merged, &version_path)?;
if working_version > current_version {
return Err(ConfigError::UnsupportedConfigVersion {
path: version_path,
found: working_version,
supported: current_version,
});
}
let mut sorted = migrations.to_vec();
sorted.sort_by_key(|migration| migration.since_version);
for migration in sorted {
if migration.since_version <= working_version || migration.since_version > current_version {
continue;
}
match &migration.kind {
ConfigMigrationKind::Rename { from, to } => {
let from = canonicalize_runtime_path(
merged,
&normalize_migration_registration_path(from)?,
);
let to =
canonicalize_runtime_path(merged, &normalize_migration_registration_path(to)?);
if let Some(value) = take_value_at_path(merged, &from) {
insert_normalized_path(merged, &to, value).map_err(|message| {
ConfigError::MetadataInvalid {
path: to.clone(),
message: format!("failed to apply migration: {message}"),
}
})?;
report.record_migration(AppliedMigration {
kind: "rename".to_owned(),
from_version: working_version,
to_version: migration.since_version,
from_path: from,
to_path: Some(to),
note: migration.note.clone(),
});
}
}
ConfigMigrationKind::Remove { path } => {
let path = canonicalize_runtime_path(
merged,
&normalize_migration_registration_path(path)?,
);
if take_value_at_path(merged, &path).is_some() {
report.record_migration(AppliedMigration {
kind: "remove".to_owned(),
from_version: working_version,
to_version: migration.since_version,
from_path: path,
to_path: None,
note: migration.note.clone(),
});
}
}
}
working_version = migration.since_version;
}
insert_normalized_path(
merged,
&version_path,
Value::Number(serde_json::Number::from(current_version)),
)
.map_err(|message| ConfigError::InvalidConfigVersion {
path: version_path,
message,
})?;
Ok(())
}
fn normalize_migration_registration_path(path: &str) -> Result<String, ConfigError> {
let normalized =
try_normalize_external_path(path).map_err(|message| ConfigError::MetadataInvalid {
path: path.to_owned(),
message: format!("invalid migration path: {message}"),
})?;
if normalized.is_empty() {
return Err(ConfigError::MetadataInvalid {
path: path.to_owned(),
message: "migration paths cannot target the configuration root".to_owned(),
});
}
Ok(normalized)
}
fn read_config_version(value: &Value, path: &str) -> Result<u32, ConfigError> {
let Some(found) = get_value_at_path(value, path) else {
return Ok(0);
};
let Some(version) = found.as_u64() else {
return Err(ConfigError::InvalidConfigVersion {
path: path.to_owned(),
message: "expected an unsigned integer".to_owned(),
});
};
u32::try_from(version).map_err(|_| ConfigError::InvalidConfigVersion {
path: path.to_owned(),
message: "version must fit in u32".to_owned(),
})
}
fn insert_normalized_path(root: &mut Value, path: &str, value: Value) -> Result<(), String> {
let segments = path.split('.').collect::<Vec<_>>();
insert_path(root, &segments, value)
}
fn take_value_at_path(root: &mut Value, path: &str) -> Option<Value> {
let segments = path
.split('.')
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>();
take_value_at_segments(root, &segments)
}
fn take_value_at_segments(current: &mut Value, segments: &[&str]) -> Option<Value> {
let segment = segments.first()?;
if segments.len() == 1 {
return match current {
Value::Object(map) => map.remove(*segment),
Value::Array(values) => {
let index = segment.parse::<usize>().ok()?;
(index < values.len()).then(|| values.remove(index))
}
_ => None,
};
}
match current {
Value::Object(map) => {
let child = map.get_mut(*segment)?;
take_value_at_segments(child, &segments[1..])
}
Value::Array(values) => {
let index = segment.parse::<usize>().ok()?;
let child = values.get_mut(index)?;
take_value_at_segments(child, &segments[1..])
}
_ => None,
}
}