1use std::{
2 any,
3 collections::{BTreeMap, HashSet},
4 iter,
5 marker::PhantomData,
6 mem,
7 sync::Arc,
8};
9
10pub use self::{env::Environment, json::Json, yaml::Yaml};
11use crate::{
12 DescribeConfig, DeserializeConfig, DeserializeConfigError, ParseError, ParseErrors,
13 de::{DeserializeContext, DeserializerOptions},
14 fallback::Fallbacks,
15 metadata::{BasicTypes, ConfigTag, ConfigVariant, TypeSuffixes},
16 schema::{ConfigData, ConfigRef, ConfigSchema},
17 utils::{EnumVariant, JsonObject, merge_json},
18 value::{Map, Pointer, Value, ValueOrigin, WithOrigin},
19 visit::Serializer,
20};
21
22#[macro_use]
23mod macros;
24mod env;
25mod json;
26#[cfg(test)]
27mod tests;
28mod yaml;
29
30pub trait ConfigSourceKind: crate::utils::Sealed {
32 #[doc(hidden)] const IS_FLAT: bool;
34}
35
36#[derive(Debug)]
38pub struct Hierarchical(());
39
40impl crate::utils::Sealed for Hierarchical {}
41impl ConfigSourceKind for Hierarchical {
42 const IS_FLAT: bool = false;
43}
44
45#[derive(Debug)]
47pub struct Flat(());
48
49impl crate::utils::Sealed for Flat {}
50impl ConfigSourceKind for Flat {
51 const IS_FLAT: bool = true;
52}
53
54pub trait ConfigSource {
56 type Kind: ConfigSourceKind;
58 fn into_contents(self) -> WithOrigin<Map>;
60}
61
62#[derive(Debug, Clone)]
64pub struct Prefixed<T> {
65 inner: T,
66 prefix: String,
67}
68
69impl<T: ConfigSource<Kind = Hierarchical>> Prefixed<T> {
70 pub fn new(inner: T, prefix: impl Into<String>) -> Self {
72 Self {
73 inner,
74 prefix: prefix.into(),
75 }
76 }
77}
78
79impl<T: ConfigSource<Kind = Hierarchical>> ConfigSource for Prefixed<T> {
80 type Kind = Hierarchical;
81
82 fn into_contents(self) -> WithOrigin<Map> {
83 let contents = self.inner.into_contents();
84
85 let origin = Arc::new(ValueOrigin::Synthetic {
86 source: contents.origin.clone(),
87 transform: format!("prefixed with `{}`", self.prefix),
88 });
89
90 if let Some((parent, key_in_parent)) = Pointer(&self.prefix).split_last() {
91 let mut root = WithOrigin::new(Value::Object(Map::new()), origin.clone());
92 root.ensure_object(parent, |_| origin.clone())
93 .insert(key_in_parent.to_owned(), contents.map(Value::Object));
94 root.map(|value| match value {
95 Value::Object(map) => map,
96 _ => unreachable!(), })
98 } else {
99 contents
100 }
101 }
102}
103
104#[derive(Debug, Clone, Default)]
107pub struct ConfigSources {
108 inner: Vec<(WithOrigin<Map>, bool)>,
109}
110
111impl ConfigSources {
112 pub fn push<S: ConfigSource>(&mut self, source: S) {
114 self.inner
115 .push((source.into_contents(), <S::Kind>::IS_FLAT));
116 }
117}
118
119#[derive(Debug, Clone)]
121#[non_exhaustive]
122pub struct SourceInfo {
123 pub origin: Arc<ValueOrigin>,
125 pub param_count: usize,
127}
128
129#[derive(Debug, Clone, Default)]
131pub struct SerializerOptions {
132 pub(crate) diff_with_default: bool,
133 pub(crate) secret_placeholder: Option<String>,
134 pub(crate) flat: bool,
135}
136
137impl SerializerOptions {
138 pub fn diff_with_default() -> Self {
140 Self {
141 diff_with_default: true,
142 secret_placeholder: None,
143 flat: false,
144 }
145 }
146
147 #[must_use]
155 pub fn flat(mut self, flat: bool) -> Self {
156 self.flat = flat;
157 self
158 }
159
160 #[must_use]
162 pub fn with_secret_placeholder(mut self, placeholder: impl Into<String>) -> Self {
163 self.secret_placeholder = Some(placeholder.into());
164 self
165 }
166
167 pub fn serialize<C: DescribeConfig>(self, config: &C) -> JsonObject {
169 let mut visitor = Serializer::new(&C::DESCRIPTION, "", self);
170 config.visit_config(&mut visitor);
171 visitor.into_inner()
172 }
173}
174
175#[derive(Debug, Clone)]
231pub struct ConfigRepository<'a> {
232 schema: &'a ConfigSchema,
233 prefixes_for_canonical_configs: HashSet<Pointer<'a>>,
234 de_options: DeserializerOptions,
235 sources: Vec<SourceInfo>,
236 merged: WithOrigin,
237}
238
239impl<'a> ConfigRepository<'a> {
240 pub fn new(schema: &'a ConfigSchema) -> Self {
242 let prefixes_for_canonical_configs: HashSet<_> = schema
243 .iter_ll()
244 .flat_map(|(path, _)| path.with_ancestors())
245 .chain([Pointer("")])
246 .collect();
247
248 let this = Self {
249 schema,
250 prefixes_for_canonical_configs,
251 de_options: DeserializerOptions::default(),
252 sources: vec![],
253 merged: WithOrigin {
254 inner: Value::Object(Map::default()),
255 origin: Arc::default(),
256 },
257 };
258 if let Some(fallbacks) = Fallbacks::new(schema) {
259 this.with(fallbacks)
260 } else {
261 this
262 }
263 }
264
265 pub fn schema(&self) -> &'a ConfigSchema {
267 self.schema
268 }
269
270 pub fn deserializer_options(&mut self) -> &mut DeserializerOptions {
272 &mut self.de_options
273 }
274
275 #[must_use]
277 pub fn with<S: ConfigSource>(mut self, source: S) -> Self {
278 self.insert_inner(source.into_contents(), <S::Kind>::IS_FLAT);
279 self
280 }
281
282 #[tracing::instrument(
283 level = "debug",
284 name = "ConfigRepository::insert",
285 skip(self, contents)
286 )]
287 fn insert_inner(&mut self, contents: WithOrigin<Map>, is_flat: bool) {
288 let mut source_value = if is_flat {
289 WithOrigin::nest_kvs(contents.inner, self.schema, &contents.origin)
290 } else {
291 WithOrigin {
292 inner: Value::Object(contents.inner),
293 origin: contents.origin.clone(),
294 }
295 };
296
297 let param_count =
298 source_value.preprocess_source(self.schema, &self.prefixes_for_canonical_configs);
299 tracing::debug!(param_count, "Inserted source into config repo");
300 self.merged
301 .guided_merge(source_value, self.schema, Pointer(""));
302 self.sources.push(SourceInfo {
303 origin: contents.origin,
304 param_count,
305 });
306 }
307
308 #[must_use]
310 pub fn with_all(mut self, sources: ConfigSources) -> Self {
311 for (contents, is_flat) in sources.inner {
312 self.insert_inner(contents, is_flat);
313 }
314 self
315 }
316
317 pub fn sources(&self) -> &[SourceInfo] {
319 &self.sources
320 }
321
322 #[doc(hidden)] pub fn merged(&self) -> &WithOrigin {
324 &self.merged
325 }
326
327 #[doc(hidden)] pub fn canonicalize(&self, options: &SerializerOptions) -> Result<JsonObject, ParseErrors> {
339 let mut json = serde_json::Map::new();
340 for config_parser in self.iter() {
341 if !config_parser.config().is_top_level() {
342 continue;
344 }
345
346 let parsed = match config_parser.parse_opt() {
347 Ok(Some(config)) => config,
348 Ok(None) => continue,
349 Err(err) => return Err(err),
350 };
351
352 let metadata = config_parser.config().metadata();
353 let prefix = config_parser.config().prefix();
354 let mut visitor = Serializer::new(metadata, prefix, options.clone());
355 (metadata.visitor)(parsed.as_ref(), &mut visitor);
356 let serialized = visitor.into_inner();
357
358 if options.flat {
359 json.extend(serialized);
360 } else {
361 merge_json(&mut json, metadata, prefix, serialized);
362 }
363 }
364 Ok(json)
365 }
366
367 pub fn iter(&self) -> impl Iterator<Item = ConfigParser<'_, ()>> + '_ {
369 self.schema.iter().map(|config_ref| ConfigParser {
370 repo: self,
371 config_ref,
372 _config: PhantomData,
373 })
374 }
375
376 pub fn single<C: DeserializeConfig>(&self) -> anyhow::Result<ConfigParser<'_, C>> {
382 let config_ref = self.schema.single(&C::DESCRIPTION)?;
383 Ok(ConfigParser {
384 repo: self,
385 config_ref,
386 _config: PhantomData,
387 })
388 }
389
390 pub fn get<'s, C: DeserializeConfig>(&'s self, prefix: &'s str) -> Option<ConfigParser<'s, C>> {
393 let config_ref = self.schema.get(&C::DESCRIPTION, prefix)?;
394 Some(ConfigParser {
395 repo: self,
396 config_ref,
397 _config: PhantomData,
398 })
399 }
400}
401
402#[derive(Debug)]
404pub struct ConfigParser<'a, C> {
405 repo: &'a ConfigRepository<'a>,
406 config_ref: ConfigRef<'a>,
407 _config: PhantomData<C>,
408}
409
410impl ConfigParser<'_, ()> {
411 #[doc(hidden)] #[allow(clippy::redundant_closure_for_method_calls)] pub fn parse(&self) -> Result<Box<dyn any::Any>, ParseErrors> {
419 self.with_context(|ctx| ctx.deserialize_any_config())
420 }
421
422 #[doc(hidden)] #[allow(clippy::redundant_closure_for_method_calls)] pub fn parse_opt(&self) -> Result<Option<Box<dyn any::Any>>, ParseErrors> {
432 self.with_context(|ctx| ctx.deserialize_any_config_opt())
433 }
434}
435
436impl<'a, C> ConfigParser<'a, C> {
437 pub fn config(&self) -> ConfigRef<'a> {
439 self.config_ref
440 }
441
442 fn with_context<R>(
443 &self,
444 action: impl FnOnce(DeserializeContext<'_>) -> Result<R, DeserializeConfigError>,
445 ) -> Result<R, ParseErrors> {
446 let mut errors = ParseErrors::default();
447 let prefix = self.config_ref.prefix();
448 let metadata = self.config_ref.data.metadata;
449 let ctx = DeserializeContext::new(
450 &self.repo.de_options,
451 &self.repo.merged,
452 prefix.to_owned(),
453 metadata,
454 &mut errors,
455 );
456 action(ctx).map_err(|_| {
457 if errors.len() == 0 {
458 errors.push(ParseError::generic(prefix.to_owned(), metadata));
459 }
460 errors
461 })
462 }
463}
464
465impl<C: DeserializeConfig> ConfigParser<'_, C> {
466 #[allow(clippy::redundant_closure_for_method_calls)] pub fn parse(self) -> Result<C, ParseErrors> {
474 self.with_context(|ctx| ctx.deserialize_config::<C>())
475 }
476
477 #[allow(clippy::redundant_closure_for_method_calls)] pub fn parse_opt(self) -> Result<Option<C>, ParseErrors> {
485 self.with_context(|ctx| ctx.deserialize_config_opt::<C>())
486 }
487}
488
489impl WithOrigin {
490 fn preprocess_source(
491 &mut self,
492 schema: &ConfigSchema,
493 prefixes_for_canonical_configs: &HashSet<Pointer<'_>>,
494 ) -> usize {
495 self.expand_enum_shorthands(schema);
496 self.copy_aliased_values(schema);
497 self.mark_secrets(schema);
498 self.convert_serde_enums(schema);
499 self.nest_object_params_and_sub_configs(schema);
500 self.nest_array_params(schema);
501 self.collect_garbage(schema, prefixes_for_canonical_configs, Pointer(""))
502 }
503
504 #[tracing::instrument(level = "debug", skip_all)]
505 fn copy_aliased_values(&mut self, schema: &ConfigSchema) {
506 for (prefix, config_data) in schema.iter_ll() {
507 let (new_values, new_map_origin) = self.copy_aliases_for_config(config_data);
508 if new_values.is_empty() {
509 continue;
510 }
511
512 let new_map_origin = new_map_origin.map(|source| {
513 Arc::new(ValueOrigin::Synthetic {
514 source,
515 transform: format!("copy to '{prefix}' per aliasing rules"),
516 })
517 });
518 self.ensure_object(prefix, |_| new_map_origin.clone().unwrap())
521 .extend(new_values);
522 }
523 }
524
525 #[must_use = "returned map should be inserted into the config"]
526 fn copy_aliases_for_config(&self, config: &ConfigData) -> (Map, Option<Arc<ValueOrigin>>) {
527 let prefix = config.prefix();
528 let canonical_map = match self.get(prefix).map(|val| &val.inner) {
529 Some(Value::Object(map)) => Some(map),
530 Some(_) => {
531 tracing::warn!(
532 prefix = prefix.0,
533 config = ?config.metadata.ty,
534 "canonical config location contains a non-object"
535 );
536 return (Map::new(), None);
537 }
538 None => None,
539 };
540
541 let mut new_values = Map::new();
542 let mut new_map_origin = None;
543
544 for param in config.metadata.params {
545 let all_paths = config.all_paths_for_param(param);
547
548 for (path, alias_options) in all_paths {
549 let (prefix, name) = Pointer(&path)
550 .split_last()
551 .expect("param paths are never empty");
552 let Some(map) = self.get(prefix) else {
553 continue;
554 };
555 let map_origin = &map.origin;
556 let Some(map) = map.inner.as_object() else {
557 continue;
558 };
559
560 let matching_values: Vec<_> =
562 if let Some(suffixes) = param.type_description().suffixes() {
563 let matching_values = map.iter().filter_map(|(key, val)| {
564 let suffix = if key == name {
565 None } else {
567 let key_suffix = Self::strip_prefix(key, name)?;
568 if !suffixes.contains(key_suffix) {
569 return None;
570 }
571 Some(key_suffix)
572 };
573 Some((suffix, val))
574 });
575 matching_values.collect()
576 } else if let Some(val) = map.get(name) {
577 vec![(None, val)]
579 } else {
580 vec![]
581 };
582
583 for (suffix, val) in matching_values {
585 let canonical_key_string;
586 let canonical_key = if let Some(suffix) = suffix {
587 canonical_key_string = format!("{}_{suffix}", param.name);
588 &canonical_key_string
589 } else {
590 param.name
591 };
592
593 if canonical_map.is_some_and(|map| map.contains_key(canonical_key)) {
594 continue;
596 }
597
598 if !new_values.contains_key(canonical_key) {
599 if alias_options.is_deprecated {
600 tracing::warn!(
601 path,
602 origin = %val.origin,
603 config = ?config.metadata.ty,
604 param = param.rust_field_name,
605 canonical_path = prefix.join(canonical_key),
606 "using deprecated alias; please use canonical_path instead"
607 );
608 }
609
610 tracing::trace!(
611 prefix = prefix.0,
612 config = ?config.metadata.ty,
613 param = param.rust_field_name,
614 name,
615 origin = ?map_origin,
616 canonical_key,
617 "copied aliased param"
618 );
619 new_values.insert(canonical_key.to_owned(), val.clone());
620 if new_map_origin.is_none() {
621 new_map_origin = Some(map_origin.clone());
622 }
623 }
624 }
625 }
626 }
627
628 (new_values, new_map_origin)
629 }
630
631 fn strip_prefix<'s>(s: &'s str, prefix: &str) -> Option<&'s str> {
632 s.strip_prefix(prefix)?
633 .strip_prefix('_')
634 .filter(|suffix| !suffix.is_empty())
635 }
636
637 #[tracing::instrument(level = "debug", skip_all)]
642 fn expand_enum_shorthands(&mut self, schema: &ConfigSchema) {
643 for (prefix, config_data) in schema.iter_ll() {
644 let Some(shorthand) = config_data.shorthand() else {
645 continue;
646 };
647 let tag = config_data
648 .metadata
649 .tag
650 .as_ref()
651 .expect("config with a shorthand is always tagged");
652
653 let alias_paths = config_data.aliases().map(|(alias, _)| Pointer(alias));
654 for path in iter::once(prefix).chain(alias_paths) {
655 let Some(value) = self.get_mut(path) else {
656 continue;
657 };
658 if matches!(value.inner, Value::Object(_) | Value::Null) {
659 continue;
660 }
661
662 tracing::debug!(
663 prefix = path.0,
664 config = ?config_data.metadata.ty,
665 variant = shorthand.variant.name,
666 "expanding enum config shorthand"
667 );
668 let origin = Arc::new(ValueOrigin::Synthetic {
669 source: value.origin.clone(),
670 transform: format!(
671 "expanding shorthand for variant '{}'",
672 shorthand.variant.name
673 ),
674 });
675 let shorthand_value = mem::take(&mut value.inner);
676 let mut map = Map::new();
677 map.insert(
678 tag.param.name.to_owned(),
679 WithOrigin::new(shorthand.variant.name.to_owned().into(), origin.clone()),
680 );
681 map.insert(
682 shorthand.param.name.to_owned(),
683 WithOrigin::new(shorthand_value, value.origin.clone()),
684 );
685 value.inner = Value::Object(map);
686 value.origin = origin;
687 }
688 }
689 }
690
691 fn mark_secrets(&mut self, schema: &ConfigSchema) {
693 for (prefix, config_data) in schema.iter_ll() {
694 let Some(Self {
695 inner: Value::Object(config_object),
696 ..
697 }) = self.get_mut(prefix)
698 else {
699 continue;
700 };
701
702 for param in config_data.metadata.params {
703 if !param.type_description().contains_secrets() {
704 continue;
705 }
706 let Some(value) = config_object.get_mut(param.name) else {
707 continue;
708 };
709
710 if let Value::String(str) = &mut value.inner {
711 tracing::trace!(
712 prefix = prefix.0,
713 config = ?config_data.metadata.ty,
714 param = param.rust_field_name,
715 "marked param as secret"
716 );
717 str.make_secret();
718 } else {
719 tracing::warn!(
720 prefix = prefix.0,
721 config = ?config_data.metadata.ty,
722 param = param.rust_field_name,
723 "param marked as secret has non-string value"
724 );
725 }
726 }
727 }
728 }
729
730 #[tracing::instrument(level = "debug", skip_all)]
731 fn convert_serde_enums(&mut self, schema: &ConfigSchema) {
732 for config_data in schema.iter() {
733 let config_meta = config_data.metadata();
734 let prefix = Pointer(config_data.prefix());
735
736 let Some(tag) = &config_meta.tag else {
737 continue; };
739 if !config_data.data.coerce_serde_enums {
740 continue;
741 }
742
743 let canonical_map = self.get(prefix).and_then(|val| val.inner.as_object());
744 let alias_maps = config_data
745 .aliases()
746 .filter_map(|(alias, _)| self.get(Pointer(alias))?.inner.as_object());
747
748 if canonical_map.is_some_and(|map| map.contains_key(tag.param.name)) {
749 continue;
751 }
752
753 let _span_guard = tracing::info_span!(
754 "convert_serde_enum",
755 config = ?config_meta.ty,
756 prefix = prefix.0,
757 tag = tag.param.name,
758 )
759 .entered();
760
761 if let Some((variant, variant_content)) =
762 Self::detect_serde_enum_variant(canonical_map, alias_maps, tag)
763 {
764 tracing::debug!(
765 variant = variant.name,
766 origin = %variant_content.origin,
767 "adding detected tag variant"
768 );
769 let origin = ValueOrigin::Synthetic {
770 source: variant_content.origin.clone(),
771 transform: "coercing serde enum".to_owned(),
772 };
773
774 let canonical_map = self.ensure_object(prefix, |_| {
775 Arc::new(ValueOrigin::Synthetic {
776 source: Arc::default(),
777 transform: "enum coercion".to_string(),
778 })
779 });
780 canonical_map.insert(
781 tag.param.name.to_owned(),
782 WithOrigin::new(variant.name.to_owned().into(), Arc::new(origin)),
783 );
784 }
785 }
786 }
787
788 fn detect_serde_enum_variant<'a>(
789 canonical_map: Option<&'a Map>,
790 alias_maps: impl Iterator<Item = &'a Map>,
791 tag: &'static ConfigTag,
792 ) -> Option<(&'static ConfigVariant, &'a Self)> {
793 let all_variant_names = tag.variants.iter().flat_map(|variant| {
794 iter::once(variant.name)
795 .chain(variant.aliases.iter().copied())
796 .filter_map(move |name| Some((EnumVariant::new(name)?.to_snake_case(), variant)))
797 });
798
799 let mut variant_match = None;
801 for map in canonical_map.into_iter().chain(alias_maps) {
802 for (candidate_field_name, variant) in all_variant_names.clone() {
803 if map.contains_key(&candidate_field_name) {
804 if let Some((_, prev_field, _)) = &variant_match
805 && *prev_field != candidate_field_name
806 {
807 tracing::info!(
808 prev_field,
809 field = candidate_field_name,
810 "multiple serde-like variant fields present"
811 );
812 return None;
813 }
814 variant_match = Some((map, candidate_field_name, variant));
815 }
816 }
817 }
818
819 let Some((map, field_name, variant)) = variant_match else {
820 return None; };
822 let variant_content = map.get(&field_name).unwrap();
823 if !matches!(&variant_content.inner, Value::Object(_)) {
824 tracing::info!(
825 field = field_name,
826 "variant contents is not an object, skipping"
827 );
828 return None;
829 }
830 Some((variant, variant_content))
831 }
832
833 fn collect_garbage(
835 &mut self,
836 schema: &ConfigSchema,
837 prefixes_for_canonical_configs: &HashSet<Pointer<'_>>,
838 at: Pointer<'_>,
839 ) -> usize {
840 if schema.contains_canonical_param(at) {
841 1
842 } else if prefixes_for_canonical_configs.contains(&at) {
843 if let Value::Object(map) = &mut self.inner {
844 let mut count = 0;
845 map.retain(|key, value| {
846 let child_path = at.join(key);
847 let descendant_count = value.collect_garbage(
848 schema,
849 prefixes_for_canonical_configs,
850 Pointer(&child_path),
851 );
852 count += descendant_count;
853 descendant_count > 0
854 });
855 count
856 } else {
857 1
859 }
860 } else {
861 0
863 }
864 }
865
866 #[tracing::instrument(level = "debug", skip_all)]
872 fn nest_object_params_and_sub_configs(&mut self, schema: &ConfigSchema) {
873 for (prefix, config_data) in schema.iter_ll() {
874 let Some(config_object) = self.get_mut(prefix) else {
875 continue;
876 };
877 let config_origin = &config_object.origin;
878 let Value::Object(config_object) = &mut config_object.inner else {
879 continue;
880 };
881
882 let params_with_suffixes = config_data.metadata.params.iter().filter_map(|param| {
883 let suffixes = param.type_description().suffixes()?;
884 Some((param.name, suffixes))
885 });
886 let nested_configs = config_data
887 .metadata
888 .nested_configs
889 .iter()
890 .filter_map(|nested| {
891 (!nested.name.is_empty()).then_some((nested.name, TypeSuffixes::All))
892 });
893 let mut insertions = vec![];
894
895 for (child_name, suffixes) in params_with_suffixes.chain(nested_configs) {
898 let target_object = match config_object.get(child_name) {
899 None => None,
900 Some(WithOrigin {
901 inner: Value::Object(obj),
902 ..
903 }) => Some(obj),
904 Some(_) => continue,
906 };
907
908 let matching_fields: Vec<_> = config_object
909 .iter()
910 .filter_map(|(name, field)| {
911 let suffix = Self::strip_prefix(name, child_name)?;
912 if !suffixes.contains(suffix) {
913 return None;
914 }
915 if let Some(param_object) = target_object
916 && param_object.contains_key(suffix)
917 {
918 return None; }
920 Some((suffix.to_owned(), field.clone()))
921 })
922 .collect();
923 if matching_fields.is_empty() {
924 continue;
925 }
926
927 tracing::trace!(
928 prefix = prefix.0,
929 config = ?config_data.metadata.ty,
930 child_name,
931 fields = ?matching_fields.iter().map(|(name, _)| name).collect::<Vec<_>>(),
932 "nesting for object param / config"
933 );
934 insertions.push((child_name, matching_fields));
935 }
936
937 for (child_name, matching_fields) in insertions {
938 if !config_object.contains_key(child_name) {
939 let origin = Arc::new(ValueOrigin::Synthetic {
940 source: config_origin.clone(),
941 transform: format!("nesting for object param '{child_name}'"),
942 });
943 let val = Self::new(Value::Object(Map::new()), origin);
944 config_object.insert(child_name.to_owned(), val);
945 }
946
947 let Value::Object(target_object) =
948 &mut config_object.get_mut(child_name).unwrap().inner
949 else {
950 unreachable!(); };
952 target_object.extend(matching_fields);
953 }
954 }
955 }
956
957 #[tracing::instrument(level = "debug", skip_all)]
962 fn nest_array_params(&mut self, schema: &ConfigSchema) {
963 for (prefix, config_data) in schema.iter_ll() {
964 let Some(config_object) = self.get_mut(prefix) else {
965 continue;
966 };
967 let config_origin = &config_object.origin;
968 let Value::Object(config_object) = &mut config_object.inner else {
969 continue;
970 };
971
972 for param in config_data.metadata.params {
973 if !param.expecting.contains(BasicTypes::ARRAY)
974 || param.expecting.contains(BasicTypes::OBJECT)
975 {
976 continue;
979 }
980 if config_object.contains_key(param.name) {
981 continue;
983 }
984
985 let matching_fields: BTreeMap<_, _> = config_object
986 .iter()
987 .filter_map(|(name, field)| {
988 let stripped_name = Self::strip_prefix(name, param.name)?;
989 let idx: usize = stripped_name.parse().ok()?;
990 Some((idx, field.clone()))
991 })
992 .collect();
993 let Some(&last_idx) = matching_fields.keys().next_back() else {
994 continue; };
996
997 if last_idx != matching_fields.len() - 1 {
998 tracing::info!(
999 prefix = prefix.0,
1000 config = ?config_data.metadata.ty,
1001 param = param.rust_field_name,
1002 indexes = ?matching_fields.keys().copied().collect::<Vec<_>>(),
1003 "indexes for array nesting are not sequential"
1004 );
1005 continue;
1006 }
1007
1008 tracing::trace!(
1009 prefix = prefix.0,
1010 config = ?config_data.metadata.ty,
1011 param = param.rust_field_name,
1012 len = matching_fields.len(),
1013 "nesting for array param"
1014 );
1015
1016 let origin = Arc::new(ValueOrigin::Synthetic {
1017 source: config_origin.clone(),
1018 transform: format!("nesting for array param '{}'", param.name),
1019 });
1020 let array_items = matching_fields.into_values().collect();
1021 let val = Self::new(Value::Array(array_items), origin);
1022 config_object.insert(param.name.to_owned(), val);
1023 }
1024 }
1025 }
1026
1027 #[tracing::instrument(level = "debug", skip_all)]
1031 fn nest_kvs(kvs: Map, schema: &ConfigSchema, source_origin: &Arc<ValueOrigin>) -> Self {
1032 let mut dest = Self {
1033 inner: Value::Object(Map::new()),
1034 origin: source_origin.clone(),
1035 };
1036
1037 for (key, value) in kvs {
1038 let mut key_prefix = key.as_str();
1047 while !key_prefix.is_empty() {
1048 for (param_path, expecting) in schema.params_with_kv_path(key_prefix) {
1049 let should_copy = key_prefix == key || expecting.contains(BasicTypes::OBJECT);
1050 if should_copy {
1051 tracing::trace!(
1052 param_path = param_path.0,
1053 ?expecting,
1054 key,
1055 key_prefix,
1056 "copied key–value entry"
1057 );
1058 dest.copy_kv_entry(source_origin, param_path, &key, value.clone());
1059 }
1060 }
1061
1062 key_prefix = match key_prefix.rsplit_once('_') {
1063 Some((prefix, _)) => prefix,
1064 None => break,
1065 };
1066 }
1067
1068 for config_path in schema.shorthand_configs_with_kv_path(&key) {
1070 tracing::trace!(
1071 config_path = config_path.0,
1072 key,
1073 "copied key–value entry as enum config shorthand"
1074 );
1075 dest.copy_kv_entry(source_origin, config_path, &key, value.clone());
1076 }
1077
1078 let Some((key_prefix, maybe_idx)) = key.rsplit_once('_') else {
1080 continue;
1081 };
1082 if !maybe_idx.bytes().all(|ch| ch.is_ascii_digit()) {
1083 continue;
1084 }
1085 for (param_path, expecting) in schema.params_with_kv_path(key_prefix) {
1086 if expecting.contains(BasicTypes::ARRAY) && !expecting.contains(BasicTypes::OBJECT)
1087 {
1088 dest.copy_kv_entry(source_origin, param_path, &key, value.clone());
1089 }
1090 }
1091 }
1092 dest
1093 }
1094
1095 fn copy_kv_entry(
1096 &mut self,
1097 source_origin: &Arc<ValueOrigin>,
1098 param_path: Pointer<'_>,
1099 key: &str,
1100 value: WithOrigin,
1101 ) {
1102 let (parent, _) = param_path.split_last().unwrap();
1104 let field_name_start = if parent.0.is_empty() {
1105 parent.0.len()
1106 } else {
1107 parent.0.len() + 1 };
1109 let field_name = key[field_name_start..].to_owned();
1110
1111 let origin = Arc::new(ValueOrigin::Synthetic {
1112 source: source_origin.clone(),
1113 transform: format!("nesting kv entries for '{param_path}'"),
1114 });
1115 self.ensure_object(parent, |_| origin.clone())
1116 .insert(field_name, value);
1117 }
1118
1119 fn guided_merge(&mut self, overrides: Self, schema: &ConfigSchema, current_path: Pointer<'_>) {
1121 match (&mut self.inner, overrides.inner) {
1122 (Value::Object(this), Value::Object(other))
1123 if !schema.contains_canonical_param(current_path) =>
1124 {
1125 for (key, value) in other {
1126 if let Some(existing_value) = this.get_mut(&key) {
1127 let child_path = current_path.join(&key);
1128 existing_value.guided_merge(value, schema, Pointer(&child_path));
1129 } else {
1130 this.insert(key, value);
1131 }
1132 }
1133 }
1134 (this, value) => {
1135 *this = value;
1136 self.origin = overrides.origin;
1137 }
1138 }
1139 }
1140}