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, ConfigVariant, NestedConfigMetadata,
16 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 aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
47 self.all_paths
48 .iter()
49 .skip(1)
50 .map(|(path, options)| (path.as_ref(), *options))
51 }
52
53 pub(crate) fn all_paths_for_param(
54 &self,
55 param: &'static ParamMetadata,
56 ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
57 self.all_paths_for_child(param.name, param.aliases, param.tag_variant)
58 }
59
60 fn all_paths_for_child(
61 &self,
62 name: &'static str,
63 aliases: &'static [(&'static str, AliasOptions)],
64 tag_variant: Option<&'static ConfigVariant>,
65 ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
66 let local_names =
67 iter::once((name, AliasOptions::default())).chain(aliases.iter().copied());
68
69 let enum_names = if let (true, Some(variant)) = (self.coerce_serde_enums, tag_variant) {
70 let variant_names = iter::once(variant.name)
71 .chain(variant.aliases.iter().copied())
72 .filter_map(|name| Some(EnumVariant::new(name)?.to_snake_case()));
73 let local_names_ = local_names.clone();
74 let paths = variant_names.flat_map(move |variant_name| {
75 local_names_
76 .clone()
77 .filter_map(move |(name_or_path, options)| {
78 if name_or_path.starts_with('.') {
79 return None;
81 }
82 let full_path = Pointer(&variant_name).join(name_or_path);
83 Some((Cow::Owned(full_path), options))
84 })
85 });
86 Some(paths)
87 } else {
88 None
89 };
90 let enum_names = enum_names.into_iter().flatten();
91 let local_names = local_names
92 .map(|(name, options)| (Cow::Borrowed(name), options))
93 .chain(enum_names);
94
95 self.all_paths
96 .iter()
97 .flat_map(move |(alias, config_options)| {
98 local_names
99 .clone()
100 .filter_map(move |(name_or_path, options)| {
101 let full_path = Pointer(alias).join_path(Pointer(&name_or_path))?;
102 Some((full_path, options.combine(*config_options)))
103 })
104 })
105 }
106}
107
108#[derive(Debug, Clone, Copy)]
110pub struct ConfigRef<'a> {
111 schema: &'a ConfigSchema,
112 prefix: &'a str,
113 pub(crate) data: &'a ConfigData,
114}
115
116impl<'a> ConfigRef<'a> {
117 pub fn prefix(&self) -> &'a str {
119 self.prefix
120 }
121
122 pub fn metadata(&self) -> &'static ConfigMetadata {
124 self.data.metadata
125 }
126
127 pub fn is_top_level(&self) -> bool {
129 self.data.parent_link.is_none()
130 }
131
132 #[doc(hidden)] pub fn parent_link(&self) -> Option<(Self, &'static NestedConfigMetadata)> {
134 let link = self.data.parent_link?;
135 let parent_prefix = if link.this_ref.name.is_empty() {
136 self.prefix
138 } else {
139 let (parent, _) = Pointer(self.prefix).split_last().unwrap();
140 parent.0
141 };
142 let parent_ref = Self {
143 schema: self.schema,
144 prefix: parent_prefix,
145 data: self.schema.get_ll(parent_prefix, link.parent_ty)?,
146 };
147 Some((parent_ref, link.this_ref))
148 }
149
150 pub fn aliases(&self) -> impl Iterator<Item = (&'a str, AliasOptions)> + '_ {
152 self.data.aliases()
153 }
154
155 #[doc(hidden)] pub fn all_paths_for_param(
159 &self,
160 param: &'static ParamMetadata,
161 ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
162 self.data.all_paths_for_param(param)
163 }
164}
165
166#[derive(Debug)]
168pub struct ConfigMut<'a> {
169 schema: &'a mut ConfigSchema,
170 prefix: String,
171 type_id: any::TypeId,
172}
173
174impl ConfigMut<'_> {
175 pub fn prefix(&self) -> &str {
177 &self.prefix
178 }
179
180 pub fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
182 let data = &self.schema.configs[self.prefix.as_str()].inner[&self.type_id];
183 data.aliases()
184 }
185
186 pub fn push_alias(self, alias: &'static str) -> anyhow::Result<Self> {
193 self.push_alias_inner(alias, AliasOptions::new())
194 }
195
196 pub fn push_deprecated_alias(self, alias: &'static str) -> anyhow::Result<Self> {
203 self.push_alias_inner(
204 alias,
205 AliasOptions {
206 is_deprecated: true,
207 },
208 )
209 }
210
211 fn push_alias_inner(self, alias: &'static str, options: AliasOptions) -> anyhow::Result<Self> {
212 let mut patched = PatchedSchema::new(self.schema);
213 patched.insert_alias(self.prefix.clone(), self.type_id, Pointer(alias), options)?;
214 patched.commit();
215 Ok(self)
216 }
217}
218
219#[derive(Debug, Clone, Default)]
220struct ConfigsForPrefix {
221 inner: HashMap<any::TypeId, ConfigData>,
222 by_depth: BTreeSet<(usize, any::TypeId)>,
223}
224
225impl ConfigsForPrefix {
226 fn by_depth(&self) -> impl Iterator<Item = &ConfigData> + '_ {
227 self.by_depth.iter().map(|(_, ty)| &self.inner[ty])
228 }
229
230 fn insert(&mut self, ty: any::TypeId, depth: Option<usize>, data: ConfigData) {
231 self.inner.insert(ty, data);
232 if let Some(depth) = depth {
233 self.by_depth.insert((depth, ty));
234 }
235 }
236
237 fn extend(&mut self, other: Self) {
238 self.inner.extend(other.inner);
239 self.by_depth.extend(other.by_depth);
240 }
241}
242
243#[derive(Debug, Clone, Default)]
246pub struct ConfigSchema {
247 configs: BTreeMap<Cow<'static, str>, ConfigsForPrefix>,
250 mounting_points: MountingPoints,
251 coerce_serde_enums: bool,
252}
253
254impl ConfigSchema {
255 #[allow(clippy::missing_panics_doc)]
257 pub fn new(metadata: &'static ConfigMetadata, prefix: &'static str) -> Self {
258 let mut this = Self::default();
259 this.insert(metadata, prefix)
260 .expect("internal error: failed inserting first config to the schema");
261 this
262 }
263
264 pub fn coerce_serde_enums(&mut self, coerce: bool) -> &mut Self {
273 self.coerce_serde_enums = coerce;
274 self
275 }
276
277 pub(crate) fn iter_ll(&self) -> impl Iterator<Item = (Pointer<'_>, &ConfigData)> + '_ {
279 self.configs
280 .iter()
281 .flat_map(|(prefix, data)| data.inner.values().map(move |data| (Pointer(prefix), data)))
282 }
283
284 pub(crate) fn contains_canonical_param(&self, at: Pointer<'_>) -> bool {
285 self.mounting_points.get(at.0).is_some_and(|mount| {
286 matches!(
287 mount,
288 MountingPoint::Param {
289 is_canonical: true,
290 ..
291 }
292 )
293 })
294 }
295
296 pub(crate) fn params_with_kv_path<'s>(
297 &'s self,
298 kv_path: &'s str,
299 ) -> impl Iterator<Item = (Pointer<'s>, BasicTypes)> + 's {
300 self.mounting_points
301 .by_kv_path(kv_path)
302 .filter_map(|(path, mount)| {
303 let expecting = match mount {
304 MountingPoint::Param { expecting, .. } => *expecting,
305 MountingPoint::Config => return None,
306 };
307 Some((path, expecting))
308 })
309 }
310
311 pub fn iter(&self) -> impl Iterator<Item = ConfigRef<'_>> + '_ {
314 self.configs.iter().flat_map(move |(prefix, data)| {
315 data.by_depth().map(move |data| ConfigRef {
316 schema: self,
317 prefix: prefix.as_ref(),
318 data,
319 })
320 })
321 }
322
323 pub fn locate(&self, metadata: &'static ConfigMetadata) -> impl Iterator<Item = &str> + '_ {
325 let config_type_id = metadata.ty.id();
326 self.configs.iter().filter_map(move |(prefix, data)| {
327 data.inner
328 .contains_key(&config_type_id)
329 .then_some(prefix.as_ref())
330 })
331 }
332
333 pub fn get<'s>(
335 &'s self,
336 metadata: &'static ConfigMetadata,
337 prefix: &'s str,
338 ) -> Option<ConfigRef<'s>> {
339 let data = self.get_ll(prefix, metadata.ty.id())?;
340 Some(ConfigRef {
341 schema: self,
342 prefix,
343 data,
344 })
345 }
346
347 fn get_ll(&self, prefix: &str, ty: any::TypeId) -> Option<&ConfigData> {
348 self.configs.get(prefix)?.inner.get(&ty)
349 }
350
351 pub fn get_mut(
353 &mut self,
354 metadata: &'static ConfigMetadata,
355 prefix: &str,
356 ) -> Option<ConfigMut<'_>> {
357 let ty = metadata.ty.id();
358 if !self.configs.get(prefix)?.inner.contains_key(&ty) {
359 return None;
360 }
361
362 Some(ConfigMut {
363 schema: self,
364 prefix: prefix.to_owned(),
365 type_id: ty,
366 })
367 }
368
369 #[allow(clippy::missing_panics_doc)] pub fn single(&self, metadata: &'static ConfigMetadata) -> anyhow::Result<ConfigRef<'_>> {
376 let prefixes: Vec<_> = self.locate(metadata).take(2).collect();
377 match prefixes.as_slice() {
378 [] => anyhow::bail!(
379 "configuration `{}` is not registered in schema",
380 metadata.ty.name_in_code()
381 ),
382 &[prefix] => Ok(ConfigRef {
383 schema: self,
384 prefix,
385 data: &self.configs[prefix].inner[&metadata.ty.id()],
386 }),
387 [first, second] => anyhow::bail!(
388 "configuration `{}` is registered in at least 2 locations: {first:?}, {second:?}",
389 metadata.ty.name_in_code()
390 ),
391 _ => unreachable!(),
392 }
393 }
394
395 #[allow(clippy::missing_panics_doc)] pub fn single_mut(
402 &mut self,
403 metadata: &'static ConfigMetadata,
404 ) -> anyhow::Result<ConfigMut<'_>> {
405 let mut it = self.locate(metadata);
406 let first_prefix = it.next().with_context(|| {
407 format!(
408 "configuration `{}` is not registered in schema",
409 metadata.ty.name_in_code()
410 )
411 })?;
412 if let Some(second_prefix) = it.next() {
413 anyhow::bail!(
414 "configuration `{}` is registered in at least 2 locations: {first_prefix:?}, {second_prefix:?}",
415 metadata.ty.name_in_code()
416 );
417 }
418
419 drop(it);
420 let prefix = first_prefix.to_owned();
421 Ok(ConfigMut {
422 schema: self,
423 type_id: metadata.ty.id(),
424 prefix,
425 })
426 }
427
428 pub fn insert(
439 &mut self,
440 metadata: &'static ConfigMetadata,
441 prefix: &'static str,
442 ) -> anyhow::Result<ConfigMut<'_>> {
443 let coerce_serde_enums = self.coerce_serde_enums;
444 let mut patched = PatchedSchema::new(self);
445 patched.insert_config(prefix, metadata, coerce_serde_enums)?;
446 patched.commit();
447 Ok(ConfigMut {
448 schema: self,
449 type_id: metadata.ty.id(),
450 prefix: prefix.to_owned(),
451 })
452 }
453}
454
455#[derive(Debug)]
457#[must_use = "Should be `commit()`ted"]
458struct PatchedSchema<'a> {
459 base: &'a mut ConfigSchema,
460 patch: ConfigSchema,
461}
462
463impl<'a> PatchedSchema<'a> {
464 fn new(base: &'a mut ConfigSchema) -> Self {
465 Self {
466 base,
467 patch: ConfigSchema::default(),
468 }
469 }
470
471 fn mount(&self, path: &str) -> Option<&MountingPoint> {
472 self.patch
473 .mounting_points
474 .get(path)
475 .or_else(|| self.base.mounting_points.get(path))
476 }
477
478 fn insert_config(
479 &mut self,
480 prefix: &'static str,
481 metadata: &'static ConfigMetadata,
482 coerce_serde_enums: bool,
483 ) -> anyhow::Result<()> {
484 self.insert_recursively(
485 prefix.into(),
486 true,
487 ConfigData {
488 metadata,
489 parent_link: None,
490 is_top_level: true,
491 coerce_serde_enums,
492 all_paths: vec![(prefix.into(), AliasOptions::new())],
493 },
494 )
495 }
496
497 fn insert_recursively(
498 &mut self,
499 prefix: Cow<'static, str>,
500 is_new: bool,
501 data: ConfigData,
502 ) -> anyhow::Result<()> {
503 let depth = is_new.then_some(0_usize);
504 let mut pending_configs = vec![(prefix, data, depth)];
505
506 while let Some((prefix, data, depth)) = pending_configs.pop() {
508 if is_new && self.base.get_ll(&prefix, data.metadata.ty.id()).is_some() {
511 continue;
512 }
513
514 let child_depth = depth.map(|d| d + 1);
515 let new_configs = Self::list_nested_configs(Pointer(&prefix), &data)
516 .map(|(prefix, data)| (prefix.into(), data, child_depth));
517 pending_configs.extend(new_configs);
518 self.insert_inner(prefix, depth, data)?;
519 }
520 Ok(())
521 }
522
523 fn insert_alias(
524 &mut self,
525 prefix: String,
526 config_id: any::TypeId,
527 alias: Pointer<'static>,
528 options: AliasOptions,
529 ) -> anyhow::Result<()> {
530 let config_data = &self.base.configs[prefix.as_str()].inner[&config_id];
531 if config_data
532 .all_paths
533 .iter()
534 .any(|(name, _)| name == alias.0)
535 {
536 return Ok(()); }
538
539 let metadata = config_data.metadata;
540 self.insert_recursively(
541 prefix.into(),
542 false,
543 ConfigData {
544 metadata,
545 parent_link: config_data.parent_link,
546 is_top_level: config_data.is_top_level,
547 coerce_serde_enums: config_data.coerce_serde_enums,
548 all_paths: vec![(alias.0.into(), options)],
549 },
550 )
551 }
552
553 fn list_nested_configs<'i>(
554 prefix: Pointer<'i>,
555 data: &'i ConfigData,
556 ) -> impl Iterator<Item = (String, ConfigData)> + 'i {
557 data.metadata.nested_configs.iter().map(move |nested| {
558 let all_paths =
559 data.all_paths_for_child(nested.name, nested.aliases, nested.tag_variant);
560 let all_paths = all_paths
561 .map(|(path, options)| (Cow::Owned(path), options))
562 .collect();
563
564 let config_data = ConfigData {
565 metadata: nested.meta,
566 parent_link: Some(ParentLink {
567 parent_ty: data.metadata.ty.id(),
568 this_ref: nested,
569 }),
570 is_top_level: false,
571 coerce_serde_enums: data.coerce_serde_enums,
572 all_paths,
573 };
574 (prefix.join(nested.name), config_data)
575 })
576 }
577
578 fn insert_inner(
579 &mut self,
580 prefix: Cow<'static, str>,
581 depth: Option<usize>,
582 mut data: ConfigData,
583 ) -> anyhow::Result<()> {
584 let config_name = data.metadata.ty.name_in_code();
585 let config_paths = data.all_paths.iter().map(|(name, _)| name.as_ref());
586 let config_paths = iter::once(prefix.as_ref()).chain(config_paths);
587
588 for path in config_paths {
589 if let Some(mount) = self.mount(path) {
590 match mount {
591 MountingPoint::Config => { }
592 MountingPoint::Param { .. } => {
593 anyhow::bail!(
594 "Cannot mount config `{}` at `{path}` because parameter(s) are already mounted at this path",
595 data.metadata.ty.name_in_code()
596 );
597 }
598 }
599 }
600 self.patch
601 .mounting_points
602 .insert(path.to_owned(), MountingPoint::Config);
603 }
604
605 for param in data.metadata.params {
606 let all_paths = data.all_paths_for_param(param);
607
608 for (name_i, (full_name, _)) in all_paths.enumerate() {
609 let mut was_canonical = false;
610 if let Some(mount) = self.mount(&full_name) {
611 let prev_expecting = match mount {
612 MountingPoint::Param {
613 expecting,
614 is_canonical,
615 } => {
616 was_canonical = *is_canonical;
617 *expecting
618 }
619 MountingPoint::Config => {
620 anyhow::bail!(
621 "Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
622 config(s) are already mounted at this path",
623 name = param.name,
624 field = param.rust_field_name
625 );
626 }
627 };
628
629 if prev_expecting != param.expecting {
630 anyhow::bail!(
631 "Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
632 it expects {expecting}, while the existing param(s) mounted at this path expect {prev_expecting}",
633 name = param.name,
634 field = param.rust_field_name,
635 expecting = param.expecting
636 );
637 }
638 }
639 let is_canonical = was_canonical || name_i == 0;
640 self.patch.mounting_points.insert(
641 full_name,
642 MountingPoint::Param {
643 expecting: param.expecting,
644 is_canonical,
645 },
646 );
647 }
648 }
649
650 let config_id = data.metadata.ty.id();
654 let prev_data = self.base.get_ll(&prefix, config_id);
655 if let Some(prev_data) = prev_data {
656 let mut all_paths = prev_data.all_paths.clone();
658 all_paths.extend_from_slice(&data.all_paths);
659 data.all_paths = all_paths;
660 }
661
662 self.patch
663 .configs
664 .entry(prefix)
665 .or_default()
666 .insert(config_id, depth, data);
667 Ok(())
668 }
669
670 fn commit(self) {
671 for (prefix, data) in self.patch.configs {
672 let prev_data = self.base.configs.entry(prefix).or_default();
673 prev_data.extend(data);
674 }
675 self.base.mounting_points.extend(self.patch.mounting_points);
676 }
677}