Skip to content

Commit

Permalink
config: allow inline table syntax in mutation and conditional scope API
Browse files Browse the repository at this point in the history
This is a middle ground. An inline table can still be overwritten or deleted
because it's syntactically a value. It would be annoying if "jj config set
colors.<name> '{ .. }'" couldn't be executed more than once because the existing
item was a table.

#5255
  • Loading branch information
yuja committed Jan 10, 2025
1 parent 1b07df1 commit b97b738
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 27 deletions.
13 changes: 8 additions & 5 deletions cli/tests/test_config_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,20 +691,23 @@ fn test_config_unset_non_existent_key() {

#[test]
fn test_config_unset_inline_table_key() {
let test_env = TestEnvironment::default();
let mut test_env = TestEnvironment::default();
// Test with fresh new config file
let user_config_path = test_env.config_path().join("config.toml");
test_env.set_config_path(&user_config_path);
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
let repo_path = test_env.env_root().join("repo");

test_env.jj_cmd_ok(
&repo_path,
&["config", "set", "--user", "inline-table", "{ foo = true }"],
);
let stderr = test_env.jj_cmd_failure(
test_env.jj_cmd_ok(
&repo_path,
&["config", "unset", "--user", "inline-table.foo"],
);

insta::assert_snapshot!(stderr, @r#"Error: "inline-table.foo" doesn't exist"#);
let user_config_toml = std::fs::read_to_string(&user_config_path).unwrap();
insta::assert_snapshot!(user_config_toml, @"inline-table = {}");
}

#[test]
Expand All @@ -724,7 +727,7 @@ fn test_config_unset_table_like() {
)
.unwrap();

// Inline table is a "value", so it can be deleted.
// Inline table is syntactically a "value", so it can be deleted.
test_env.jj_cmd_success(
test_env.env_root(),
&["config", "unset", "--user", "inline-table"],
Expand Down
51 changes: 36 additions & 15 deletions lib/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,14 @@ pub enum ConfigUpdateError {
/// Dotted config name path.
name: String,
},
/// Table exists at the path, which shouldn't be overwritten by a value.
/// Non-inline table exists at the path, which shouldn't be overwritten by a
/// value.
#[error("Would overwrite entire table {name}")]
WouldOverwriteTable {
/// Dotted config name path.
name: String,
},
/// Table exists at the path, which shouldn't be deleted.
/// Non-inline table exists at the path, which shouldn't be deleted.
#[error("Would delete entire table {name}")]
WouldDeleteTable {
/// Dotted config name path.
Expand Down Expand Up @@ -397,7 +398,8 @@ impl ConfigLayer {
/// Sets `new_value` to the `name` path. Returns old value if any.
///
/// This function errors out if attempted to overwrite a non-table middle
/// node or a leaf table (in the same way as file/directory operation.)
/// node or a leaf non-inline table. An inline table can be overwritten
/// because it's syntactically a value.
pub fn set_value(
&mut self,
name: impl ToConfigNamePath,
Expand All @@ -411,7 +413,6 @@ impl ConfigLayer {
})?;
match parent_table.entry_format(leaf_key) {
toml_edit::Entry::Occupied(mut entry) => {
// TODO: Don't overwrite inline table because it is a merge-able item
if !entry.get().is_value() {
return Err(ConfigUpdateError::WouldOverwriteTable {
name: name.to_string(),
Expand All @@ -433,7 +434,8 @@ impl ConfigLayer {
/// Deletes value specified by the `name` path. Returns old value if any.
///
/// Returns `Ok(None)` if middle node wasn't a table or a value wasn't
/// found. Returns `Err` if attempted to delete a table.
/// found. Returns `Err` if attempted to delete a non-inline table. An
/// inline table can be deleted because it's syntactically a value.
pub fn delete_value(
&mut self,
name: impl ToConfigNamePath,
Expand All @@ -445,16 +447,14 @@ impl ConfigLayer {
let leaf_key = keys
.next_back()
.ok_or_else(|| would_delete_table(name.to_string()))?;
let root_table = self.data.as_table_mut();
let Some(parent_table) =
// TODO: as_table_like_mut()
keys.try_fold(root_table, |table, key| table.get_mut(key)?.as_table_mut())
else {
let Some(parent_table) = keys.try_fold(
self.data.as_table_mut() as &mut ConfigTableLike,
|table, key| table.get_mut(key)?.as_table_like_mut(),
) else {
return Ok(None);
};
match parent_table.entry(leaf_key) {
toml_edit::Entry::Occupied(entry) => {
// TODO: Don't delete inline table because it is a merge-able item
if !entry.get().is_value() {
return Err(would_delete_table(name.to_string()));
}
Expand Down Expand Up @@ -488,15 +488,14 @@ fn look_up_item<'a>(
/// Inserts tables down to the parent of the `name` path. Returns `Err(keys)` if
/// middle node exists at the prefix name `keys` and wasn't a table.
fn ensure_parent_table<'a, 'b>(
root_table: &'a mut ConfigTable,
root_table: &'a mut ConfigTableLike<'a>,
name: &'b ConfigNamePathBuf,
) -> Result<(&'a mut ConfigTable, &'b toml_edit::Key), &'b [toml_edit::Key]> {
) -> Result<(&'a mut ConfigTableLike<'a>, &'b toml_edit::Key), &'b [toml_edit::Key]> {
let mut keys = name.components();
let leaf_key = keys.next_back().ok_or(&name.0[..])?;
let parent_table = keys.enumerate().try_fold(root_table, |table, (i, key)| {
let sub_item = table.entry_format(key).or_insert_with(new_implicit_table);
// TODO: as_table_like_mut()
sub_item.as_table_mut().ok_or(&name.0[..=i])
sub_item.as_table_like_mut().ok_or(&name.0[..=i])
})?;
Ok((parent_table, leaf_key))
}
Expand Down Expand Up @@ -884,11 +883,15 @@ mod tests {
layer
.set_value("bar.qux", ConfigValue::from_iter([("inline", "table")]))
.unwrap();
layer
.set_value("bar.to-update", ConfigValue::from_iter([("some", true)]))
.unwrap();
insta::assert_snapshot!(layer.data, @r#"
foo = 1
[bar]
qux = { inline = "table" }
to-update = { some = true }
[bar.baz]
blah = "2"
Expand All @@ -900,6 +903,13 @@ mod tests {
.unwrap();
// Can overwrite inline table
layer.set_value("bar.qux", "new bar.qux").unwrap();
// Can add value to inline table
layer
.set_value(
"bar.to-update.new",
ConfigValue::from_iter([("table", "value")]),
)
.unwrap();
// Cannot overwrite table
assert_matches!(
layer.set_value("bar", 0),
Expand All @@ -915,6 +925,7 @@ mod tests {
[bar]
qux = "new bar.qux"
to-update = { some = true, new = { table = "value" } }
[bar.baz]
blah = "2"
Expand Down Expand Up @@ -961,11 +972,15 @@ mod tests {
layer
.set_value("bar.qux", ConfigValue::from_iter([("inline", "table")]))
.unwrap();
layer
.set_value("bar.to-update", ConfigValue::from_iter([("some", true)]))
.unwrap();
insta::assert_snapshot!(layer.data, @r#"
foo = 1
[bar]
qux = { inline = "table" }
to-update = { some = true }
[bar.baz]
blah = "2"
Expand All @@ -977,6 +992,9 @@ mod tests {
// Can delete inline table
let old_value = layer.delete_value("bar.qux").unwrap();
assert!(old_value.is_some_and(|v| v.is_inline_table()));
// Can delete inner value from inline table
let old_value = layer.delete_value("bar.to-update.some").unwrap();
assert_eq!(old_value.and_then(|v| v.as_bool()), Some(true));
// Cannot delete table
assert_matches!(
layer.delete_value("bar"),
Expand All @@ -986,6 +1004,9 @@ mod tests {
// exist
assert_matches!(layer.delete_value("bar.baz.blah.blah"), Ok(None));
insta::assert_snapshot!(layer.data, @r#"
[bar]
to-update = {}
[bar.baz]
blah = "2"
"#);
Expand Down
10 changes: 3 additions & 7 deletions lib/src/config_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ use serde::Deserialize as _;
use toml_edit::DocumentMut;

use crate::config::ConfigGetError;
use crate::config::ConfigItem;
use crate::config::ConfigLayer;
use crate::config::ConfigNamePathBuf;
use crate::config::ConfigUpdateError;
Expand Down Expand Up @@ -170,15 +169,12 @@ fn pop_scope_tables(layer: &mut ConfigLayer) -> Result<toml_edit::ArrayOfTables,
let Some(item) = layer.data.remove(SCOPE_TABLE_KEY) else {
return Ok(toml_edit::ArrayOfTables::new());
};
// TODO: item.into_array_of_tables()
match item {
ConfigItem::ArrayOfTables(tables) => Ok(tables),
_ => Err(ConfigGetError::Type {
item.into_array_of_tables()
.map_err(|item| ConfigGetError::Type {
name: SCOPE_TABLE_KEY.to_owned(),
error: format!("Expected an array of tables, but is {}", item.type_name()).into(),
source_path: layer.path.clone(),
}),
}
})
}

/// Rule to migrate deprecated config variables.
Expand Down

0 comments on commit b97b738

Please sign in to comment.