comparison rust/hg-core/src/config/config_items.rs @ 50802:f8412da86d05

rust-config: add support for default config items Now that configitems.toml exists, we can read from it the default values for all core config items. We will add the devel-warning for use of undeclared config items in a later patch when we're done adding the missing entries for `rhg`.
author Rapha?l Gom?s <rgomes@octobus.net>
date Thu, 06 Jul 2023 14:32:07 +0200
parents
children 7f8f6fe13fa9
comparison
equal deleted inserted replaced
50801:c51b178b0b7e 50802:f8412da86d05
1 //! Code for parsing default Mercurial config items.
2 use itertools::Itertools;
3 use serde::Deserialize;
4
5 use crate::{errors::HgError, exit_codes, FastHashMap};
6
7 /// Corresponds to the structure of `mercurial/configitems.toml`.
8 #[derive(Debug, Deserialize)]
9 pub struct ConfigItems {
10 items: Vec<DefaultConfigItem>,
11 templates: FastHashMap<String, Vec<TemplateItem>>,
12 #[serde(rename = "template-applications")]
13 template_applications: Vec<TemplateApplication>,
14 }
15
16 /// Corresponds to a config item declaration in `mercurial/configitems.toml`.
17 #[derive(Clone, Debug, PartialEq, Deserialize)]
18 #[serde(try_from = "RawDefaultConfigItem")]
19 pub struct DefaultConfigItem {
20 /// Section of the config the item is in (e.g. `[merge-tools]`)
21 section: String,
22 /// Name of the item (e.g. `meld.gui`)
23 name: String,
24 /// Default value (can be dynamic, see [`DefaultConfigItemType`])
25 default: Option<DefaultConfigItemType>,
26 /// If the config option is generic (e.g. `merge-tools.*`), defines
27 /// the priority of this item relative to other generic items.
28 /// If we're looking for <pattern>, then all generic items within the same
29 /// section will be sorted by order of priority, and the first regex match
30 /// against `name` is returned.
31 #[serde(default)]
32 priority: Option<isize>,
33 /// Aliases, if any. Each alias is a tuple of `(section, name)` for each
34 /// option that is aliased to this one.
35 #[serde(default)]
36 alias: Vec<(String, String)>,
37 /// Whether the config item is marked as experimental
38 #[serde(default)]
39 experimental: bool,
40 /// The (possibly empty) docstring for the item
41 #[serde(default)]
42 documentation: String,
43 }
44
45 /// Corresponds to the raw (i.e. on disk) structure of config items. Used as
46 /// an intermediate step in deserialization.
47 #[derive(Clone, Debug, Deserialize)]
48 struct RawDefaultConfigItem {
49 section: String,
50 name: String,
51 default: Option<toml::Value>,
52 #[serde(rename = "default-type")]
53 default_type: Option<String>,
54 #[serde(default)]
55 priority: isize,
56 #[serde(default)]
57 generic: bool,
58 #[serde(default)]
59 alias: Vec<(String, String)>,
60 #[serde(default)]
61 experimental: bool,
62 #[serde(default)]
63 documentation: String,
64 }
65
66 impl TryFrom<RawDefaultConfigItem> for DefaultConfigItem {
67 type Error = HgError;
68
69 fn try_from(value: RawDefaultConfigItem) -> Result<Self, Self::Error> {
70 Ok(Self {
71 section: value.section,
72 name: value.name,
73 default: raw_default_to_concrete(
74 value.default_type,
75 value.default,
76 )?,
77 priority: if value.generic {
78 Some(value.priority)
79 } else {
80 None
81 },
82 alias: value.alias,
83 experimental: value.experimental,
84 documentation: value.documentation,
85 })
86 }
87 }
88
89 impl DefaultConfigItem {
90 fn is_generic(&self) -> bool {
91 self.priority.is_some()
92 }
93 }
94
95 impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a str> {
96 type Error = HgError;
97
98 fn try_from(
99 value: &'a DefaultConfigItem,
100 ) -> Result<Option<&'a str>, Self::Error> {
101 match &value.default {
102 Some(default) => {
103 let err = HgError::abort(
104 format!(
105 "programming error: wrong query on config item '{}.{}'",
106 value.section,
107 value.name
108 ),
109 exit_codes::ABORT,
110 Some(format!(
111 "asked for '&str', type of default is '{}'",
112 default.type_str()
113 )),
114 );
115 match default {
116 DefaultConfigItemType::Primitive(toml::Value::String(
117 s,
118 )) => Ok(Some(s)),
119 _ => Err(err),
120 }
121 }
122 None => Ok(None),
123 }
124 }
125 }
126
127 impl TryFrom<&DefaultConfigItem> for Option<bool> {
128 type Error = HgError;
129
130 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
131 match &value.default {
132 Some(default) => {
133 let err = HgError::abort(
134 format!(
135 "programming error: wrong query on config item '{}.{}'",
136 value.section,
137 value.name
138 ),
139 exit_codes::ABORT,
140 Some(format!(
141 "asked for 'bool', type of default is '{}'",
142 default.type_str()
143 )),
144 );
145 match default {
146 DefaultConfigItemType::Primitive(
147 toml::Value::Boolean(b),
148 ) => Ok(Some(*b)),
149 _ => Err(err),
150 }
151 }
152 None => Ok(Some(false)),
153 }
154 }
155 }
156
157 impl TryFrom<&DefaultConfigItem> for Option<u32> {
158 type Error = HgError;
159
160 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
161 match &value.default {
162 Some(default) => {
163 let err = HgError::abort(
164 format!(
165 "programming error: wrong query on config item '{}.{}'",
166 value.section,
167 value.name
168 ),
169 exit_codes::ABORT,
170 Some(format!(
171 "asked for 'u32', type of default is '{}'",
172 default.type_str()
173 )),
174 );
175 match default {
176 DefaultConfigItemType::Primitive(
177 toml::Value::Integer(b),
178 ) => {
179 Ok(Some((*b).try_into().expect("TOML integer to u32")))
180 }
181 _ => Err(err),
182 }
183 }
184 None => Ok(None),
185 }
186 }
187 }
188
189 impl TryFrom<&DefaultConfigItem> for Option<u64> {
190 type Error = HgError;
191
192 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
193 match &value.default {
194 Some(default) => {
195 let err = HgError::abort(
196 format!(
197 "programming error: wrong query on config item '{}.{}'",
198 value.section,
199 value.name
200 ),
201 exit_codes::ABORT,
202 Some(format!(
203 "asked for 'u64', type of default is '{}'",
204 default.type_str()
205 )),
206 );
207 match default {
208 DefaultConfigItemType::Primitive(
209 toml::Value::Integer(b),
210 ) => {
211 Ok(Some((*b).try_into().expect("TOML integer to u64")))
212 }
213 _ => Err(err),
214 }
215 }
216 None => Ok(None),
217 }
218 }
219 }
220
221 /// Allows abstracting over more complex default values than just primitives.
222 /// The former `configitems.py` contained some dynamic code that is encoded
223 /// in this enum.
224 #[derive(Debug, PartialEq, Clone, Deserialize)]
225 pub enum DefaultConfigItemType {
226 /// Some primitive type (string, integer, boolean)
227 Primitive(toml::Value),
228 /// A dynamic value that will be given by the code at runtime
229 Dynamic,
230 /// An lazily-returned array (possibly only relevant in the Python impl)
231 /// Example: `lambda: [b"zstd", b"zlib"]`
232 Lambda(Vec<String>),
233 /// For now, a special case for `web.encoding` that points to the
234 /// `encoding.encoding` module in the Python impl so that local encoding
235 /// is correctly resolved at runtime
236 LazyModule(String),
237 ListType,
238 }
239
240 impl DefaultConfigItemType {
241 pub fn type_str(&self) -> &str {
242 match self {
243 DefaultConfigItemType::Primitive(primitive) => {
244 primitive.type_str()
245 }
246 DefaultConfigItemType::Dynamic => "dynamic",
247 DefaultConfigItemType::Lambda(_) => "lambda",
248 DefaultConfigItemType::LazyModule(_) => "lazy_module",
249 DefaultConfigItemType::ListType => "list_type",
250 }
251 }
252 }
253
254 /// Most of the fields are shared with [`DefaultConfigItem`].
255 #[derive(Debug, Clone, Deserialize)]
256 #[serde(try_from = "RawTemplateItem")]
257 struct TemplateItem {
258 suffix: String,
259 default: Option<DefaultConfigItemType>,
260 priority: Option<isize>,
261 #[serde(default)]
262 alias: Vec<(String, String)>,
263 #[serde(default)]
264 experimental: bool,
265 #[serde(default)]
266 documentation: String,
267 }
268
269 /// Corresponds to the raw (i.e. on disk) representation of a template item.
270 /// Used as an intermediate step in deserialization.
271 #[derive(Clone, Debug, Deserialize)]
272 struct RawTemplateItem {
273 suffix: String,
274 default: Option<toml::Value>,
275 #[serde(rename = "default-type")]
276 default_type: Option<String>,
277 #[serde(default)]
278 priority: isize,
279 #[serde(default)]
280 generic: bool,
281 #[serde(default)]
282 alias: Vec<(String, String)>,
283 #[serde(default)]
284 experimental: bool,
285 #[serde(default)]
286 documentation: String,
287 }
288
289 impl TemplateItem {
290 fn into_default_item(
291 self,
292 application: TemplateApplication,
293 ) -> DefaultConfigItem {
294 DefaultConfigItem {
295 section: application.section,
296 name: application
297 .prefix
298 .map(|prefix| format!("{}.{}", prefix, self.suffix))
299 .unwrap_or(self.suffix),
300 default: self.default,
301 priority: self.priority,
302 alias: self.alias,
303 experimental: self.experimental,
304 documentation: self.documentation,
305 }
306 }
307 }
308
309 impl TryFrom<RawTemplateItem> for TemplateItem {
310 type Error = HgError;
311
312 fn try_from(value: RawTemplateItem) -> Result<Self, Self::Error> {
313 Ok(Self {
314 suffix: value.suffix,
315 default: raw_default_to_concrete(
316 value.default_type,
317 value.default,
318 )?,
319 priority: if value.generic {
320 Some(value.priority)
321 } else {
322 None
323 },
324 alias: value.alias,
325 experimental: value.experimental,
326 documentation: value.documentation,
327 })
328 }
329 }
330
331 /// Transforms the on-disk string-based representation of complex default types
332 /// to the concrete [`DefaultconfigItemType`].
333 fn raw_default_to_concrete(
334 default_type: Option<String>,
335 default: Option<toml::Value>,
336 ) -> Result<Option<DefaultConfigItemType>, HgError> {
337 Ok(match default_type.as_deref() {
338 None => default.as_ref().map(|default| {
339 DefaultConfigItemType::Primitive(default.to_owned())
340 }),
341 Some("dynamic") => Some(DefaultConfigItemType::Dynamic),
342 Some("list_type") => Some(DefaultConfigItemType::ListType),
343 Some("lambda") => match &default {
344 Some(default) => Some(DefaultConfigItemType::Lambda(
345 default.to_owned().try_into().map_err(|e| {
346 HgError::abort(
347 e.to_string(),
348 exit_codes::ABORT,
349 Some("Check 'mercurial/configitems.toml'".into()),
350 )
351 })?,
352 )),
353 None => {
354 return Err(HgError::abort(
355 "lambda defined with no return value".to_string(),
356 exit_codes::ABORT,
357 Some("Check 'mercurial/configitems.toml'".into()),
358 ))
359 }
360 },
361 Some("lazy_module") => match &default {
362 Some(default) => {
363 Some(DefaultConfigItemType::LazyModule(match default {
364 toml::Value::String(module) => module.to_owned(),
365 _ => {
366 return Err(HgError::abort(
367 "lazy_module module name should be a string"
368 .to_string(),
369 exit_codes::ABORT,
370 Some("Check 'mercurial/configitems.toml'".into()),
371 ))
372 }
373 }))
374 }
375 None => {
376 return Err(HgError::abort(
377 "lazy_module should have a default value".to_string(),
378 exit_codes::ABORT,
379 Some("Check 'mercurial/configitems.toml'".into()),
380 ))
381 }
382 },
383 Some(invalid) => {
384 return Err(HgError::abort(
385 format!("invalid default_type '{}'", invalid),
386 exit_codes::ABORT,
387 Some("Check 'mercurial/configitems.toml'".into()),
388 ))
389 }
390 })
391 }
392
393 #[derive(Debug, Clone, Deserialize)]
394 struct TemplateApplication {
395 template: String,
396 section: String,
397 #[serde(default)]
398 prefix: Option<String>,
399 }
400
401 /// Represents the (dynamic) set of default core Mercurial config items from
402 /// `mercurial/configitems.toml`.
403 #[derive(Clone, Debug, Default)]
404 pub struct DefaultConfig {
405 /// Mapping of section -> (mapping of name -> item)
406 items: FastHashMap<String, FastHashMap<String, DefaultConfigItem>>,
407 }
408
409 impl DefaultConfig {
410 pub fn empty() -> DefaultConfig {
411 Self {
412 items: Default::default(),
413 }
414 }
415
416 /// Returns `Self`, given the contents of `mercurial/configitems.toml`
417 #[logging_timer::time("trace")]
418 pub fn from_contents(contents: &str) -> Result<Self, HgError> {
419 let mut from_file: ConfigItems =
420 toml::from_str(contents).map_err(|e| {
421 HgError::abort(
422 e.to_string(),
423 exit_codes::ABORT,
424 Some("Check 'mercurial/configitems.toml'".into()),
425 )
426 })?;
427
428 let mut flat_items = from_file.items;
429
430 for application in from_file.template_applications.drain(..) {
431 match from_file.templates.get(&application.template) {
432 None => return Err(
433 HgError::abort(
434 format!(
435 "template application refers to undefined template '{}'",
436 application.template
437 ),
438 exit_codes::ABORT,
439 Some("Check 'mercurial/configitems.toml'".into())
440 )
441 ),
442 Some(template_items) => {
443 for template_item in template_items {
444 flat_items.push(
445 template_item
446 .clone()
447 .into_default_item(application.clone()),
448 )
449 }
450 }
451 };
452 }
453
454 let items = flat_items.into_iter().fold(
455 FastHashMap::default(),
456 |mut acc, item| {
457 acc.entry(item.section.to_owned())
458 .or_insert_with(|| {
459 let mut section = FastHashMap::default();
460 section.insert(item.name.to_owned(), item.to_owned());
461 section
462 })
463 .insert(item.name.to_owned(), item);
464 acc
465 },
466 );
467
468 Ok(Self { items })
469 }
470
471 /// Return the default config item that matches `section` and `item`.
472 pub fn get(
473 &self,
474 section: &[u8],
475 item: &[u8],
476 ) -> Option<&DefaultConfigItem> {
477 // Core items must be valid UTF-8
478 let section = String::from_utf8_lossy(section);
479 let section_map = self.items.get(section.as_ref())?;
480 let item_name_lossy = String::from_utf8_lossy(item);
481 match section_map.get(item_name_lossy.as_ref()) {
482 Some(item) => Some(item),
483 None => {
484 for generic_item in section_map
485 .values()
486 .filter(|item| item.is_generic())
487 .sorted_by_key(|item| match item.priority {
488 Some(priority) => (priority, &item.name),
489 _ => unreachable!(),
490 })
491 {
492 if regex::bytes::Regex::new(&generic_item.name)
493 .expect("invalid regex in configitems")
494 .is_match(item)
495 {
496 return Some(generic_item);
497 }
498 }
499 None
500 }
501 }
502 }
503 }
504
505 #[cfg(test)]
506 mod tests {
507 use crate::config::config_items::{
508 DefaultConfigItem, DefaultConfigItemType,
509 };
510
511 use super::DefaultConfig;
512
513 #[test]
514 fn test_config_read() {
515 let contents = r#"
516 [[items]]
517 section = "alias"
518 name = "abcd.*"
519 default = 3
520 generic = true
521 priority = -1
522
523 [[items]]
524 section = "alias"
525 name = ".*"
526 default-type = "dynamic"
527 generic = true
528
529 [[items]]
530 section = "cmdserver"
531 name = "track-log"
532 default-type = "lambda"
533 default = [ "chgserver", "cmdserver", "repocache",]
534
535 [[items]]
536 section = "chgserver"
537 name = "idletimeout"
538 default = 3600
539
540 [[items]]
541 section = "cmdserver"
542 name = "message-encodings"
543 default-type = "list_type"
544
545 [[items]]
546 section = "web"
547 name = "encoding"
548 default-type = "lazy_module"
549 default = "encoding.encoding"
550
551 [[items]]
552 section = "command-templates"
553 name = "graphnode"
554 alias = [["ui", "graphnodetemplate"]]
555 documentation = """This is a docstring.
556 This is another line \
557 but this is not."""
558
559 [[items]]
560 section = "censor"
561 name = "policy"
562 default = "abort"
563 experimental = true
564
565 [[template-applications]]
566 template = "diff-options"
567 section = "commands"
568 prefix = "revert.interactive"
569
570 [[template-applications]]
571 template = "diff-options"
572 section = "diff"
573
574 [templates]
575 [[templates.diff-options]]
576 suffix = "nodates"
577 default = false
578
579 [[templates.diff-options]]
580 suffix = "showfunc"
581 default = false
582
583 [[templates.diff-options]]
584 suffix = "unified"
585 "#;
586 let res = DefaultConfig::from_contents(contents);
587 let config = match res {
588 Ok(config) => config,
589 Err(e) => panic!("{}", e),
590 };
591 let expected = DefaultConfigItem {
592 section: "censor".into(),
593 name: "policy".into(),
594 default: Some(DefaultConfigItemType::Primitive("abort".into())),
595 priority: None,
596 alias: vec![],
597 experimental: true,
598 documentation: "".into(),
599 };
600 assert_eq!(config.get(b"censor", b"policy"), Some(&expected));
601
602 // Test generic priority. The `.*` pattern is wider than `abcd.*`, but
603 // `abcd.*` has priority, so it should match first.
604 let expected = DefaultConfigItem {
605 section: "alias".into(),
606 name: "abcd.*".into(),
607 default: Some(DefaultConfigItemType::Primitive(3.into())),
608 priority: Some(-1),
609 alias: vec![],
610 experimental: false,
611 documentation: "".into(),
612 };
613 assert_eq!(config.get(b"alias", b"abcdsomething"), Some(&expected));
614
615 //... but if it doesn't, we should fallback to `.*`
616 let expected = DefaultConfigItem {
617 section: "alias".into(),
618 name: ".*".into(),
619 default: Some(DefaultConfigItemType::Dynamic),
620 priority: Some(0),
621 alias: vec![],
622 experimental: false,
623 documentation: "".into(),
624 };
625 assert_eq!(config.get(b"alias", b"something"), Some(&expected));
626
627 let expected = DefaultConfigItem {
628 section: "chgserver".into(),
629 name: "idletimeout".into(),
630 default: Some(DefaultConfigItemType::Primitive(3600.into())),
631 priority: None,
632 alias: vec![],
633 experimental: false,
634 documentation: "".into(),
635 };
636 assert_eq!(config.get(b"chgserver", b"idletimeout"), Some(&expected));
637
638 let expected = DefaultConfigItem {
639 section: "cmdserver".into(),
640 name: "track-log".into(),
641 default: Some(DefaultConfigItemType::Lambda(vec![
642 "chgserver".into(),
643 "cmdserver".into(),
644 "repocache".into(),
645 ])),
646 priority: None,
647 alias: vec![],
648 experimental: false,
649 documentation: "".into(),
650 };
651 assert_eq!(config.get(b"cmdserver", b"track-log"), Some(&expected));
652
653 let expected = DefaultConfigItem {
654 section: "command-templates".into(),
655 name: "graphnode".into(),
656 default: None,
657 priority: None,
658 alias: vec![("ui".into(), "graphnodetemplate".into())],
659 experimental: false,
660 documentation:
661 "This is a docstring.\nThis is another line but this is not."
662 .into(),
663 };
664 assert_eq!(
665 config.get(b"command-templates", b"graphnode"),
666 Some(&expected)
667 );
668 }
669 }