mirror of
https://github.com/martinvonz/jj.git
synced 2025-02-06 11:34:54 +00:00
config: add migration type that renames and updates value
This will be used in order to migrate boolean value to enum, for example.
This commit is contained in:
parent
9d77ad5594
commit
ffaaf89f05
1 changed files with 126 additions and 2 deletions
|
@ -195,6 +195,15 @@ pub enum ConfigMigrateLayerError {
|
||||||
/// Cannot delete old value or set new value.
|
/// Cannot delete old value or set new value.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Update(#[from] ConfigUpdateError),
|
Update(#[from] ConfigUpdateError),
|
||||||
|
/// Old config value cannot be converted.
|
||||||
|
#[error("Invalid type or value for {name}")]
|
||||||
|
Type {
|
||||||
|
/// Dotted config name path.
|
||||||
|
name: String,
|
||||||
|
/// Source error.
|
||||||
|
#[source]
|
||||||
|
error: DynError,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigMigrateLayerError {
|
impl ConfigMigrateLayerError {
|
||||||
|
@ -206,6 +215,8 @@ impl ConfigMigrateLayerError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DynError = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
|
||||||
/// Rule to migrate deprecated config variables.
|
/// Rule to migrate deprecated config variables.
|
||||||
pub struct ConfigMigrationRule {
|
pub struct ConfigMigrationRule {
|
||||||
inner: MigrationRule,
|
inner: MigrationRule,
|
||||||
|
@ -216,6 +227,12 @@ enum MigrationRule {
|
||||||
old_name: ConfigNamePathBuf,
|
old_name: ConfigNamePathBuf,
|
||||||
new_name: ConfigNamePathBuf,
|
new_name: ConfigNamePathBuf,
|
||||||
},
|
},
|
||||||
|
RenameUpdateValue {
|
||||||
|
old_name: ConfigNamePathBuf,
|
||||||
|
new_name: ConfigNamePathBuf,
|
||||||
|
#[allow(clippy::type_complexity)] // type alias wouldn't help readability
|
||||||
|
new_value_fn: Box<dyn Fn(&ConfigValue) -> Result<ConfigValue, DynError>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigMigrationRule {
|
impl ConfigMigrationRule {
|
||||||
|
@ -228,12 +245,31 @@ impl ConfigMigrationRule {
|
||||||
ConfigMigrationRule { inner }
|
ConfigMigrationRule { inner }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rename + update value, generic Box<dyn Fn>, etc.
|
/// Creates rule that moves value from `old_name` to `new_name`, and updates
|
||||||
|
/// the value.
|
||||||
|
///
|
||||||
|
/// If `new_value_fn(&old_value)` returned an error, the whole migration
|
||||||
|
/// process would fail.
|
||||||
|
pub fn rename_update_value(
|
||||||
|
old_name: impl ToConfigNamePath,
|
||||||
|
new_name: impl ToConfigNamePath,
|
||||||
|
new_value_fn: impl Fn(&ConfigValue) -> Result<ConfigValue, DynError> + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let inner = MigrationRule::RenameUpdateValue {
|
||||||
|
old_name: old_name.into_name_path().into(),
|
||||||
|
new_name: new_name.into_name_path().into(),
|
||||||
|
new_value_fn: Box::new(new_value_fn),
|
||||||
|
};
|
||||||
|
ConfigMigrationRule { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: update value, generic Box<dyn Fn>, etc.
|
||||||
|
|
||||||
/// Returns true if `layer` contains an item to be migrated.
|
/// Returns true if `layer` contains an item to be migrated.
|
||||||
fn matches(&self, layer: &ConfigLayer) -> bool {
|
fn matches(&self, layer: &ConfigLayer) -> bool {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
MigrationRule::RenameValue { old_name, .. } => {
|
MigrationRule::RenameValue { old_name, .. }
|
||||||
|
| MigrationRule::RenameUpdateValue { old_name, .. } => {
|
||||||
matches!(layer.look_up_item(old_name), Ok(Some(_)))
|
matches!(layer.look_up_item(old_name), Ok(Some(_)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,6 +281,11 @@ impl ConfigMigrationRule {
|
||||||
MigrationRule::RenameValue { old_name, new_name } => {
|
MigrationRule::RenameValue { old_name, new_name } => {
|
||||||
rename_value(layer, old_name, new_name)
|
rename_value(layer, old_name, new_name)
|
||||||
}
|
}
|
||||||
|
MigrationRule::RenameUpdateValue {
|
||||||
|
old_name,
|
||||||
|
new_name,
|
||||||
|
new_value_fn,
|
||||||
|
} => rename_update_value(layer, old_name, new_name, new_value_fn),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,6 +303,24 @@ fn rename_value(
|
||||||
Ok(format!("{old_name} is renamed to {new_name}"))
|
Ok(format!("{old_name} is renamed to {new_name}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rename_update_value(
|
||||||
|
layer: &mut ConfigLayer,
|
||||||
|
old_name: &ConfigNamePathBuf,
|
||||||
|
new_name: &ConfigNamePathBuf,
|
||||||
|
new_value_fn: impl FnOnce(&ConfigValue) -> Result<ConfigValue, DynError>,
|
||||||
|
) -> Result<String, ConfigMigrateLayerError> {
|
||||||
|
let old_value = layer.delete_value(old_name)?.expect("tested by matches()");
|
||||||
|
if matches!(layer.look_up_item(new_name), Ok(Some(_))) {
|
||||||
|
return Ok(format!("{old_name} is deleted (superseded by {new_name})"));
|
||||||
|
}
|
||||||
|
let new_value = new_value_fn(&old_value).map_err(|error| ConfigMigrateLayerError::Type {
|
||||||
|
name: old_name.to_string(),
|
||||||
|
error,
|
||||||
|
})?;
|
||||||
|
layer.set_value(new_name, new_value.clone())?;
|
||||||
|
Ok(format!("{old_name} is updated to {new_name} = {new_value}"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Applies migration `rules` to `config`. Returns descriptions of the applied
|
/// Applies migration `rules` to `config`. Returns descriptions of the applied
|
||||||
/// migrations.
|
/// migrations.
|
||||||
pub fn migrate(
|
pub fn migrate(
|
||||||
|
@ -636,4 +695,69 @@ mod tests {
|
||||||
new = 'bar.old #1'
|
new = 'bar.old #1'
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_migrate_rename_update_value() {
|
||||||
|
let mut config = StackedConfig::empty();
|
||||||
|
config.add_layer(new_user_layer(indoc! {"
|
||||||
|
[foo]
|
||||||
|
old = 'foo.old #0'
|
||||||
|
[bar]
|
||||||
|
old = 'bar.old #0'
|
||||||
|
[baz]
|
||||||
|
new = 'baz.new #0'
|
||||||
|
"}));
|
||||||
|
config.add_layer(new_user_layer(indoc! {"
|
||||||
|
[bar]
|
||||||
|
old = 'bar.old #1'
|
||||||
|
"}));
|
||||||
|
|
||||||
|
let rules = [
|
||||||
|
// to array
|
||||||
|
ConfigMigrationRule::rename_update_value("foo.old", "foo.new", |old_value| {
|
||||||
|
let val = old_value.clone().decorated("", "");
|
||||||
|
Ok(ConfigValue::from_iter([val]))
|
||||||
|
}),
|
||||||
|
// update string or error
|
||||||
|
ConfigMigrationRule::rename_update_value("bar.old", "baz.new", |old_value| {
|
||||||
|
let s = old_value.as_str().ok_or("not a string")?;
|
||||||
|
Ok(format!("{s} updated").into())
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let descriptions = migrate(&mut config, &rules).unwrap();
|
||||||
|
insta::assert_debug_snapshot!(descriptions, @r#"
|
||||||
|
[
|
||||||
|
"foo.old is updated to foo.new = ['foo.old #0']",
|
||||||
|
"bar.old is deleted (superseded by baz.new)",
|
||||||
|
"bar.old is updated to baz.new = \"bar.old #1 updated\"",
|
||||||
|
]
|
||||||
|
"#);
|
||||||
|
insta::assert_snapshot!(config.layers()[0].data, @r"
|
||||||
|
[foo]
|
||||||
|
new = ['foo.old #0']
|
||||||
|
[bar]
|
||||||
|
[baz]
|
||||||
|
new = 'baz.new #0'
|
||||||
|
");
|
||||||
|
insta::assert_snapshot!(config.layers()[1].data, @r#"
|
||||||
|
[bar]
|
||||||
|
|
||||||
|
[baz]
|
||||||
|
new = "bar.old #1 updated"
|
||||||
|
"#);
|
||||||
|
|
||||||
|
config.add_layer(new_user_layer(indoc! {"
|
||||||
|
[bar]
|
||||||
|
old = false # not a string
|
||||||
|
"}));
|
||||||
|
insta::assert_debug_snapshot!(migrate(&mut config, &rules).unwrap_err(), @r#"
|
||||||
|
ConfigMigrateError {
|
||||||
|
error: Type {
|
||||||
|
name: "bar.old",
|
||||||
|
error: "not a string",
|
||||||
|
},
|
||||||
|
source_path: None,
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue