diff --git a/README.md b/README.md index c4a2419..867aa37 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Options: -c, --oldconfig Path to the old config file -o, --output Path to the output config file -f, --fmt The output format [default: toml] [possible values: toml, rust] + -w, --write Setting a config item with format `table.key=value` + -v, --verbose Verbose mode -h, --help Print help -V, --version Print version ``` diff --git a/axconfig-gen/README.md b/axconfig-gen/README.md index 191a92e..c7ac99e 100644 --- a/axconfig-gen/README.md +++ b/axconfig-gen/README.md @@ -12,6 +12,8 @@ Options: -c, --oldconfig Path to the old config file -o, --output Path to the output config file -f, --fmt The output format [default: toml] [possible values: toml, rust] + -w, --write Setting a config item with format `table.key=value` + -v, --verbose Verbose mode -h, --help Print help -V, --version Print version ``` diff --git a/axconfig-gen/src/config.rs b/axconfig-gen/src/config.rs index f54b2cd..c3bf197 100644 --- a/axconfig-gen/src/config.rs +++ b/axconfig-gen/src/config.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use toml_edit::{Decor, DocumentMut, Item, Table, Value}; use crate::output::{Output, OutputFormat}; @@ -11,13 +11,14 @@ type ConfigTable = BTreeMap; /// It contains the config key, value and comments. #[derive(Debug, Clone)] pub struct ConfigItem { + table_name: String, key: String, value: ConfigValue, comments: String, } impl ConfigItem { - fn new(table: &Table, key: &str, value: &Value) -> ConfigResult { + fn new(table_name: &str, table: &Table, key: &str, value: &Value) -> ConfigResult { let inner = || { let item = table.key(key).unwrap(); let comments = prefix_comments(item.leaf_decor()) @@ -32,6 +33,7 @@ impl ConfigItem { ConfigValue::from_raw_value(value)? }; Ok(Self { + table_name: table_name.into(), key: key.into(), value, comments, @@ -44,6 +46,27 @@ impl ConfigItem { res } + fn new_global(table: &Table, key: &str, value: &Value) -> ConfigResult { + Self::new(Config::GLOBAL_TABLE_NAME, table, key, value) + } + + /// Returns the unique name of the config item. + /// + /// If the item is contained in the global table, it returns the iten key. + /// Otherwise, it returns a string with the format `table.key`. + pub fn item_name(&self) -> String { + if self.table_name == Config::GLOBAL_TABLE_NAME { + self.key.clone() + } else { + format!("{}.{}", self.table_name, self.key) + } + } + + /// Returns the table name of the config item. + pub fn table_name(&self) -> &str { + &self.table_name + } + /// Returns the key of the config item. pub fn key(&self) -> &str { &self.key @@ -58,6 +81,11 @@ impl ConfigItem { pub fn comments(&self) -> &str { &self.comments } + + /// Returns the mutable reference to the value of the config item. + pub fn value_mut(&mut self) -> &mut ConfigValue { + &mut self.value + } } /// A structure storing all config items. @@ -72,6 +100,9 @@ pub struct Config { } impl Config { + /// The name of the global table of the config. + pub const GLOBAL_TABLE_NAME: &'static str = "$GLOBAL"; + /// Create a new empty config object. pub fn new() -> Self { Self { @@ -82,10 +113,11 @@ impl Config { } fn new_table(&mut self, name: &str, comments: &str) -> ConfigResult<&mut ConfigTable> { - if name == "__GLOBAL__" { - return Err(ConfigErr::Other( - "Table name `__GLOBAL__` is reserved".into(), - )); + if name == Self::GLOBAL_TABLE_NAME { + return Err(ConfigErr::Other(format!( + "Table name `{}` is reserved", + Self::GLOBAL_TABLE_NAME + ))); } if self.tables.contains_key(name) { return Err(ConfigErr::Other(format!("Duplicate table name `{}`", name))); @@ -102,12 +134,20 @@ impl Config { /// Returns the reference to the table with the specified name. pub fn table_at(&self, name: &str) -> Option<&BTreeMap> { - self.tables.get(name) + if name == Self::GLOBAL_TABLE_NAME { + Some(&self.global) + } else { + self.tables.get(name) + } } /// Returns the mutable reference to the table with the specified name. pub fn table_at_mut(&mut self, name: &str) -> Option<&mut BTreeMap> { - self.tables.get_mut(name) + if name == Self::GLOBAL_TABLE_NAME { + Some(&mut self.global) + } else { + self.tables.get_mut(name) + } } /// Returns the reference to the config item with the specified table name and key. @@ -129,9 +169,9 @@ impl Config { /// Returns the iterator of all tables. /// /// The iterator returns a tuple of table name, table and comments. The - /// global table is named `__GLOBAL__`. + /// global table is named `$GLOBAL`. pub fn table_iter(&self) -> impl Iterator { - let global_iter = [("__GLOBAL__", &self.global, "")].into_iter(); + let global_iter = [(Self::GLOBAL_TABLE_NAME, &self.global, "")].into_iter(); let other_iter = self.tables.iter().map(|(name, configs)| { ( name.as_str(), @@ -145,16 +185,9 @@ impl Config { /// Returns the iterator of all config items. /// /// The iterator returns a tuple of table name, key and config item. The - /// global table is named `__GLOBAL__`. - pub fn iter(&self) -> impl Iterator { - let global_iter = self - .global - .iter() - .map(|(k, v)| ("__GLOBAL__", k.as_str(), v)); - let other_iter = self - .table_iter() - .flat_map(|(t, c, _)| c.iter().map(move |(k, v)| (t, k.as_str(), v))); - global_iter.chain(other_iter) + /// global table is named `$GLOBAL`. + pub fn iter(&self) -> impl Iterator { + self.table_iter().flat_map(|(_, c, _)| c.values()) } } @@ -170,14 +203,16 @@ impl Config { Item::Value(val) => { result .global - .insert(key.into(), ConfigItem::new(table, key, val)?); + .insert(key.into(), ConfigItem::new_global(table, key, val)?); } Item::Table(table) => { + let table_name = key; let comments = prefix_comments(table.decor()); let configs = result.new_table(key, comments.unwrap_or_default())?; for (key, item) in table.iter() { if let Item::Value(val) = item { - configs.insert(key.into(), ConfigItem::new(table, key, val)?); + configs + .insert(key.into(), ConfigItem::new(table_name, table, key, val)?); } else { return Err(ConfigErr::InvalidValue); } @@ -199,7 +234,7 @@ impl Config { pub fn dump(&self, fmt: OutputFormat) -> ConfigResult { let mut output = Output::new(fmt); for (name, table, comments) in self.table_iter() { - if name != "__GLOBAL__" { + if name != Self::GLOBAL_TABLE_NAME { output.table_begin(name, comments); } for (key, item) in table.iter() { @@ -207,7 +242,7 @@ impl Config { eprintln!("Dump config `{}` failed: {:?}", key, e); } } - if name != "__GLOBAL__" { + if name != Self::GLOBAL_TABLE_NAME { output.table_end(); } } @@ -224,12 +259,10 @@ impl Config { self.dump(OutputFormat::Rust) } - /// Merge the other config into self, if there is a duplicate key, return an error. + /// Merge the other config into `self`, if there is a duplicate key, return an error. pub fn merge(&mut self, other: &Self) -> ConfigResult<()> { for (name, other_table, table_comments) in other.table_iter() { - let self_table = if name == "__GLOBAL__" { - &mut self.global - } else if let Some(table) = self.tables.get_mut(name) { + let self_table = if let Some(table) = self.table_at_mut(name) { table } else { self.new_table(name, table_comments)? @@ -245,31 +278,41 @@ impl Config { Ok(()) } - /// Update the values of self with the other config, if there is a key not found in self, skip it. - pub fn update(&mut self, other: &Self) -> ConfigResult<()> { - for (table_name, key, other_item) in other.iter() { - let self_table = if table_name == "__GLOBAL__" { - &mut self.global - } else if let Some(table) = self.tables.get_mut(table_name) { + /// Update the values of `self` with the other config, if there is a key not + /// found in `self`, skip it. + /// + /// It returns two vectors of `ConfigItem`, the first contains the keys that + /// are included in `self` but not in `other`, the second contains the keys + /// that are included in `other` but not in `self`. + pub fn update(&mut self, other: &Self) -> ConfigResult<(Vec, Vec)> { + let mut touched = BTreeSet::new(); // included in both `self` and `other` + let mut extra = Vec::new(); // included in `other` but not in `self` + + for other_item in other.iter() { + let table_name = other_item.table_name.clone(); + let key = other_item.key.clone(); + let self_table = if let Some(table) = self.table_at_mut(&table_name) { table } else { + extra.push(other_item.clone()); continue; }; - if let Some(self_item) = self_table.get_mut(key) { - if let Some(ty) = self_item.value.ty() { - if let Ok(new_value) = - ConfigValue::from_raw_value_type(other_item.value.value(), ty.clone()) - { - self_item.value = new_value; - } else { - eprintln!("Type mismatch for key `{}`: expected `{:?}`", key, ty); - return Err(ConfigErr::ValueTypeMismatch); - } - } + if let Some(self_item) = self_table.get_mut(&key) { + self_item.value.update(other_item.value.clone())?; + touched.insert(self_item.item_name()); + } else { + extra.push(other_item.clone()); } } - Ok(()) + + // included in `self` but not in `other` + let untouched = self + .iter() + .filter(|item| !touched.contains(&item.item_name())) + .cloned() + .collect::>(); + Ok((untouched, extra)) } } diff --git a/axconfig-gen/src/main.rs b/axconfig-gen/src/main.rs index f36ea51..04a88ce 100644 --- a/axconfig-gen/src/main.rs +++ b/axconfig-gen/src/main.rs @@ -1,6 +1,6 @@ use std::io; -use axconfig_gen::{Config, OutputFormat}; +use axconfig_gen::{Config, ConfigValue, OutputFormat}; use clap::builder::{PossibleValuesParser, TypedValueParser}; use clap::Parser; @@ -27,28 +27,96 @@ struct Args { .map(|s| s.parse::().unwrap()), )] fmt: OutputFormat, + + /// Setting a config item with format `table.key=value` + #[arg(short, long, id = "CONFIG")] + write: Vec, + + /// Verbose mode + #[arg(short, long)] + verbose: bool, +} + +fn parse_config_write_cmd(cmd: &str) -> Result<(String, String, String), String> { + let (item, value) = cmd.split_once('=').ok_or_else(|| { + format!( + "Invalid config setting command `{}`, expected `table.key=value`", + cmd + ) + })?; + if let Some((table, key)) = item.split_once('.') { + Ok((table.into(), key.into(), value.into())) + } else { + Ok((Config::GLOBAL_TABLE_NAME.into(), item.into(), value.into())) + } +} + +macro_rules! unwrap { + ($e:expr) => { + match $e { + Ok(v) => v, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + } + }; } fn main() -> io::Result<()> { let args = Args::parse(); - let mut defconfig = Config::new(); + macro_rules! debug { + ($($arg:tt)*) => { + if args.verbose { + eprintln!($($arg)*); + } + }; + } + + let mut config = Config::new(); for spec in args.spec { + debug!("Reading config spec from {:?}", spec); let spec_toml = std::fs::read_to_string(spec)?; - let sub_config = Config::from_toml(&spec_toml).unwrap(); - defconfig.merge(&sub_config).unwrap(); + let sub_config = unwrap!(Config::from_toml(&spec_toml)); + unwrap!(config.merge(&sub_config)); } - let output_config = if let Some(oldconfig_path) = args.oldconfig { + if let Some(oldconfig_path) = args.oldconfig { + debug!("Loading old config from {:?}", oldconfig_path); let oldconfig_toml = std::fs::read_to_string(oldconfig_path)?; - let oldconfig = Config::from_toml(&oldconfig_toml).unwrap(); - defconfig.update(&oldconfig).unwrap(); - defconfig - } else { - defconfig - }; + let oldconfig = unwrap!(Config::from_toml(&oldconfig_toml)); + + let (untouched, extra) = unwrap!(config.update(&oldconfig)); + for item in &untouched { + eprintln!( + "Warning: config item `{}` not set in the old config, using default value", + item.item_name(), + ); + } + for item in &extra { + eprintln!( + "Warning: config item `{}` not found in the specification, ignoring", + item.item_name(), + ); + } + } + + for cmd in args.write { + let (table, key, value) = unwrap!(parse_config_write_cmd(&cmd)); + if table == Config::GLOBAL_TABLE_NAME { + debug!("Setting config item `{}` to `{}`", key, value); + } else { + debug!("Setting config item `{}.{}` to `{}`", table, key, value); + } + let new_value = unwrap!(ConfigValue::new(&value)); + let item = unwrap!(config + .config_at_mut(&table, &key) + .ok_or("Config item not found")); + unwrap!(item.value_mut().update(new_value)); + } - let output = output_config.dump(args.fmt).unwrap(); + let output = unwrap!(config.dump(args.fmt)); if let Some(path) = args.output { std::fs::write(path, output)?; } else { diff --git a/axconfig-gen/src/tests.rs b/axconfig-gen/src/tests.rs index 7dbdab2..0dd0c26 100644 --- a/axconfig-gen/src/tests.rs +++ b/axconfig-gen/src/tests.rs @@ -78,7 +78,7 @@ fn test_type_match() { check_match!("0", "uint"); check_match!("0", "int"); check_match!("2333", "int"); - check_match!("-2333", "uint"); + check_match!("-2333", " uint"); check_match!("0b1010", "int"); check_match!("0xdead_beef", "int"); @@ -91,11 +91,11 @@ fn test_type_match() { check_match!("\"\"", "str"); check_match!("[1, 2, 3]", "[uint]"); - check_match!("[\"1\", \"2\", \"3\"]", "[uint]"); + check_match!("[\"1\", \"2\", \"3\"]", "[ uint ]"); check_match!("[\"1\", \"2\", \"3\"]", "[str]"); check_match!("[true, false, true]", "[bool]"); - check_match!("[\"0\", \"a\", true, -2]", "(uint, str, bool, int)"); - check_mismatch!("[\"0\", \"a\", true, -2]", "[uint]"); + check_match!("[\"0\", \"a\", true, -2]", "( uint, str, bool, int )"); + check_mismatch!("[\"0\", \"a\", true, -2]", "[uint] "); check_match!("[]", "[int]"); check_match!("[[]]", "[()]"); check_match!("[[2, 3, 3, 3], [4, 5, 6, 7]]", "[[uint]]"); @@ -117,6 +117,7 @@ fn test_type_match() { #[test] fn test_err() { assert_err!(ConfigType::new("Bool"), InvalidType); + assert_err!(ConfigType::new("u int"), InvalidType); assert_err!(ConfigType::new("usize"), InvalidType); assert_err!(ConfigType::new(""), InvalidType); assert_err!(ConfigType::new("&str"), InvalidType); diff --git a/axconfig-gen/src/ty.rs b/axconfig-gen/src/ty.rs index a458d1f..75845e4 100644 --- a/axconfig-gen/src/ty.rs +++ b/axconfig-gen/src/ty.rs @@ -24,20 +24,19 @@ pub enum ConfigType { impl ConfigType { /// Parses a type string into a [`ConfigType`]. pub fn new(ty: &str) -> ConfigResult { + let ty = ty.trim(); #[cfg(test)] if ty == "?" { return Ok(Self::Unknown); } - - let ty = ty.replace(" ", ""); - match ty.as_str() { + match ty { "bool" => Ok(Self::Bool), "int" => Ok(Self::Int), "uint" => Ok(Self::Uint), "str" => Ok(Self::String), _ => { if ty.starts_with("(") && ty.ends_with(")") { - let tuple = &ty[1..ty.len() - 1]; + let tuple = ty[1..ty.len() - 1].trim(); if tuple.is_empty() { return Ok(Self::Tuple(Vec::new())); } @@ -48,7 +47,7 @@ impl ConfigType { .collect::>>()?; Ok(Self::Tuple(tuple_types)) } else if ty.starts_with('[') && ty.ends_with("]") { - let element = &ty[1..ty.len() - 1]; + let element = ty[1..ty.len() - 1].trim(); if element.is_empty() { return Err(ConfigErr::InvalidType); } diff --git a/axconfig-gen/src/value.rs b/axconfig-gen/src/value.rs index 7595af4..fd5ca0f 100644 --- a/axconfig-gen/src/value.rs +++ b/axconfig-gen/src/value.rs @@ -54,9 +54,29 @@ impl ConfigValue { self.ty.as_ref() } - /// Returns the value of the config item. - pub(crate) fn value(&self) -> &Value { - &self.value + /// Updates the config value with a new value. + pub fn update(&mut self, new_value: Self) -> ConfigResult<()> { + match (&self.ty, &new_value.ty) { + (Some(ty), Some(new_ty)) => { + if ty != new_ty { + return Err(ConfigErr::ValueTypeMismatch); + } + } + (Some(ty), None) => { + if !value_type_matches(&new_value.value, ty) { + return Err(ConfigErr::ValueTypeMismatch); + } + } + (None, Some(new_ty)) => { + if !value_type_matches(&self.value, new_ty) { + return Err(ConfigErr::ValueTypeMismatch); + } + self.ty = new_value.ty; + } + _ => {} + } + self.value = new_value.value; + Ok(()) } /// Returns the inferred type of the config value.