1use std::{
4 any,
5 borrow::Cow,
6 collections::{BTreeMap, BTreeSet, HashMap},
7 iter,
8};
9
10use anyhow::Context;
11
12use self::mount::{MountingPoint, MountingPoints};
13use crate::{
14 metadata::{
15 AliasOptions, BasicTypes, ConfigMetadata, ConfigShorthand, ConfigVariant,
16 NestedConfigMetadata, ParamMetadata,
17 },
18 utils::EnumVariant,
19 value::Pointer,
20};
21
22mod mount;
23#[cfg(test)]
24mod tests;
25
26#[derive(Debug, Clone, Copy)]
27struct ParentLink {
28 parent_ty: any::TypeId,
29 this_ref: &'static NestedConfigMetadata,
30}
31
32#[derive(Debug, Clone)]
33pub(crate) struct ConfigData {
34 pub(crate) metadata: &'static ConfigMetadata,
35 parent_link: Option<ParentLink>,
36 pub(crate) is_top_level: bool,
37 pub(crate) coerce_serde_enums: bool,
38 all_paths: Vec<(Cow<'static, str>, AliasOptions)>,
39}
40
41impl ConfigData {
42 pub(crate) fn prefix(&self) -> Pointer<'_> {
43 Pointer(self.all_paths[0].0.as_ref())
44 }
45
46 pub(crate) fn shorthand(&self) -> Option<ConfigShorthand> {
48 let is_flattened = self
49 .parent_link
50 .as_ref()
51 .is_some_and(|link| link.this_ref.name.is_empty());
52 if is_flattened {
53 return None;
54 }
55 self.metadata.tag.as_ref()?.shorthand
56 }
57
58 pub(crate) fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
59 self.all_paths
60 .iter()
61 .skip(1)
62 .map(|(path, options)| (path.as_ref(), *options))
63 }
64
65 pub(crate) fn all_paths_for_param(
66 &self,
67 param: &'static ParamMetadata,
68 ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
69 self.all_paths_for_child(param.name, param.aliases, param.tag_variant)
70 }
71
72 fn all_paths_for_child(
73 &self,
74 name: &'static str,
75 aliases: &'static [(&'static str, AliasOptions)],
76 tag_variant: Option<&'static ConfigVariant>,
77 ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
78 let local_names =
79 iter::once((name, AliasOptions::default())).chain(aliases.iter().copied());
80
81 let enum_names = if let (true, Some(variant)) = (self.coerce_serde_enums, tag_variant) {
82 let variant_names = iter::once(variant.name)
83 .chain(variant.aliases.iter().copied())
84 .filter_map(|name| Some(EnumVariant::new(name)?.to_snake_case()));
85 let local_names_ = local_names.clone();
86 let paths = variant_names.flat_map(move |variant_name| {
87 local_names_
88 .clone()
89 .filter_map(move |(name_or_path, options)| {
90 if name_or_path.starts_with('.') {
91 return None;
93 }
94 let full_path = Pointer(&variant_name).join(name_or_path);
95 Some((Cow::Owned(full_path), options))
96 })
97 });
98 Some(paths)
99 } else {
100 None
101 };
102 let enum_names = enum_names.into_iter().flatten();
103 let local_names = local_names
104 .map(|(name, options)| (Cow::Borrowed(name), options))
105 .chain(enum_names);
106
107 self.all_paths
108 .iter()
109 .flat_map(move |(alias, config_options)| {
110 local_names
111 .clone()
112 .filter_map(move |(name_or_path, options)| {
113 let full_path = Pointer(alias).join_path(Pointer(&name_or_path))?;
114 Some((full_path, options.combine(*config_options)))
115 })
116 })
117 }
118}
119
120#[derive(Debug, Clone, Copy)]
122pub struct ConfigRef<'a> {
123 schema: &'a ConfigSchema,
124 prefix: &'a str,
125 pub(crate) data: &'a ConfigData,
126}
127
128impl<'a> ConfigRef<'a> {
129 pub fn prefix(&self) -> &'a str {
131 self.prefix
132 }
133
134 pub fn metadata(&self) -> &'static ConfigMetadata {
136 self.data.metadata
137 }
138
139 pub fn is_top_level(&self) -> bool {
141 self.data.parent_link.is_none()
142 }
143
144 pub fn shorthand(&self) -> Option<ConfigShorthand> {
147 self.data.shorthand()
148 }
149
150 #[doc(hidden)] pub fn parent_link(&self) -> Option<(Self, &'static NestedConfigMetadata)> {
152 let link = self.data.parent_link?;
153 let parent_prefix = if link.this_ref.name.is_empty() {
154 self.prefix
156 } else {
157 let (parent, _) = Pointer(self.prefix).split_last().unwrap();
158 parent.0
159 };
160 let parent_ref = Self {
161 schema: self.schema,
162 prefix: parent_prefix,
163 data: self.schema.get_ll(parent_prefix, link.parent_ty)?,
164 };
165 Some((parent_ref, link.this_ref))
166 }
167
168 pub fn aliases(&self) -> impl Iterator<Item = (&'a str, AliasOptions)> + '_ {
170 self.data.aliases()
171 }
172
173 #[doc(hidden)] pub fn all_paths_for_param(
177 &self,
178 param: &'static ParamMetadata,
179 ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
180 self.data.all_paths_for_param(param)
181 }
182}
183
184#[derive(Debug)]
186pub struct ConfigMut<'a> {
187 schema: &'a mut ConfigSchema,
188 prefix: String,
189 type_id: any::TypeId,
190}
191
192impl ConfigMut<'_> {
193 pub fn prefix(&self) -> &str {
195 &self.prefix
196 }
197
198 pub fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
200 let data = &self.schema.configs[self.prefix.as_str()].inner[&self.type_id];
201 data.aliases()
202 }
203
204 pub fn push_alias(self, alias: &'static str) -> anyhow::Result<Self> {
211 self.push_alias_inner(alias, AliasOptions::new())
212 }
213
214 pub fn push_deprecated_alias(self, alias: &'static str) -> anyhow::Result<Self> {
221 self.push_alias_inner(
222 alias,
223 AliasOptions {
224 is_deprecated: true,
225 },
226 )
227 }
228
229 fn push_alias_inner(self, alias: &'static str, options: AliasOptions) -> anyhow::Result<Self> {
230 let mut patched = PatchedSchema::new(self.schema);
231 patched.insert_alias(self.prefix.clone(), self.type_id, Pointer(alias), options)?;
232 patched.commit();
233 Ok(self)
234 }
235}
236
237#[derive(Debug, Clone, Default)]
238struct ConfigsForPrefix {
239 inner: HashMap<any::TypeId, ConfigData>,
240 by_depth: BTreeSet<(usize, any::TypeId)>,
241}
242
243impl ConfigsForPrefix {
244 fn by_depth(&self) -> impl Iterator<Item = &ConfigData> + '_ {
245 self.by_depth.iter().map(|(_, ty)| &self.inner[ty])
246 }
247
248 fn insert(&mut self, ty: any::TypeId, depth: Option<usize>, data: ConfigData) {
249 self.inner.insert(ty, data);
250 if let Some(depth) = depth {
251 self.by_depth.insert((depth, ty));
252 }
253 }
254
255 fn extend(&mut self, other: Self) {
256 self.inner.extend(other.inner);
257 self.by_depth.extend(other.by_depth);
258 }
259}
260
261#[derive(Debug, Clone, Default)]
264pub struct ConfigSchema {
265 configs: BTreeMap<Cow<'static, str>, ConfigsForPrefix>,
268 mounting_points: MountingPoints,
269 coerce_serde_enums: bool,
270}
271
272impl ConfigSchema {
273 #[allow(clippy::missing_panics_doc)]
275 pub fn new(metadata: &'static ConfigMetadata, prefix: &'static str) -> Self {
276 let mut this = Self::default();
277 this.insert(metadata, prefix)
278 .expect("internal error: failed inserting first config to the schema");
279 this
280 }
281
282 pub fn coerce_serde_enums(&mut self, coerce: bool) -> &mut Self {
291 self.coerce_serde_enums = coerce;
292 self
293 }
294
295 pub(crate) fn iter_ll(&self) -> impl Iterator<Item = (Pointer<'_>, &ConfigData)> + '_ {
297 self.configs
298 .iter()
299 .flat_map(|(prefix, data)| data.inner.values().map(move |data| (Pointer(prefix), data)))
300 }
301
302 pub(crate) fn contains_canonical_param(&self, at: Pointer<'_>) -> bool {
303 self.mounting_points.get(at.0).is_some_and(|mount| {
304 matches!(
305 mount,
306 MountingPoint::Param {
307 is_canonical: true,
308 ..
309 }
310 )
311 })
312 }
313
314 pub(crate) fn params_with_kv_path<'s>(
315 &'s self,
316 kv_path: &'s str,
317 ) -> impl Iterator<Item = (Pointer<'s>, BasicTypes)> + 's {
318 self.mounting_points
319 .by_kv_path(kv_path)
320 .filter_map(|(path, mount)| {
321 let expecting = match mount {
322 MountingPoint::Param { expecting, .. } => *expecting,
323 MountingPoint::Config { .. } => return None,
324 };
325 Some((path, expecting))
326 })
327 }
328
329 pub(crate) fn shorthand_configs_with_kv_path<'s>(
331 &'s self,
332 kv_path: &'s str,
333 ) -> impl Iterator<Item = Pointer<'s>> + 's {
334 self.mounting_points
335 .by_kv_path(kv_path)
336 .filter_map(|(path, mount)| {
337 matches!(mount, MountingPoint::Config { shorthand: true }).then_some(path)
338 })
339 }
340
341 pub fn iter(&self) -> impl Iterator<Item = ConfigRef<'_>> + '_ {
344 self.configs.iter().flat_map(move |(prefix, data)| {
345 data.by_depth().map(move |data| ConfigRef {
346 schema: self,
347 prefix: prefix.as_ref(),
348 data,
349 })
350 })
351 }
352
353 pub fn locate(&self, metadata: &'static ConfigMetadata) -> impl Iterator<Item = &str> + '_ {
355 let config_type_id = metadata.ty.id();
356 self.configs.iter().filter_map(move |(prefix, data)| {
357 data.inner
358 .contains_key(&config_type_id)
359 .then_some(prefix.as_ref())
360 })
361 }
362
363 pub fn get<'s>(
365 &'s self,
366 metadata: &'static ConfigMetadata,
367 prefix: &'s str,
368 ) -> Option<ConfigRef<'s>> {
369 let data = self.get_ll(prefix, metadata.ty.id())?;
370 Some(ConfigRef {
371 schema: self,
372 prefix,
373 data,
374 })
375 }
376
377 fn get_ll(&self, prefix: &str, ty: any::TypeId) -> Option<&ConfigData> {
378 self.configs.get(prefix)?.inner.get(&ty)
379 }
380
381 pub fn get_mut(
383 &mut self,
384 metadata: &'static ConfigMetadata,
385 prefix: &str,
386 ) -> Option<ConfigMut<'_>> {
387 let ty = metadata.ty.id();
388 if !self.configs.get(prefix)?.inner.contains_key(&ty) {
389 return None;
390 }
391
392 Some(ConfigMut {
393 schema: self,
394 prefix: prefix.to_owned(),
395 type_id: ty,
396 })
397 }
398
399 #[allow(clippy::missing_panics_doc)] pub fn single(&self, metadata: &'static ConfigMetadata) -> anyhow::Result<ConfigRef<'_>> {
406 let prefixes: Vec<_> = self.locate(metadata).take(2).collect();
407 match prefixes.as_slice() {
408 [] => anyhow::bail!(
409 "configuration `{}` is not registered in schema",
410 metadata.ty.name_in_code()
411 ),
412 &[prefix] => Ok(ConfigRef {
413 schema: self,
414 prefix,
415 data: &self.configs[prefix].inner[&metadata.ty.id()],
416 }),
417 [first, second] => anyhow::bail!(
418 "configuration `{}` is registered in at least 2 locations: {first:?}, {second:?}",
419 metadata.ty.name_in_code()
420 ),
421 _ => unreachable!(),
422 }
423 }
424
425 #[allow(clippy::missing_panics_doc)] pub fn single_mut(
432 &mut self,
433 metadata: &'static ConfigMetadata,
434 ) -> anyhow::Result<ConfigMut<'_>> {
435 let mut it = self.locate(metadata);
436 let first_prefix = it.next().with_context(|| {
437 format!(
438 "configuration `{}` is not registered in schema",
439 metadata.ty.name_in_code()
440 )
441 })?;
442 if let Some(second_prefix) = it.next() {
443 anyhow::bail!(
444 "configuration `{}` is registered in at least 2 locations: {first_prefix:?}, {second_prefix:?}",
445 metadata.ty.name_in_code()
446 );
447 }
448
449 drop(it);
450 let prefix = first_prefix.to_owned();
451 Ok(ConfigMut {
452 schema: self,
453 type_id: metadata.ty.id(),
454 prefix,
455 })
456 }
457
458 pub fn insert(
469 &mut self,
470 metadata: &'static ConfigMetadata,
471 prefix: &'static str,
472 ) -> anyhow::Result<ConfigMut<'_>> {
473 let coerce_serde_enums = self.coerce_serde_enums;
474 let mut patched = PatchedSchema::new(self);
475 patched.insert_config(prefix, metadata, coerce_serde_enums)?;
476 patched.commit();
477 Ok(ConfigMut {
478 schema: self,
479 type_id: metadata.ty.id(),
480 prefix: prefix.to_owned(),
481 })
482 }
483}
484
485#[derive(Debug)]
487#[must_use = "Should be `commit()`ted"]
488struct PatchedSchema<'a> {
489 base: &'a mut ConfigSchema,
490 patch: ConfigSchema,
491}
492
493impl<'a> PatchedSchema<'a> {
494 fn new(base: &'a mut ConfigSchema) -> Self {
495 Self {
496 base,
497 patch: ConfigSchema::default(),
498 }
499 }
500
501 fn mount(&self, path: &str) -> Option<&MountingPoint> {
502 self.patch
503 .mounting_points
504 .get(path)
505 .or_else(|| self.base.mounting_points.get(path))
506 }
507
508 fn insert_config(
509 &mut self,
510 prefix: &'static str,
511 metadata: &'static ConfigMetadata,
512 coerce_serde_enums: bool,
513 ) -> anyhow::Result<()> {
514 self.insert_recursively(
515 prefix.into(),
516 true,
517 ConfigData {
518 metadata,
519 parent_link: None,
520 is_top_level: true,
521 coerce_serde_enums,
522 all_paths: vec![(prefix.into(), AliasOptions::new())],
523 },
524 )
525 }
526
527 fn insert_recursively(
528 &mut self,
529 prefix: Cow<'static, str>,
530 is_new: bool,
531 data: ConfigData,
532 ) -> anyhow::Result<()> {
533 let depth = is_new.then_some(0_usize);
534 let mut pending_configs = vec![(prefix, data, depth)];
535
536 while let Some((prefix, data, depth)) = pending_configs.pop() {
538 if is_new && self.base.get_ll(&prefix, data.metadata.ty.id()).is_some() {
541 continue;
542 }
543
544 let child_depth = depth.map(|d| d + 1);
545 let new_configs = Self::list_nested_configs(Pointer(&prefix), &data)
546 .map(|(prefix, data)| (prefix.into(), data, child_depth));
547 pending_configs.extend(new_configs);
548 self.insert_inner(prefix, depth, data)?;
549 }
550 Ok(())
551 }
552
553 fn insert_alias(
554 &mut self,
555 prefix: String,
556 config_id: any::TypeId,
557 alias: Pointer<'static>,
558 options: AliasOptions,
559 ) -> anyhow::Result<()> {
560 let config_data = &self.base.configs[prefix.as_str()].inner[&config_id];
561 if config_data
562 .all_paths
563 .iter()
564 .any(|(name, _)| name == alias.0)
565 {
566 return Ok(()); }
568
569 let metadata = config_data.metadata;
570 self.insert_recursively(
571 prefix.into(),
572 false,
573 ConfigData {
574 metadata,
575 parent_link: config_data.parent_link,
576 is_top_level: config_data.is_top_level,
577 coerce_serde_enums: config_data.coerce_serde_enums,
578 all_paths: vec![(alias.0.into(), options)],
579 },
580 )
581 }
582
583 fn list_nested_configs<'i>(
584 prefix: Pointer<'i>,
585 data: &'i ConfigData,
586 ) -> impl Iterator<Item = (String, ConfigData)> + 'i {
587 data.metadata.nested_configs.iter().map(move |nested| {
588 let all_paths =
589 data.all_paths_for_child(nested.name, nested.aliases, nested.tag_variant);
590 let all_paths = all_paths
591 .map(|(path, options)| (Cow::Owned(path), options))
592 .collect();
593
594 let config_data = ConfigData {
595 metadata: nested.meta,
596 parent_link: Some(ParentLink {
597 parent_ty: data.metadata.ty.id(),
598 this_ref: nested,
599 }),
600 is_top_level: false,
601 coerce_serde_enums: data.coerce_serde_enums,
602 all_paths,
603 };
604 (prefix.join(nested.name), config_data)
605 })
606 }
607
608 fn insert_inner(
609 &mut self,
610 prefix: Cow<'static, str>,
611 depth: Option<usize>,
612 mut data: ConfigData,
613 ) -> anyhow::Result<()> {
614 let config_name = data.metadata.ty.name_in_code();
615 let config_paths = data.all_paths.iter().map(|(name, _)| name.as_ref());
616 let config_paths = iter::once(prefix.as_ref()).chain(config_paths);
617 let has_shorthand = data.shorthand().is_some();
618
619 for path in config_paths {
620 let mut shorthand = has_shorthand;
621 if let Some(mount) = self.mount(path) {
622 match mount {
623 MountingPoint::Config {
624 shorthand: prev_shorthand,
625 } => shorthand |= *prev_shorthand,
626 MountingPoint::Param { .. } => {
627 anyhow::bail!(
628 "Cannot mount config `{}` at `{path}` because parameter(s) are already mounted at this path",
629 data.metadata.ty.name_in_code()
630 );
631 }
632 }
633 }
634 self.patch
635 .mounting_points
636 .insert(path.to_owned(), MountingPoint::Config { shorthand });
637 }
638
639 for param in data.metadata.params {
640 let all_paths = data.all_paths_for_param(param);
641
642 for (name_i, (full_name, _)) in all_paths.enumerate() {
643 let mut was_canonical = false;
644 if let Some(mount) = self.mount(&full_name) {
645 let prev_expecting = match mount {
646 MountingPoint::Param {
647 expecting,
648 is_canonical,
649 } => {
650 was_canonical = *is_canonical;
651 *expecting
652 }
653 MountingPoint::Config { .. } => {
654 anyhow::bail!(
655 "Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
656 config(s) are already mounted at this path",
657 name = param.name,
658 field = param.rust_field_name
659 );
660 }
661 };
662
663 if prev_expecting != param.expecting {
664 anyhow::bail!(
665 "Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
666 it expects {expecting}, while the existing param(s) mounted at this path expect {prev_expecting}",
667 name = param.name,
668 field = param.rust_field_name,
669 expecting = param.expecting
670 );
671 }
672 }
673 let is_canonical = was_canonical || name_i == 0;
674 self.patch.mounting_points.insert(
675 full_name,
676 MountingPoint::Param {
677 expecting: param.expecting,
678 is_canonical,
679 },
680 );
681 }
682 }
683
684 let config_id = data.metadata.ty.id();
688 let prev_data = self.base.get_ll(&prefix, config_id);
689 if let Some(prev_data) = prev_data {
690 let mut all_paths = prev_data.all_paths.clone();
692 all_paths.extend_from_slice(&data.all_paths);
693 data.all_paths = all_paths;
694 }
695
696 self.patch
697 .configs
698 .entry(prefix)
699 .or_default()
700 .insert(config_id, depth, data);
701 Ok(())
702 }
703
704 fn commit(self) {
705 for (prefix, data) in self.patch.configs {
706 let prev_data = self.base.configs.entry(prefix).or_default();
707 prev_data.extend(data);
708 }
709 self.base.mounting_points.extend(self.patch.mounting_points);
710 }
711}