just playing with tangled
at ig/vimdiffwarn 1344 lines 46 kB view raw
1// Copyright 2022 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15//! Configuration store helpers. 16 17use std::borrow::Borrow; 18use std::convert::Infallible; 19use std::fmt; 20use std::fmt::Display; 21use std::fs; 22use std::io; 23use std::ops::Range; 24use std::path::Path; 25use std::path::PathBuf; 26use std::slice; 27use std::str::FromStr; 28use std::sync::Arc; 29 30use itertools::Itertools as _; 31use once_cell::sync::Lazy; 32use serde::de::IntoDeserializer as _; 33use serde::Deserialize; 34use thiserror::Error; 35use toml_edit::DocumentMut; 36use toml_edit::ImDocument; 37 38pub use crate::config_resolver::migrate; 39pub use crate::config_resolver::resolve; 40pub use crate::config_resolver::ConfigMigrateError; 41pub use crate::config_resolver::ConfigMigrateLayerError; 42pub use crate::config_resolver::ConfigMigrationRule; 43pub use crate::config_resolver::ConfigResolutionContext; 44use crate::file_util::IoResultExt as _; 45use crate::file_util::PathError; 46 47/// Config value or table node. 48pub type ConfigItem = toml_edit::Item; 49/// Non-inline table of config key and value pairs. 50pub type ConfigTable = toml_edit::Table; 51/// Non-inline or inline table of config key and value pairs. 52pub type ConfigTableLike<'a> = dyn toml_edit::TableLike + 'a; 53/// Generic config value. 54pub type ConfigValue = toml_edit::Value; 55 56/// Error that can occur when parsing or loading config variables. 57#[derive(Debug, Error)] 58pub enum ConfigLoadError { 59 /// Config file or directory cannot be read. 60 #[error("Failed to read configuration file")] 61 Read(#[source] PathError), 62 /// TOML file or text cannot be parsed. 63 #[error("Configuration cannot be parsed as TOML document")] 64 Parse { 65 /// Source error. 66 #[source] 67 error: toml_edit::TomlError, 68 /// Source file path. 69 source_path: Option<PathBuf>, 70 }, 71} 72 73/// Error that can occur when saving config variables to file. 74#[derive(Debug, Error)] 75#[error("Failed to write configuration file")] 76pub struct ConfigFileSaveError(#[source] pub PathError); 77 78/// Error that can occur when looking up config variable. 79#[derive(Debug, Error)] 80pub enum ConfigGetError { 81 /// Config value is not set. 82 #[error("Value not found for {name}")] 83 NotFound { 84 /// Dotted config name path. 85 name: String, 86 }, 87 /// Config value cannot be converted to the expected type. 88 #[error("Invalid type or value for {name}")] 89 Type { 90 /// Dotted config name path. 91 name: String, 92 /// Source error. 93 #[source] 94 error: Box<dyn std::error::Error + Send + Sync>, 95 /// Source file path where the value is defined. 96 source_path: Option<PathBuf>, 97 }, 98} 99 100/// Error that can occur when updating config variable. 101#[derive(Debug, Error)] 102pub enum ConfigUpdateError { 103 /// Non-table value exists at parent path, which shouldn't be removed. 104 #[error("Would overwrite non-table value with parent table {name}")] 105 WouldOverwriteValue { 106 /// Dotted config name path. 107 name: String, 108 }, 109 /// Non-inline table exists at the path, which shouldn't be overwritten by a 110 /// value. 111 #[error("Would overwrite entire table {name}")] 112 WouldOverwriteTable { 113 /// Dotted config name path. 114 name: String, 115 }, 116 /// Non-inline table exists at the path, which shouldn't be deleted. 117 #[error("Would delete entire table {name}")] 118 WouldDeleteTable { 119 /// Dotted config name path. 120 name: String, 121 }, 122} 123 124/// Extension methods for `Result<T, ConfigGetError>`. 125pub trait ConfigGetResultExt<T> { 126 /// Converts `NotFound` error to `Ok(None)`, leaving other errors. 127 fn optional(self) -> Result<Option<T>, ConfigGetError>; 128} 129 130impl<T> ConfigGetResultExt<T> for Result<T, ConfigGetError> { 131 fn optional(self) -> Result<Option<T>, ConfigGetError> { 132 match self { 133 Ok(value) => Ok(Some(value)), 134 Err(ConfigGetError::NotFound { .. }) => Ok(None), 135 Err(err) => Err(err), 136 } 137 } 138} 139 140/// Dotted config name path. 141#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 142pub struct ConfigNamePathBuf(Vec<toml_edit::Key>); 143 144impl ConfigNamePathBuf { 145 /// Creates an empty path pointing to the root table. 146 /// 147 /// This isn't a valid TOML key expression, but provided for convenience. 148 pub fn root() -> Self { 149 ConfigNamePathBuf(vec![]) 150 } 151 152 /// Returns true if the path is empty (i.e. pointing to the root table.) 153 pub fn is_root(&self) -> bool { 154 self.0.is_empty() 155 } 156 157 /// Returns true if the `base` is a prefix of this path. 158 pub fn starts_with(&self, base: impl AsRef<[toml_edit::Key]>) -> bool { 159 self.0.starts_with(base.as_ref()) 160 } 161 162 /// Returns iterator of path components (or keys.) 163 pub fn components(&self) -> slice::Iter<'_, toml_edit::Key> { 164 self.0.iter() 165 } 166 167 /// Appends the given `key` component. 168 pub fn push(&mut self, key: impl Into<toml_edit::Key>) { 169 self.0.push(key.into()); 170 } 171} 172 173// Help obtain owned value from ToConfigNamePath::Output. If we add a slice 174// type (like &Path for PathBuf), this will be From<&ConfigNamePath>. 175impl From<&ConfigNamePathBuf> for ConfigNamePathBuf { 176 fn from(value: &ConfigNamePathBuf) -> Self { 177 value.clone() 178 } 179} 180 181impl<K: Into<toml_edit::Key>> FromIterator<K> for ConfigNamePathBuf { 182 fn from_iter<I: IntoIterator<Item = K>>(iter: I) -> Self { 183 let keys = iter.into_iter().map(|k| k.into()).collect(); 184 ConfigNamePathBuf(keys) 185 } 186} 187 188impl FromStr for ConfigNamePathBuf { 189 type Err = toml_edit::TomlError; 190 191 fn from_str(s: &str) -> Result<Self, Self::Err> { 192 // TOML parser ensures that the returned vec is not empty. 193 toml_edit::Key::parse(s).map(ConfigNamePathBuf) 194 } 195} 196 197impl AsRef<[toml_edit::Key]> for ConfigNamePathBuf { 198 fn as_ref(&self) -> &[toml_edit::Key] { 199 &self.0 200 } 201} 202 203impl Display for ConfigNamePathBuf { 204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 205 let mut components = self.0.iter().fuse(); 206 if let Some(key) = components.next() { 207 write!(f, "{key}")?; 208 } 209 components.try_for_each(|key| write!(f, ".{key}")) 210 } 211} 212 213/// Value that can be converted to a dotted config name path. 214/// 215/// This is an abstraction to specify a config name path in either a string or a 216/// parsed form. It's similar to `Into<T>`, but the output type `T` is 217/// constrained by the source type. 218pub trait ToConfigNamePath: Sized { 219 /// Path type to be converted from `Self`. 220 type Output: Borrow<ConfigNamePathBuf> + Into<ConfigNamePathBuf>; 221 222 /// Converts this object into a dotted config name path. 223 fn into_name_path(self) -> Self::Output; 224} 225 226impl ToConfigNamePath for ConfigNamePathBuf { 227 type Output = Self; 228 229 fn into_name_path(self) -> Self::Output { 230 self 231 } 232} 233 234impl ToConfigNamePath for &ConfigNamePathBuf { 235 type Output = Self; 236 237 fn into_name_path(self) -> Self::Output { 238 self 239 } 240} 241 242impl ToConfigNamePath for &'static str { 243 // This can be changed to ConfigNamePathStr(str) if allocation cost matters. 244 type Output = ConfigNamePathBuf; 245 246 /// Parses this string into a dotted config name path. 247 /// 248 /// The string must be a valid TOML dotted key. A static str is required to 249 /// prevent API misuse. 250 fn into_name_path(self) -> Self::Output { 251 self.parse() 252 .expect("valid TOML dotted key must be provided") 253 } 254} 255 256impl<const N: usize> ToConfigNamePath for [&str; N] { 257 type Output = ConfigNamePathBuf; 258 259 fn into_name_path(self) -> Self::Output { 260 self.into_iter().collect() 261 } 262} 263 264impl<const N: usize> ToConfigNamePath for &[&str; N] { 265 type Output = ConfigNamePathBuf; 266 267 fn into_name_path(self) -> Self::Output { 268 self.as_slice().into_name_path() 269 } 270} 271 272impl ToConfigNamePath for &[&str] { 273 type Output = ConfigNamePathBuf; 274 275 fn into_name_path(self) -> Self::Output { 276 self.iter().copied().collect() 277 } 278} 279 280/// Source of configuration variables in order of precedence. 281#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 282pub enum ConfigSource { 283 /// Default values (which has the lowest precedence.) 284 Default, 285 /// Base environment variables. 286 EnvBase, 287 /// User configuration files. 288 User, 289 /// Repo configuration files. 290 Repo, 291 /// Override environment variables. 292 EnvOverrides, 293 /// Command-line arguments (which has the highest precedence.) 294 CommandArg, 295} 296 297impl Display for ConfigSource { 298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 299 use ConfigSource::*; 300 let c = match self { 301 Default => "default", 302 User => "user", 303 Repo => "repo", 304 CommandArg => "cli", 305 EnvBase | EnvOverrides => "env", 306 }; 307 write!(f, "{c}") 308 } 309} 310 311/// Set of configuration variables with source information. 312#[derive(Clone, Debug)] 313pub struct ConfigLayer { 314 /// Source type of this layer. 315 pub source: ConfigSource, 316 /// Source file path of this layer if any. 317 pub path: Option<PathBuf>, 318 /// Configuration variables. 319 pub data: DocumentMut, 320} 321 322impl ConfigLayer { 323 /// Creates new layer with empty data. 324 pub fn empty(source: ConfigSource) -> Self { 325 Self::with_data(source, DocumentMut::new()) 326 } 327 328 /// Creates new layer with the configuration variables `data`. 329 pub fn with_data(source: ConfigSource, data: DocumentMut) -> Self { 330 ConfigLayer { 331 source, 332 path: None, 333 data, 334 } 335 } 336 337 /// Parses TOML document `text` into new layer. 338 pub fn parse(source: ConfigSource, text: &str) -> Result<Self, ConfigLoadError> { 339 let data = ImDocument::parse(text).map_err(|error| ConfigLoadError::Parse { 340 error, 341 source_path: None, 342 })?; 343 Ok(Self::with_data(source, data.into_mut())) 344 } 345 346 /// Loads TOML file from the specified `path`. 347 pub fn load_from_file(source: ConfigSource, path: PathBuf) -> Result<Self, ConfigLoadError> { 348 let text = fs::read_to_string(&path) 349 .context(&path) 350 .map_err(ConfigLoadError::Read)?; 351 let data = ImDocument::parse(text).map_err(|error| ConfigLoadError::Parse { 352 error, 353 source_path: Some(path.clone()), 354 })?; 355 Ok(ConfigLayer { 356 source, 357 path: Some(path), 358 data: data.into_mut(), 359 }) 360 } 361 362 fn load_from_dir(source: ConfigSource, path: &Path) -> Result<Vec<Self>, ConfigLoadError> { 363 // TODO: Walk the directory recursively? 364 let mut file_paths: Vec<_> = path 365 .read_dir() 366 .and_then(|dir_entries| { 367 dir_entries 368 .map(|entry| Ok(entry?.path())) 369 .filter_ok(|path| path.is_file() && path.extension() == Some("toml".as_ref())) 370 .try_collect() 371 }) 372 .context(path) 373 .map_err(ConfigLoadError::Read)?; 374 file_paths.sort_unstable(); 375 file_paths 376 .into_iter() 377 .map(|path| Self::load_from_file(source, path)) 378 .try_collect() 379 } 380 381 /// Returns true if the table has no configuration variables. 382 pub fn is_empty(&self) -> bool { 383 self.data.is_empty() 384 } 385 386 // Add .get_value(name) if needed. look_up_*() are low-level API. 387 388 /// Looks up sub table by the `name` path. Returns `Some(table)` if a table 389 /// was found at the path. Returns `Err(item)` if middle or leaf node wasn't 390 /// a table. 391 pub fn look_up_table( 392 &self, 393 name: impl ToConfigNamePath, 394 ) -> Result<Option<&ConfigTableLike>, &ConfigItem> { 395 match self.look_up_item(name) { 396 Ok(Some(item)) => match item.as_table_like() { 397 Some(table) => Ok(Some(table)), 398 None => Err(item), 399 }, 400 Ok(None) => Ok(None), 401 Err(item) => Err(item), 402 } 403 } 404 405 /// Looks up item by the `name` path. Returns `Some(item)` if an item 406 /// found at the path. Returns `Err(item)` if middle node wasn't a table. 407 pub fn look_up_item( 408 &self, 409 name: impl ToConfigNamePath, 410 ) -> Result<Option<&ConfigItem>, &ConfigItem> { 411 look_up_item(self.data.as_item(), name.into_name_path().borrow()) 412 } 413 414 /// Sets `new_value` to the `name` path. Returns old value if any. 415 /// 416 /// This function errors out if attempted to overwrite a non-table middle 417 /// node or a leaf non-inline table. An inline table can be overwritten 418 /// because it's syntactically a value. 419 pub fn set_value( 420 &mut self, 421 name: impl ToConfigNamePath, 422 new_value: impl Into<ConfigValue>, 423 ) -> Result<Option<ConfigValue>, ConfigUpdateError> { 424 let would_overwrite_table = |name| ConfigUpdateError::WouldOverwriteValue { name }; 425 let name = name.into_name_path(); 426 let name = name.borrow(); 427 let (leaf_key, table_keys) = name 428 .0 429 .split_last() 430 .ok_or_else(|| would_overwrite_table(name.to_string()))?; 431 let parent_table = ensure_table(self.data.as_table_mut(), table_keys) 432 .map_err(|keys| would_overwrite_table(keys.join(".")))?; 433 match parent_table.entry_format(leaf_key) { 434 toml_edit::Entry::Occupied(mut entry) => { 435 if !entry.get().is_value() { 436 return Err(ConfigUpdateError::WouldOverwriteTable { 437 name: name.to_string(), 438 }); 439 } 440 let old_item = entry.insert(toml_edit::value(new_value)); 441 Ok(Some(old_item.into_value().unwrap())) 442 } 443 toml_edit::Entry::Vacant(entry) => { 444 entry.insert(toml_edit::value(new_value)); 445 // Reset whitespace formatting (i.e. insert space before '=') 446 let mut new_key = parent_table.key_mut(leaf_key).unwrap(); 447 new_key.leaf_decor_mut().clear(); 448 Ok(None) 449 } 450 } 451 } 452 453 /// Deletes value specified by the `name` path. Returns old value if any. 454 /// 455 /// Returns `Ok(None)` if middle node wasn't a table or a value wasn't 456 /// found. Returns `Err` if attempted to delete a non-inline table. An 457 /// inline table can be deleted because it's syntactically a value. 458 pub fn delete_value( 459 &mut self, 460 name: impl ToConfigNamePath, 461 ) -> Result<Option<ConfigValue>, ConfigUpdateError> { 462 let would_delete_table = |name| ConfigUpdateError::WouldDeleteTable { name }; 463 let name = name.into_name_path(); 464 let name = name.borrow(); 465 let mut keys = name.components(); 466 let leaf_key = keys 467 .next_back() 468 .ok_or_else(|| would_delete_table(name.to_string()))?; 469 let Some(parent_table) = keys.try_fold( 470 self.data.as_table_mut() as &mut ConfigTableLike, 471 |table, key| table.get_mut(key)?.as_table_like_mut(), 472 ) else { 473 return Ok(None); 474 }; 475 match parent_table.entry(leaf_key) { 476 toml_edit::Entry::Occupied(entry) => { 477 if !entry.get().is_value() { 478 return Err(would_delete_table(name.to_string())); 479 } 480 let old_item = entry.remove(); 481 Ok(Some(old_item.into_value().unwrap())) 482 } 483 toml_edit::Entry::Vacant(_) => Ok(None), 484 } 485 } 486 487 /// Inserts tables down to the `name` path. Returns mutable reference to the 488 /// leaf table. 489 /// 490 /// This function errors out if attempted to overwrite a non-table node. In 491 /// file-system analogy, this is equivalent to `std::fs::create_dir_all()`. 492 pub fn ensure_table( 493 &mut self, 494 name: impl ToConfigNamePath, 495 ) -> Result<&mut ConfigTableLike, ConfigUpdateError> { 496 let would_overwrite_table = |name| ConfigUpdateError::WouldOverwriteValue { name }; 497 let name = name.into_name_path(); 498 let name = name.borrow(); 499 ensure_table(self.data.as_table_mut(), &name.0) 500 .map_err(|keys| would_overwrite_table(keys.join("."))) 501 } 502} 503 504/// Looks up item from the `root_item`. Returns `Some(item)` if an item found at 505/// the path. Returns `Err(item)` if middle node wasn't a table. 506fn look_up_item<'a>( 507 root_item: &'a ConfigItem, 508 name: &ConfigNamePathBuf, 509) -> Result<Option<&'a ConfigItem>, &'a ConfigItem> { 510 let mut cur_item = root_item; 511 for key in name.components() { 512 let Some(table) = cur_item.as_table_like() else { 513 return Err(cur_item); 514 }; 515 cur_item = match table.get(key) { 516 Some(item) => item, 517 None => return Ok(None), 518 }; 519 } 520 Ok(Some(cur_item)) 521} 522 523/// Inserts tables recursively. Returns `Err(keys)` if middle node exists at the 524/// prefix name `keys` and wasn't a table. 525fn ensure_table<'a, 'b>( 526 root_table: &'a mut ConfigTableLike<'a>, 527 keys: &'b [toml_edit::Key], 528) -> Result<&'a mut ConfigTableLike<'a>, &'b [toml_edit::Key]> { 529 keys.iter() 530 .enumerate() 531 .try_fold(root_table, |table, (i, key)| { 532 let sub_item = table.entry_format(key).or_insert_with(new_implicit_table); 533 sub_item.as_table_like_mut().ok_or(&keys[..=i]) 534 }) 535} 536 537fn new_implicit_table() -> ConfigItem { 538 let mut table = ConfigTable::new(); 539 table.set_implicit(true); 540 ConfigItem::Table(table) 541} 542 543/// Wrapper for file-based [`ConfigLayer`], providing convenient methods for 544/// modification. 545#[derive(Clone, Debug)] 546pub struct ConfigFile { 547 layer: Arc<ConfigLayer>, 548} 549 550impl ConfigFile { 551 /// Loads TOML file from the specified `path` if exists. Returns an empty 552 /// object if the file doesn't exist. 553 pub fn load_or_empty( 554 source: ConfigSource, 555 path: impl Into<PathBuf>, 556 ) -> Result<Self, ConfigLoadError> { 557 let layer = match ConfigLayer::load_from_file(source, path.into()) { 558 Ok(layer) => Arc::new(layer), 559 Err(ConfigLoadError::Read(PathError { path, error })) 560 if error.kind() == io::ErrorKind::NotFound => 561 { 562 Arc::new(ConfigLayer { 563 source, 564 path: Some(path), 565 data: DocumentMut::new(), 566 }) 567 } 568 Err(err) => return Err(err), 569 }; 570 Ok(ConfigFile { layer }) 571 } 572 573 /// Wraps file-based [`ConfigLayer`] for modification. Returns `Err(layer)` 574 /// if the source `path` is unknown. 575 pub fn from_layer(layer: Arc<ConfigLayer>) -> Result<Self, Arc<ConfigLayer>> { 576 if layer.path.is_some() { 577 Ok(ConfigFile { layer }) 578 } else { 579 Err(layer) 580 } 581 } 582 583 /// Writes serialized data to the source file. 584 pub fn save(&self) -> Result<(), ConfigFileSaveError> { 585 fs::write(self.path(), self.layer.data.to_string()) 586 .context(self.path()) 587 .map_err(ConfigFileSaveError) 588 } 589 590 /// Source file path. 591 pub fn path(&self) -> &Path { 592 self.layer.path.as_ref().expect("path must be known") 593 } 594 595 /// Returns the underlying config layer. 596 pub fn layer(&self) -> &Arc<ConfigLayer> { 597 &self.layer 598 } 599 600 /// See [`ConfigLayer::set_value()`]. 601 pub fn set_value( 602 &mut self, 603 name: impl ToConfigNamePath, 604 new_value: impl Into<ConfigValue>, 605 ) -> Result<Option<ConfigValue>, ConfigUpdateError> { 606 Arc::make_mut(&mut self.layer).set_value(name, new_value) 607 } 608 609 /// See [`ConfigLayer::delete_value()`]. 610 pub fn delete_value( 611 &mut self, 612 name: impl ToConfigNamePath, 613 ) -> Result<Option<ConfigValue>, ConfigUpdateError> { 614 Arc::make_mut(&mut self.layer).delete_value(name) 615 } 616} 617 618/// Stack of configuration layers which can be merged as needed. 619/// 620/// A [`StackedConfig`] is something like a read-only `overlayfs`. Tables and 621/// values are directories and files respectively, and tables are merged across 622/// layers. Tables and values can be addressed by [dotted name 623/// paths](ToConfigNamePath). 624/// 625/// There's no tombstone notation to remove items from the lower layers. 626/// 627/// Beware that arrays of tables are no different than inline arrays. They are 628/// values, so are never merged. This might be confusing because they would be 629/// merged if two TOML documents are concatenated literally. Avoid using array 630/// of tables syntax. 631#[derive(Clone, Debug)] 632pub struct StackedConfig { 633 /// Layers sorted by `source` (the lowest precedence one first.) 634 layers: Vec<Arc<ConfigLayer>>, 635} 636 637impl StackedConfig { 638 /// Creates an empty stack of configuration layers. 639 pub fn empty() -> Self { 640 StackedConfig { layers: vec![] } 641 } 642 643 /// Creates a stack of configuration layers containing the default variables 644 /// referred to by `jj-lib`. 645 pub fn with_defaults() -> Self { 646 StackedConfig { 647 layers: DEFAULT_CONFIG_LAYERS.to_vec(), 648 } 649 } 650 651 /// Loads config file from the specified `path`, inserts it at the position 652 /// specified by `source`. The file should exist. 653 pub fn load_file( 654 &mut self, 655 source: ConfigSource, 656 path: impl Into<PathBuf>, 657 ) -> Result<(), ConfigLoadError> { 658 let layer = ConfigLayer::load_from_file(source, path.into())?; 659 self.add_layer(layer); 660 Ok(()) 661 } 662 663 /// Loads config files from the specified directory `path`, inserts them at 664 /// the position specified by `source`. The directory should exist. 665 pub fn load_dir( 666 &mut self, 667 source: ConfigSource, 668 path: impl AsRef<Path>, 669 ) -> Result<(), ConfigLoadError> { 670 let layers = ConfigLayer::load_from_dir(source, path.as_ref())?; 671 self.extend_layers(layers); 672 Ok(()) 673 } 674 675 /// Inserts new layer at the position specified by `layer.source`. 676 pub fn add_layer(&mut self, layer: impl Into<Arc<ConfigLayer>>) { 677 let layer = layer.into(); 678 let index = self.insert_point(layer.source); 679 self.layers.insert(index, layer); 680 } 681 682 /// Inserts multiple layers at the positions specified by `layer.source`. 683 pub fn extend_layers<I>(&mut self, layers: I) 684 where 685 I: IntoIterator, 686 I::Item: Into<Arc<ConfigLayer>>, 687 { 688 let layers = layers.into_iter().map(Into::into); 689 for (source, chunk) in &layers.chunk_by(|layer| layer.source) { 690 let index = self.insert_point(source); 691 self.layers.splice(index..index, chunk); 692 } 693 } 694 695 /// Removes layers of the specified `source`. 696 pub fn remove_layers(&mut self, source: ConfigSource) { 697 self.layers.drain(self.layer_range(source)); 698 } 699 700 fn layer_range(&self, source: ConfigSource) -> Range<usize> { 701 // Linear search since the size of Vec wouldn't be large. 702 let start = self 703 .layers 704 .iter() 705 .take_while(|layer| layer.source < source) 706 .count(); 707 let count = self.layers[start..] 708 .iter() 709 .take_while(|layer| layer.source == source) 710 .count(); 711 start..(start + count) 712 } 713 714 fn insert_point(&self, source: ConfigSource) -> usize { 715 // Search from end since layers are usually added in order, and the size 716 // of Vec wouldn't be large enough to do binary search. 717 let skip = self 718 .layers 719 .iter() 720 .rev() 721 .take_while(|layer| layer.source > source) 722 .count(); 723 self.layers.len() - skip 724 } 725 726 /// Layers sorted by precedence. 727 pub fn layers(&self) -> &[Arc<ConfigLayer>] { 728 &self.layers 729 } 730 731 /// Mutable references to layers sorted by precedence. 732 pub fn layers_mut(&mut self) -> &mut [Arc<ConfigLayer>] { 733 &mut self.layers 734 } 735 736 /// Layers of the specified `source`. 737 pub fn layers_for(&self, source: ConfigSource) -> &[Arc<ConfigLayer>] { 738 &self.layers[self.layer_range(source)] 739 } 740 741 /// Looks up value of the specified type `T` from all layers, merges sub 742 /// fields as needed. 743 pub fn get<'de, T: Deserialize<'de>>( 744 &self, 745 name: impl ToConfigNamePath, 746 ) -> Result<T, ConfigGetError> { 747 self.get_value_with(name, |value| T::deserialize(value.into_deserializer())) 748 } 749 750 /// Looks up value from all layers, merges sub fields as needed. 751 pub fn get_value(&self, name: impl ToConfigNamePath) -> Result<ConfigValue, ConfigGetError> { 752 self.get_value_with::<_, Infallible>(name, Ok) 753 } 754 755 /// Looks up value from all layers, merges sub fields as needed, then 756 /// converts the value by using the given function. 757 pub fn get_value_with<T, E: Into<Box<dyn std::error::Error + Send + Sync>>>( 758 &self, 759 name: impl ToConfigNamePath, 760 convert: impl FnOnce(ConfigValue) -> Result<T, E>, 761 ) -> Result<T, ConfigGetError> { 762 self.get_item_with(name, |item| { 763 // Item variants other than Item::None can be converted to a Value, 764 // and Item::None is not a valid TOML type. See also the following 765 // thread: https://github.com/toml-rs/toml/issues/299 766 let value = item 767 .into_value() 768 .expect("Item::None should not exist in loaded tables"); 769 convert(value) 770 }) 771 } 772 773 /// Looks up sub table from all layers, merges fields as needed. 774 /// 775 /// Use `table_keys(prefix)` and `get([prefix, key])` instead if table 776 /// values have to be converted to non-generic value type. 777 pub fn get_table(&self, name: impl ToConfigNamePath) -> Result<ConfigTable, ConfigGetError> { 778 self.get_item_with(name, |item| { 779 item.into_table() 780 .map_err(|item| format!("Expected a table, but is {}", item.type_name())) 781 }) 782 } 783 784 fn get_item_with<T, E: Into<Box<dyn std::error::Error + Send + Sync>>>( 785 &self, 786 name: impl ToConfigNamePath, 787 convert: impl FnOnce(ConfigItem) -> Result<T, E>, 788 ) -> Result<T, ConfigGetError> { 789 let name = name.into_name_path(); 790 let name = name.borrow(); 791 let (item, layer_index) = 792 get_merged_item(&self.layers, name).ok_or_else(|| ConfigGetError::NotFound { 793 name: name.to_string(), 794 })?; 795 // If the value is a table, the error might come from lower layers. We 796 // cannot report precise source information in that case. However, 797 // toml_edit captures dotted keys in the error object. If the keys field 798 // were public, we can look up the source information. This is probably 799 // simpler than reimplementing Deserializer. 800 convert(item).map_err(|err| ConfigGetError::Type { 801 name: name.to_string(), 802 error: err.into(), 803 source_path: self.layers[layer_index].path.clone(), 804 }) 805 } 806 807 /// Returns iterator over sub table keys in order of layer precedence. 808 /// Duplicated keys are omitted. 809 pub fn table_keys(&self, name: impl ToConfigNamePath) -> impl Iterator<Item = &str> { 810 let name = name.into_name_path(); 811 let name = name.borrow(); 812 let to_merge = get_tables_to_merge(&self.layers, name); 813 to_merge 814 .into_iter() 815 .rev() 816 .flat_map(|table| table.iter().map(|(k, _)| k)) 817 .unique() 818 } 819} 820 821/// Looks up item from `layers`, merges sub fields as needed. Returns a merged 822/// item and the uppermost layer index where the item was found. 823fn get_merged_item( 824 layers: &[Arc<ConfigLayer>], 825 name: &ConfigNamePathBuf, 826) -> Option<(ConfigItem, usize)> { 827 let mut to_merge = Vec::new(); 828 for (index, layer) in layers.iter().enumerate().rev() { 829 let item = match layer.look_up_item(name) { 830 Ok(Some(item)) => item, 831 Ok(None) => continue, // parent is a table, but no value found 832 Err(_) => break, // parent is not a table, shadows lower layers 833 }; 834 if item.is_table_like() { 835 to_merge.push((item, index)); 836 } else if to_merge.is_empty() { 837 return Some((item.clone(), index)); // no need to allocate vec 838 } else { 839 break; // shadows lower layers 840 } 841 } 842 843 // Simply merge tables from the bottom layer. Upper items should override 844 // the lower items (including their children) no matter if the upper items 845 // are shadowed by the other upper items. 846 let (item, mut top_index) = to_merge.pop()?; 847 let mut merged = item.clone(); 848 for (item, index) in to_merge.into_iter().rev() { 849 merge_items(&mut merged, item); 850 top_index = index; 851 } 852 Some((merged, top_index)) 853} 854 855/// Looks up tables to be merged from `layers`, returns in reverse order. 856fn get_tables_to_merge<'a>( 857 layers: &'a [Arc<ConfigLayer>], 858 name: &ConfigNamePathBuf, 859) -> Vec<&'a ConfigTableLike<'a>> { 860 let mut to_merge = Vec::new(); 861 for layer in layers.iter().rev() { 862 match layer.look_up_table(name) { 863 Ok(Some(table)) => to_merge.push(table), 864 Ok(None) => {} // parent is a table, but no value found 865 Err(_) => break, // parent/leaf is not a table, shadows lower layers 866 } 867 } 868 to_merge 869} 870 871/// Merges `upper_item` fields into `lower_item` recursively. 872fn merge_items(lower_item: &mut ConfigItem, upper_item: &ConfigItem) { 873 let (Some(lower_table), Some(upper_table)) = 874 (lower_item.as_table_like_mut(), upper_item.as_table_like()) 875 else { 876 // Not a table, the upper item wins. 877 *lower_item = upper_item.clone(); 878 return; 879 }; 880 for (key, upper) in upper_table.iter() { 881 match lower_table.entry(key) { 882 toml_edit::Entry::Occupied(entry) => { 883 merge_items(entry.into_mut(), upper); 884 } 885 toml_edit::Entry::Vacant(entry) => { 886 entry.insert(upper.clone()); 887 } 888 }; 889 } 890} 891 892static DEFAULT_CONFIG_LAYERS: Lazy<[Arc<ConfigLayer>; 1]> = Lazy::new(|| { 893 let parse = |text: &str| Arc::new(ConfigLayer::parse(ConfigSource::Default, text).unwrap()); 894 [parse(include_str!("config/misc.toml"))] 895}); 896 897#[cfg(test)] 898mod tests { 899 use assert_matches::assert_matches; 900 use indoc::indoc; 901 use pretty_assertions::assert_eq; 902 903 use super::*; 904 905 #[test] 906 fn test_config_layer_set_value() { 907 let mut layer = ConfigLayer::empty(ConfigSource::User); 908 // Cannot overwrite the root table 909 assert_matches!( 910 layer.set_value(ConfigNamePathBuf::root(), 0), 911 Err(ConfigUpdateError::WouldOverwriteValue { name }) if name.is_empty() 912 ); 913 914 // Insert some values 915 layer.set_value("foo", 1).unwrap(); 916 layer.set_value("bar.baz.blah", "2").unwrap(); 917 layer 918 .set_value("bar.qux", ConfigValue::from_iter([("inline", "table")])) 919 .unwrap(); 920 layer 921 .set_value("bar.to-update", ConfigValue::from_iter([("some", true)])) 922 .unwrap(); 923 insta::assert_snapshot!(layer.data, @r#" 924 foo = 1 925 926 [bar] 927 qux = { inline = "table" } 928 to-update = { some = true } 929 930 [bar.baz] 931 blah = "2" 932 "#); 933 934 // Can overwrite value 935 layer 936 .set_value("foo", ConfigValue::from_iter(["new", "foo"])) 937 .unwrap(); 938 // Can overwrite inline table 939 layer.set_value("bar.qux", "new bar.qux").unwrap(); 940 // Can add value to inline table 941 layer 942 .set_value( 943 "bar.to-update.new", 944 ConfigValue::from_iter([("table", "value")]), 945 ) 946 .unwrap(); 947 // Cannot overwrite table 948 assert_matches!( 949 layer.set_value("bar", 0), 950 Err(ConfigUpdateError::WouldOverwriteTable { name }) if name == "bar" 951 ); 952 // Cannot overwrite value by table 953 assert_matches!( 954 layer.set_value("bar.baz.blah.blah", 0), 955 Err(ConfigUpdateError::WouldOverwriteValue { name }) if name == "bar.baz.blah" 956 ); 957 insta::assert_snapshot!(layer.data, @r#" 958 foo = ["new", "foo"] 959 960 [bar] 961 qux = "new bar.qux" 962 to-update = { some = true, new = { table = "value" } } 963 964 [bar.baz] 965 blah = "2" 966 "#); 967 } 968 969 #[test] 970 fn test_config_layer_set_value_formatting() { 971 let mut layer = ConfigLayer::empty(ConfigSource::User); 972 // Quoting style should be preserved on insertion 973 layer 974 .set_value( 975 "'foo' . bar . 'baz'", 976 ConfigValue::from_str("'value'").unwrap(), 977 ) 978 .unwrap(); 979 insta::assert_snapshot!(layer.data, @r" 980 ['foo' . bar] 981 'baz' = 'value' 982 "); 983 984 // Style of existing keys isn't updated 985 layer.set_value("foo.bar.baz", "new value").unwrap(); 986 layer.set_value("foo.'bar'.blah", 0).unwrap(); 987 insta::assert_snapshot!(layer.data, @r#" 988 ['foo' . bar] 989 'baz' = "new value" 990 blah = 0 991 "#); 992 } 993 994 #[test] 995 fn test_config_layer_delete_value() { 996 let mut layer = ConfigLayer::empty(ConfigSource::User); 997 // Cannot delete the root table 998 assert_matches!( 999 layer.delete_value(ConfigNamePathBuf::root()), 1000 Err(ConfigUpdateError::WouldDeleteTable { name }) if name.is_empty() 1001 ); 1002 1003 // Insert some values 1004 layer.set_value("foo", 1).unwrap(); 1005 layer.set_value("bar.baz.blah", "2").unwrap(); 1006 layer 1007 .set_value("bar.qux", ConfigValue::from_iter([("inline", "table")])) 1008 .unwrap(); 1009 layer 1010 .set_value("bar.to-update", ConfigValue::from_iter([("some", true)])) 1011 .unwrap(); 1012 insta::assert_snapshot!(layer.data, @r#" 1013 foo = 1 1014 1015 [bar] 1016 qux = { inline = "table" } 1017 to-update = { some = true } 1018 1019 [bar.baz] 1020 blah = "2" 1021 "#); 1022 1023 // Can delete value 1024 let old_value = layer.delete_value("foo").unwrap(); 1025 assert_eq!(old_value.and_then(|v| v.as_integer()), Some(1)); 1026 // Can delete inline table 1027 let old_value = layer.delete_value("bar.qux").unwrap(); 1028 assert!(old_value.is_some_and(|v| v.is_inline_table())); 1029 // Can delete inner value from inline table 1030 let old_value = layer.delete_value("bar.to-update.some").unwrap(); 1031 assert_eq!(old_value.and_then(|v| v.as_bool()), Some(true)); 1032 // Cannot delete table 1033 assert_matches!( 1034 layer.delete_value("bar"), 1035 Err(ConfigUpdateError::WouldDeleteTable { name }) if name == "bar" 1036 ); 1037 // Deleting a non-table child isn't an error because the value doesn't 1038 // exist 1039 assert_matches!(layer.delete_value("bar.baz.blah.blah"), Ok(None)); 1040 insta::assert_snapshot!(layer.data, @r#" 1041 [bar] 1042 to-update = {} 1043 1044 [bar.baz] 1045 blah = "2" 1046 "#); 1047 } 1048 1049 #[test] 1050 fn test_stacked_config_layer_order() { 1051 let empty_data = || DocumentMut::new(); 1052 let layer_sources = |config: &StackedConfig| { 1053 config 1054 .layers() 1055 .iter() 1056 .map(|layer| layer.source) 1057 .collect_vec() 1058 }; 1059 1060 // Insert in reverse order 1061 let mut config = StackedConfig::empty(); 1062 config.add_layer(ConfigLayer::with_data(ConfigSource::Repo, empty_data())); 1063 config.add_layer(ConfigLayer::with_data(ConfigSource::User, empty_data())); 1064 config.add_layer(ConfigLayer::with_data(ConfigSource::Default, empty_data())); 1065 assert_eq!( 1066 layer_sources(&config), 1067 vec![ 1068 ConfigSource::Default, 1069 ConfigSource::User, 1070 ConfigSource::Repo, 1071 ] 1072 ); 1073 1074 // Insert some more 1075 config.add_layer(ConfigLayer::with_data( 1076 ConfigSource::CommandArg, 1077 empty_data(), 1078 )); 1079 config.add_layer(ConfigLayer::with_data(ConfigSource::EnvBase, empty_data())); 1080 config.add_layer(ConfigLayer::with_data(ConfigSource::User, empty_data())); 1081 assert_eq!( 1082 layer_sources(&config), 1083 vec![ 1084 ConfigSource::Default, 1085 ConfigSource::EnvBase, 1086 ConfigSource::User, 1087 ConfigSource::User, 1088 ConfigSource::Repo, 1089 ConfigSource::CommandArg, 1090 ] 1091 ); 1092 1093 // Remove last, first, middle 1094 config.remove_layers(ConfigSource::CommandArg); 1095 config.remove_layers(ConfigSource::Default); 1096 config.remove_layers(ConfigSource::User); 1097 assert_eq!( 1098 layer_sources(&config), 1099 vec![ConfigSource::EnvBase, ConfigSource::Repo] 1100 ); 1101 1102 // Remove unknown 1103 config.remove_layers(ConfigSource::Default); 1104 config.remove_layers(ConfigSource::EnvOverrides); 1105 assert_eq!( 1106 layer_sources(&config), 1107 vec![ConfigSource::EnvBase, ConfigSource::Repo] 1108 ); 1109 1110 // Insert multiple 1111 config.extend_layers([ 1112 ConfigLayer::with_data(ConfigSource::Repo, empty_data()), 1113 ConfigLayer::with_data(ConfigSource::Repo, empty_data()), 1114 ConfigLayer::with_data(ConfigSource::User, empty_data()), 1115 ]); 1116 assert_eq!( 1117 layer_sources(&config), 1118 vec![ 1119 ConfigSource::EnvBase, 1120 ConfigSource::User, 1121 ConfigSource::Repo, 1122 ConfigSource::Repo, 1123 ConfigSource::Repo, 1124 ] 1125 ); 1126 1127 // Remove remainders 1128 config.remove_layers(ConfigSource::EnvBase); 1129 config.remove_layers(ConfigSource::User); 1130 config.remove_layers(ConfigSource::Repo); 1131 assert_eq!(layer_sources(&config), vec![]); 1132 } 1133 1134 fn new_user_layer(text: &str) -> ConfigLayer { 1135 ConfigLayer::parse(ConfigSource::User, text).unwrap() 1136 } 1137 1138 #[test] 1139 fn test_stacked_config_get_simple_value() { 1140 let mut config = StackedConfig::empty(); 1141 config.add_layer(new_user_layer(indoc! {" 1142 a.b.c = 'a.b.c #0' 1143 "})); 1144 config.add_layer(new_user_layer(indoc! {" 1145 a.d = ['a.d #1'] 1146 "})); 1147 1148 assert_eq!(config.get::<String>("a.b.c").unwrap(), "a.b.c #0"); 1149 1150 assert_eq!( 1151 config.get::<Vec<String>>("a.d").unwrap(), 1152 vec!["a.d #1".to_owned()] 1153 ); 1154 1155 // Table "a.b" exists, but key doesn't 1156 assert_matches!( 1157 config.get::<String>("a.b.missing"), 1158 Err(ConfigGetError::NotFound { name }) if name == "a.b.missing" 1159 ); 1160 1161 // Node "a.b.c" is not a table 1162 assert_matches!( 1163 config.get::<String>("a.b.c.d"), 1164 Err(ConfigGetError::NotFound { name }) if name == "a.b.c.d" 1165 ); 1166 1167 // Type error 1168 assert_matches!( 1169 config.get::<String>("a.b"), 1170 Err(ConfigGetError::Type { name, .. }) if name == "a.b" 1171 ); 1172 } 1173 1174 #[test] 1175 fn test_stacked_config_get_table_as_value() { 1176 let mut config = StackedConfig::empty(); 1177 config.add_layer(new_user_layer(indoc! {" 1178 a.b = { c = 'a.b.c #0' } 1179 "})); 1180 config.add_layer(new_user_layer(indoc! {" 1181 a.d = ['a.d #1'] 1182 "})); 1183 1184 // Table can be converted to a value (so it can be deserialized to a 1185 // structured value.) 1186 insta::assert_snapshot!( 1187 config.get_value("a").unwrap(), 1188 @"{ b = { c = 'a.b.c #0' }, d = ['a.d #1'] }"); 1189 } 1190 1191 #[test] 1192 fn test_stacked_config_get_inline_table() { 1193 let mut config = StackedConfig::empty(); 1194 config.add_layer(new_user_layer(indoc! {" 1195 a.b = { c = 'a.b.c #0' } 1196 "})); 1197 config.add_layer(new_user_layer(indoc! {" 1198 a.b = { d = 'a.b.d #1' } 1199 "})); 1200 1201 // Inline tables are merged 1202 insta::assert_snapshot!( 1203 config.get_value("a.b").unwrap(), 1204 @" { c = 'a.b.c #0' , d = 'a.b.d #1' }"); 1205 } 1206 1207 #[test] 1208 fn test_stacked_config_get_inline_non_inline_table() { 1209 let mut config = StackedConfig::empty(); 1210 config.add_layer(new_user_layer(indoc! {" 1211 a.b = { c = 'a.b.c #0' } 1212 "})); 1213 config.add_layer(new_user_layer(indoc! {" 1214 a.b.d = 'a.b.d #1' 1215 "})); 1216 1217 insta::assert_snapshot!( 1218 config.get_value("a.b").unwrap(), 1219 @" { c = 'a.b.c #0' , d = 'a.b.d #1'}"); 1220 insta::assert_snapshot!( 1221 config.get_table("a").unwrap(), 1222 @"b = { c = 'a.b.c #0' , d = 'a.b.d #1'}"); 1223 } 1224 1225 #[test] 1226 fn test_stacked_config_get_value_shadowing_table() { 1227 let mut config = StackedConfig::empty(); 1228 config.add_layer(new_user_layer(indoc! {" 1229 a.b.c = 'a.b.c #0' 1230 "})); 1231 // a.b.c is shadowed by a.b 1232 config.add_layer(new_user_layer(indoc! {" 1233 a.b = 'a.b #1' 1234 "})); 1235 1236 assert_eq!(config.get::<String>("a.b").unwrap(), "a.b #1"); 1237 1238 assert_matches!( 1239 config.get::<String>("a.b.c"), 1240 Err(ConfigGetError::NotFound { name }) if name == "a.b.c" 1241 ); 1242 } 1243 1244 #[test] 1245 fn test_stacked_config_get_table_shadowing_table() { 1246 let mut config = StackedConfig::empty(); 1247 config.add_layer(new_user_layer(indoc! {" 1248 a.b = 'a.b #0' 1249 "})); 1250 // a.b is shadowed by a.b.c 1251 config.add_layer(new_user_layer(indoc! {" 1252 a.b.c = 'a.b.c #1' 1253 "})); 1254 insta::assert_snapshot!(config.get_table("a.b").unwrap(), @"c = 'a.b.c #1'"); 1255 } 1256 1257 #[test] 1258 fn test_stacked_config_get_merged_table() { 1259 let mut config = StackedConfig::empty(); 1260 config.add_layer(new_user_layer(indoc! {" 1261 a.a.a = 'a.a.a #0' 1262 a.a.b = 'a.a.b #0' 1263 a.b = 'a.b #0' 1264 "})); 1265 config.add_layer(new_user_layer(indoc! {" 1266 a.a.b = 'a.a.b #1' 1267 a.a.c = 'a.a.c #1' 1268 a.c = 'a.c #1' 1269 "})); 1270 insta::assert_snapshot!(config.get_table("a").unwrap(), @r" 1271 a.a = 'a.a.a #0' 1272 a.b = 'a.a.b #1' 1273 a.c = 'a.a.c #1' 1274 b = 'a.b #0' 1275 c = 'a.c #1' 1276 "); 1277 assert_eq!(config.table_keys("a").collect_vec(), vec!["a", "b", "c"]); 1278 assert_eq!(config.table_keys("a.a").collect_vec(), vec!["a", "b", "c"]); 1279 assert_eq!(config.table_keys("a.b").collect_vec(), vec![""; 0]); 1280 assert_eq!(config.table_keys("a.missing").collect_vec(), vec![""; 0]); 1281 } 1282 1283 #[test] 1284 fn test_stacked_config_get_merged_table_shadowed_top() { 1285 let mut config = StackedConfig::empty(); 1286 config.add_layer(new_user_layer(indoc! {" 1287 a.a.a = 'a.a.a #0' 1288 a.b = 'a.b #0' 1289 "})); 1290 // a.a.a and a.b are shadowed by a 1291 config.add_layer(new_user_layer(indoc! {" 1292 a = 'a #1' 1293 "})); 1294 // a is shadowed by a.a.b 1295 config.add_layer(new_user_layer(indoc! {" 1296 a.a.b = 'a.a.b #2' 1297 "})); 1298 insta::assert_snapshot!(config.get_table("a").unwrap(), @"a.b = 'a.a.b #2'"); 1299 assert_eq!(config.table_keys("a").collect_vec(), vec!["a"]); 1300 assert_eq!(config.table_keys("a.a").collect_vec(), vec!["b"]); 1301 } 1302 1303 #[test] 1304 fn test_stacked_config_get_merged_table_shadowed_child() { 1305 let mut config = StackedConfig::empty(); 1306 config.add_layer(new_user_layer(indoc! {" 1307 a.a.a = 'a.a.a #0' 1308 a.b = 'a.b #0' 1309 "})); 1310 // a.a.a is shadowed by a.a 1311 config.add_layer(new_user_layer(indoc! {" 1312 a.a = 'a.a #1' 1313 "})); 1314 // a.a is shadowed by a.a.b 1315 config.add_layer(new_user_layer(indoc! {" 1316 a.a.b = 'a.a.b #2' 1317 "})); 1318 insta::assert_snapshot!(config.get_table("a").unwrap(), @r" 1319 a.b = 'a.a.b #2' 1320 b = 'a.b #0' 1321 "); 1322 assert_eq!(config.table_keys("a").collect_vec(), vec!["a", "b"]); 1323 assert_eq!(config.table_keys("a.a").collect_vec(), vec!["b"]); 1324 } 1325 1326 #[test] 1327 fn test_stacked_config_get_merged_table_shadowed_parent() { 1328 let mut config = StackedConfig::empty(); 1329 config.add_layer(new_user_layer(indoc! {" 1330 a.a.a = 'a.a.a #0' 1331 "})); 1332 // a.a.a is shadowed by a 1333 config.add_layer(new_user_layer(indoc! {" 1334 a = 'a #1' 1335 "})); 1336 // a is shadowed by a.a.b 1337 config.add_layer(new_user_layer(indoc! {" 1338 a.a.b = 'a.a.b #2' 1339 "})); 1340 // a is not under a.a, but it should still shadow lower layers 1341 insta::assert_snapshot!(config.get_table("a.a").unwrap(), @"b = 'a.a.b #2'"); 1342 assert_eq!(config.table_keys("a.a").collect_vec(), vec!["b"]); 1343 } 1344}