config_items.rs
669 lines
| 20.9 KiB
| application/rls-services+xml
|
RustLexer
|
r51656 | //! Code for parsing default Mercurial config items. | ||
use itertools::Itertools; | ||||
use serde::Deserialize; | ||||
use crate::{errors::HgError, exit_codes, FastHashMap}; | ||||
/// Corresponds to the structure of `mercurial/configitems.toml`. | ||||
#[derive(Debug, Deserialize)] | ||||
pub struct ConfigItems { | ||||
items: Vec<DefaultConfigItem>, | ||||
templates: FastHashMap<String, Vec<TemplateItem>>, | ||||
#[serde(rename = "template-applications")] | ||||
template_applications: Vec<TemplateApplication>, | ||||
} | ||||
/// Corresponds to a config item declaration in `mercurial/configitems.toml`. | ||||
#[derive(Clone, Debug, PartialEq, Deserialize)] | ||||
#[serde(try_from = "RawDefaultConfigItem")] | ||||
pub struct DefaultConfigItem { | ||||
/// Section of the config the item is in (e.g. `[merge-tools]`) | ||||
section: String, | ||||
/// Name of the item (e.g. `meld.gui`) | ||||
name: String, | ||||
/// Default value (can be dynamic, see [`DefaultConfigItemType`]) | ||||
default: Option<DefaultConfigItemType>, | ||||
/// If the config option is generic (e.g. `merge-tools.*`), defines | ||||
/// the priority of this item relative to other generic items. | ||||
/// If we're looking for <pattern>, then all generic items within the same | ||||
/// section will be sorted by order of priority, and the first regex match | ||||
/// against `name` is returned. | ||||
#[serde(default)] | ||||
priority: Option<isize>, | ||||
/// Aliases, if any. Each alias is a tuple of `(section, name)` for each | ||||
/// option that is aliased to this one. | ||||
#[serde(default)] | ||||
alias: Vec<(String, String)>, | ||||
/// Whether the config item is marked as experimental | ||||
#[serde(default)] | ||||
experimental: bool, | ||||
/// The (possibly empty) docstring for the item | ||||
#[serde(default)] | ||||
documentation: String, | ||||
} | ||||
/// Corresponds to the raw (i.e. on disk) structure of config items. Used as | ||||
/// an intermediate step in deserialization. | ||||
#[derive(Clone, Debug, Deserialize)] | ||||
struct RawDefaultConfigItem { | ||||
section: String, | ||||
name: String, | ||||
default: Option<toml::Value>, | ||||
#[serde(rename = "default-type")] | ||||
default_type: Option<String>, | ||||
#[serde(default)] | ||||
priority: isize, | ||||
#[serde(default)] | ||||
generic: bool, | ||||
#[serde(default)] | ||||
alias: Vec<(String, String)>, | ||||
#[serde(default)] | ||||
experimental: bool, | ||||
#[serde(default)] | ||||
documentation: String, | ||||
} | ||||
impl TryFrom<RawDefaultConfigItem> for DefaultConfigItem { | ||||
type Error = HgError; | ||||
fn try_from(value: RawDefaultConfigItem) -> Result<Self, Self::Error> { | ||||
Ok(Self { | ||||
section: value.section, | ||||
name: value.name, | ||||
default: raw_default_to_concrete( | ||||
value.default_type, | ||||
value.default, | ||||
)?, | ||||
priority: if value.generic { | ||||
Some(value.priority) | ||||
} else { | ||||
None | ||||
}, | ||||
alias: value.alias, | ||||
experimental: value.experimental, | ||||
documentation: value.documentation, | ||||
}) | ||||
} | ||||
} | ||||
impl DefaultConfigItem { | ||||
fn is_generic(&self) -> bool { | ||||
self.priority.is_some() | ||||
} | ||||
} | ||||
impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a str> { | ||||
type Error = HgError; | ||||
fn try_from( | ||||
value: &'a DefaultConfigItem, | ||||
) -> Result<Option<&'a str>, Self::Error> { | ||||
match &value.default { | ||||
Some(default) => { | ||||
let err = HgError::abort( | ||||
format!( | ||||
"programming error: wrong query on config item '{}.{}'", | ||||
value.section, | ||||
value.name | ||||
), | ||||
exit_codes::ABORT, | ||||
Some(format!( | ||||
"asked for '&str', type of default is '{}'", | ||||
default.type_str() | ||||
)), | ||||
); | ||||
match default { | ||||
DefaultConfigItemType::Primitive(toml::Value::String( | ||||
s, | ||||
)) => Ok(Some(s)), | ||||
_ => Err(err), | ||||
} | ||||
} | ||||
None => Ok(None), | ||||
} | ||||
} | ||||
} | ||||
impl TryFrom<&DefaultConfigItem> for Option<bool> { | ||||
type Error = HgError; | ||||
fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> { | ||||
match &value.default { | ||||
Some(default) => { | ||||
let err = HgError::abort( | ||||
format!( | ||||
"programming error: wrong query on config item '{}.{}'", | ||||
value.section, | ||||
value.name | ||||
), | ||||
exit_codes::ABORT, | ||||
Some(format!( | ||||
"asked for 'bool', type of default is '{}'", | ||||
default.type_str() | ||||
)), | ||||
); | ||||
match default { | ||||
DefaultConfigItemType::Primitive( | ||||
toml::Value::Boolean(b), | ||||
) => Ok(Some(*b)), | ||||
_ => Err(err), | ||||
} | ||||
} | ||||
None => Ok(Some(false)), | ||||
} | ||||
} | ||||
} | ||||
impl TryFrom<&DefaultConfigItem> for Option<u32> { | ||||
type Error = HgError; | ||||
fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> { | ||||
match &value.default { | ||||
Some(default) => { | ||||
let err = HgError::abort( | ||||
format!( | ||||
"programming error: wrong query on config item '{}.{}'", | ||||
value.section, | ||||
value.name | ||||
), | ||||
exit_codes::ABORT, | ||||
Some(format!( | ||||
"asked for 'u32', type of default is '{}'", | ||||
default.type_str() | ||||
)), | ||||
); | ||||
match default { | ||||
DefaultConfigItemType::Primitive( | ||||
toml::Value::Integer(b), | ||||
) => { | ||||
Ok(Some((*b).try_into().expect("TOML integer to u32"))) | ||||
} | ||||
_ => Err(err), | ||||
} | ||||
} | ||||
None => Ok(None), | ||||
} | ||||
} | ||||
} | ||||
impl TryFrom<&DefaultConfigItem> for Option<u64> { | ||||
type Error = HgError; | ||||
fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> { | ||||
match &value.default { | ||||
Some(default) => { | ||||
let err = HgError::abort( | ||||
format!( | ||||
"programming error: wrong query on config item '{}.{}'", | ||||
value.section, | ||||
value.name | ||||
), | ||||
exit_codes::ABORT, | ||||
Some(format!( | ||||
"asked for 'u64', type of default is '{}'", | ||||
default.type_str() | ||||
)), | ||||
); | ||||
match default { | ||||
DefaultConfigItemType::Primitive( | ||||
toml::Value::Integer(b), | ||||
) => { | ||||
Ok(Some((*b).try_into().expect("TOML integer to u64"))) | ||||
} | ||||
_ => Err(err), | ||||
} | ||||
} | ||||
None => Ok(None), | ||||
} | ||||
} | ||||
} | ||||
/// Allows abstracting over more complex default values than just primitives. | ||||
/// The former `configitems.py` contained some dynamic code that is encoded | ||||
/// in this enum. | ||||
#[derive(Debug, PartialEq, Clone, Deserialize)] | ||||
pub enum DefaultConfigItemType { | ||||
/// Some primitive type (string, integer, boolean) | ||||
Primitive(toml::Value), | ||||
/// A dynamic value that will be given by the code at runtime | ||||
Dynamic, | ||||
/// An lazily-returned array (possibly only relevant in the Python impl) | ||||
/// Example: `lambda: [b"zstd", b"zlib"]` | ||||
Lambda(Vec<String>), | ||||
/// For now, a special case for `web.encoding` that points to the | ||||
/// `encoding.encoding` module in the Python impl so that local encoding | ||||
/// is correctly resolved at runtime | ||||
LazyModule(String), | ||||
ListType, | ||||
} | ||||
impl DefaultConfigItemType { | ||||
pub fn type_str(&self) -> &str { | ||||
match self { | ||||
DefaultConfigItemType::Primitive(primitive) => { | ||||
primitive.type_str() | ||||
} | ||||
DefaultConfigItemType::Dynamic => "dynamic", | ||||
DefaultConfigItemType::Lambda(_) => "lambda", | ||||
DefaultConfigItemType::LazyModule(_) => "lazy_module", | ||||
DefaultConfigItemType::ListType => "list_type", | ||||
} | ||||
} | ||||
} | ||||
/// Most of the fields are shared with [`DefaultConfigItem`]. | ||||
#[derive(Debug, Clone, Deserialize)] | ||||
#[serde(try_from = "RawTemplateItem")] | ||||
struct TemplateItem { | ||||
suffix: String, | ||||
default: Option<DefaultConfigItemType>, | ||||
priority: Option<isize>, | ||||
#[serde(default)] | ||||
alias: Vec<(String, String)>, | ||||
#[serde(default)] | ||||
experimental: bool, | ||||
#[serde(default)] | ||||
documentation: String, | ||||
} | ||||
/// Corresponds to the raw (i.e. on disk) representation of a template item. | ||||
/// Used as an intermediate step in deserialization. | ||||
#[derive(Clone, Debug, Deserialize)] | ||||
struct RawTemplateItem { | ||||
suffix: String, | ||||
default: Option<toml::Value>, | ||||
#[serde(rename = "default-type")] | ||||
default_type: Option<String>, | ||||
#[serde(default)] | ||||
priority: isize, | ||||
#[serde(default)] | ||||
generic: bool, | ||||
#[serde(default)] | ||||
alias: Vec<(String, String)>, | ||||
#[serde(default)] | ||||
experimental: bool, | ||||
#[serde(default)] | ||||
documentation: String, | ||||
} | ||||
impl TemplateItem { | ||||
fn into_default_item( | ||||
self, | ||||
application: TemplateApplication, | ||||
) -> DefaultConfigItem { | ||||
DefaultConfigItem { | ||||
section: application.section, | ||||
name: application | ||||
.prefix | ||||
.map(|prefix| format!("{}.{}", prefix, self.suffix)) | ||||
.unwrap_or(self.suffix), | ||||
default: self.default, | ||||
priority: self.priority, | ||||
alias: self.alias, | ||||
experimental: self.experimental, | ||||
documentation: self.documentation, | ||||
} | ||||
} | ||||
} | ||||
impl TryFrom<RawTemplateItem> for TemplateItem { | ||||
type Error = HgError; | ||||
fn try_from(value: RawTemplateItem) -> Result<Self, Self::Error> { | ||||
Ok(Self { | ||||
suffix: value.suffix, | ||||
default: raw_default_to_concrete( | ||||
value.default_type, | ||||
value.default, | ||||
)?, | ||||
priority: if value.generic { | ||||
Some(value.priority) | ||||
} else { | ||||
None | ||||
}, | ||||
alias: value.alias, | ||||
experimental: value.experimental, | ||||
documentation: value.documentation, | ||||
}) | ||||
} | ||||
} | ||||
/// Transforms the on-disk string-based representation of complex default types | ||||
/// to the concrete [`DefaultconfigItemType`]. | ||||
fn raw_default_to_concrete( | ||||
default_type: Option<String>, | ||||
default: Option<toml::Value>, | ||||
) -> Result<Option<DefaultConfigItemType>, HgError> { | ||||
Ok(match default_type.as_deref() { | ||||
None => default.as_ref().map(|default| { | ||||
DefaultConfigItemType::Primitive(default.to_owned()) | ||||
}), | ||||
Some("dynamic") => Some(DefaultConfigItemType::Dynamic), | ||||
Some("list_type") => Some(DefaultConfigItemType::ListType), | ||||
Some("lambda") => match &default { | ||||
Some(default) => Some(DefaultConfigItemType::Lambda( | ||||
default.to_owned().try_into().map_err(|e| { | ||||
HgError::abort( | ||||
e.to_string(), | ||||
exit_codes::ABORT, | ||||
Some("Check 'mercurial/configitems.toml'".into()), | ||||
) | ||||
})?, | ||||
)), | ||||
None => { | ||||
return Err(HgError::abort( | ||||
"lambda defined with no return value".to_string(), | ||||
exit_codes::ABORT, | ||||
Some("Check 'mercurial/configitems.toml'".into()), | ||||
)) | ||||
} | ||||
}, | ||||
Some("lazy_module") => match &default { | ||||
Some(default) => { | ||||
Some(DefaultConfigItemType::LazyModule(match default { | ||||
toml::Value::String(module) => module.to_owned(), | ||||
_ => { | ||||
return Err(HgError::abort( | ||||
"lazy_module module name should be a string" | ||||
.to_string(), | ||||
exit_codes::ABORT, | ||||
Some("Check 'mercurial/configitems.toml'".into()), | ||||
)) | ||||
} | ||||
})) | ||||
} | ||||
None => { | ||||
return Err(HgError::abort( | ||||
"lazy_module should have a default value".to_string(), | ||||
exit_codes::ABORT, | ||||
Some("Check 'mercurial/configitems.toml'".into()), | ||||
)) | ||||
} | ||||
}, | ||||
Some(invalid) => { | ||||
return Err(HgError::abort( | ||||
format!("invalid default_type '{}'", invalid), | ||||
exit_codes::ABORT, | ||||
Some("Check 'mercurial/configitems.toml'".into()), | ||||
)) | ||||
} | ||||
}) | ||||
} | ||||
#[derive(Debug, Clone, Deserialize)] | ||||
struct TemplateApplication { | ||||
template: String, | ||||
section: String, | ||||
#[serde(default)] | ||||
prefix: Option<String>, | ||||
} | ||||
/// Represents the (dynamic) set of default core Mercurial config items from | ||||
/// `mercurial/configitems.toml`. | ||||
#[derive(Clone, Debug, Default)] | ||||
pub struct DefaultConfig { | ||||
/// Mapping of section -> (mapping of name -> item) | ||||
items: FastHashMap<String, FastHashMap<String, DefaultConfigItem>>, | ||||
} | ||||
impl DefaultConfig { | ||||
pub fn empty() -> DefaultConfig { | ||||
Self { | ||||
items: Default::default(), | ||||
} | ||||
} | ||||
/// Returns `Self`, given the contents of `mercurial/configitems.toml` | ||||
#[logging_timer::time("trace")] | ||||
pub fn from_contents(contents: &str) -> Result<Self, HgError> { | ||||
let mut from_file: ConfigItems = | ||||
toml::from_str(contents).map_err(|e| { | ||||
HgError::abort( | ||||
e.to_string(), | ||||
exit_codes::ABORT, | ||||
Some("Check 'mercurial/configitems.toml'".into()), | ||||
) | ||||
})?; | ||||
let mut flat_items = from_file.items; | ||||
for application in from_file.template_applications.drain(..) { | ||||
match from_file.templates.get(&application.template) { | ||||
None => return Err( | ||||
HgError::abort( | ||||
format!( | ||||
"template application refers to undefined template '{}'", | ||||
application.template | ||||
), | ||||
exit_codes::ABORT, | ||||
Some("Check 'mercurial/configitems.toml'".into()) | ||||
) | ||||
), | ||||
Some(template_items) => { | ||||
for template_item in template_items { | ||||
flat_items.push( | ||||
template_item | ||||
.clone() | ||||
.into_default_item(application.clone()), | ||||
) | ||||
} | ||||
} | ||||
}; | ||||
} | ||||
let items = flat_items.into_iter().fold( | ||||
FastHashMap::default(), | ||||
|mut acc, item| { | ||||
acc.entry(item.section.to_owned()) | ||||
.or_insert_with(|| { | ||||
let mut section = FastHashMap::default(); | ||||
section.insert(item.name.to_owned(), item.to_owned()); | ||||
section | ||||
}) | ||||
.insert(item.name.to_owned(), item); | ||||
acc | ||||
}, | ||||
); | ||||
Ok(Self { items }) | ||||
} | ||||
/// Return the default config item that matches `section` and `item`. | ||||
pub fn get( | ||||
&self, | ||||
section: &[u8], | ||||
item: &[u8], | ||||
) -> Option<&DefaultConfigItem> { | ||||
// Core items must be valid UTF-8 | ||||
let section = String::from_utf8_lossy(section); | ||||
let section_map = self.items.get(section.as_ref())?; | ||||
let item_name_lossy = String::from_utf8_lossy(item); | ||||
match section_map.get(item_name_lossy.as_ref()) { | ||||
Some(item) => Some(item), | ||||
None => { | ||||
for generic_item in section_map | ||||
.values() | ||||
.filter(|item| item.is_generic()) | ||||
.sorted_by_key(|item| match item.priority { | ||||
Some(priority) => (priority, &item.name), | ||||
_ => unreachable!(), | ||||
}) | ||||
{ | ||||
if regex::bytes::Regex::new(&generic_item.name) | ||||
.expect("invalid regex in configitems") | ||||
.is_match(item) | ||||
{ | ||||
return Some(generic_item); | ||||
} | ||||
} | ||||
None | ||||
} | ||||
} | ||||
} | ||||
} | ||||
#[cfg(test)] | ||||
mod tests { | ||||
use crate::config::config_items::{ | ||||
DefaultConfigItem, DefaultConfigItemType, | ||||
}; | ||||
use super::DefaultConfig; | ||||
#[test] | ||||
fn test_config_read() { | ||||
let contents = r#" | ||||
[[items]] | ||||
section = "alias" | ||||
name = "abcd.*" | ||||
default = 3 | ||||
generic = true | ||||
priority = -1 | ||||
[[items]] | ||||
section = "alias" | ||||
name = ".*" | ||||
default-type = "dynamic" | ||||
generic = true | ||||
[[items]] | ||||
section = "cmdserver" | ||||
name = "track-log" | ||||
default-type = "lambda" | ||||
default = [ "chgserver", "cmdserver", "repocache",] | ||||
[[items]] | ||||
section = "chgserver" | ||||
name = "idletimeout" | ||||
default = 3600 | ||||
[[items]] | ||||
section = "cmdserver" | ||||
name = "message-encodings" | ||||
default-type = "list_type" | ||||
[[items]] | ||||
section = "web" | ||||
name = "encoding" | ||||
default-type = "lazy_module" | ||||
default = "encoding.encoding" | ||||
[[items]] | ||||
section = "command-templates" | ||||
name = "graphnode" | ||||
alias = [["ui", "graphnodetemplate"]] | ||||
documentation = """This is a docstring. | ||||
This is another line \ | ||||
but this is not.""" | ||||
[[items]] | ||||
section = "censor" | ||||
name = "policy" | ||||
default = "abort" | ||||
experimental = true | ||||
[[template-applications]] | ||||
template = "diff-options" | ||||
section = "commands" | ||||
prefix = "revert.interactive" | ||||
[[template-applications]] | ||||
template = "diff-options" | ||||
section = "diff" | ||||
[templates] | ||||
[[templates.diff-options]] | ||||
suffix = "nodates" | ||||
default = false | ||||
[[templates.diff-options]] | ||||
suffix = "showfunc" | ||||
default = false | ||||
[[templates.diff-options]] | ||||
suffix = "unified" | ||||
"#; | ||||
let res = DefaultConfig::from_contents(contents); | ||||
let config = match res { | ||||
Ok(config) => config, | ||||
Err(e) => panic!("{}", e), | ||||
}; | ||||
let expected = DefaultConfigItem { | ||||
section: "censor".into(), | ||||
name: "policy".into(), | ||||
default: Some(DefaultConfigItemType::Primitive("abort".into())), | ||||
priority: None, | ||||
alias: vec![], | ||||
experimental: true, | ||||
documentation: "".into(), | ||||
}; | ||||
assert_eq!(config.get(b"censor", b"policy"), Some(&expected)); | ||||
// Test generic priority. The `.*` pattern is wider than `abcd.*`, but | ||||
// `abcd.*` has priority, so it should match first. | ||||
let expected = DefaultConfigItem { | ||||
section: "alias".into(), | ||||
name: "abcd.*".into(), | ||||
default: Some(DefaultConfigItemType::Primitive(3.into())), | ||||
priority: Some(-1), | ||||
alias: vec![], | ||||
experimental: false, | ||||
documentation: "".into(), | ||||
}; | ||||
assert_eq!(config.get(b"alias", b"abcdsomething"), Some(&expected)); | ||||
//... but if it doesn't, we should fallback to `.*` | ||||
let expected = DefaultConfigItem { | ||||
section: "alias".into(), | ||||
name: ".*".into(), | ||||
default: Some(DefaultConfigItemType::Dynamic), | ||||
priority: Some(0), | ||||
alias: vec![], | ||||
experimental: false, | ||||
documentation: "".into(), | ||||
}; | ||||
assert_eq!(config.get(b"alias", b"something"), Some(&expected)); | ||||
let expected = DefaultConfigItem { | ||||
section: "chgserver".into(), | ||||
name: "idletimeout".into(), | ||||
default: Some(DefaultConfigItemType::Primitive(3600.into())), | ||||
priority: None, | ||||
alias: vec![], | ||||
experimental: false, | ||||
documentation: "".into(), | ||||
}; | ||||
assert_eq!(config.get(b"chgserver", b"idletimeout"), Some(&expected)); | ||||
let expected = DefaultConfigItem { | ||||
section: "cmdserver".into(), | ||||
name: "track-log".into(), | ||||
default: Some(DefaultConfigItemType::Lambda(vec![ | ||||
"chgserver".into(), | ||||
"cmdserver".into(), | ||||
"repocache".into(), | ||||
])), | ||||
priority: None, | ||||
alias: vec![], | ||||
experimental: false, | ||||
documentation: "".into(), | ||||
}; | ||||
assert_eq!(config.get(b"cmdserver", b"track-log"), Some(&expected)); | ||||
let expected = DefaultConfigItem { | ||||
section: "command-templates".into(), | ||||
name: "graphnode".into(), | ||||
default: None, | ||||
priority: None, | ||||
alias: vec![("ui".into(), "graphnodetemplate".into())], | ||||
experimental: false, | ||||
documentation: | ||||
"This is a docstring.\nThis is another line but this is not." | ||||
.into(), | ||||
}; | ||||
assert_eq!( | ||||
config.get(b"command-templates", b"graphnode"), | ||||
Some(&expected) | ||||
); | ||||
} | ||||
} | ||||