1use std::{
10 collections::{BTreeSet, HashMap},
11 ffi::OsStr,
12 fs,
13 path::Component,
14 path::{Path, PathBuf},
15 sync::Arc,
16};
17
18use confique::{
19 Config, FileFormat, Layer,
20 meta::{Expr, FieldKind, LeafKind, MapKey, Meta},
21};
22use figment::{
23 Figment, Metadata, Profile, Provider, Source,
24 providers::{Env, Format, Json, Toml, Yaml},
25 value::{Dict, Map, Uncased},
26};
27use schemars::{JsonSchema, generate::SchemaSettings};
28use serde_json::Value;
29use tracing::trace;
30
31use crate::{
32 ConfigError, ConfigSource, ConfigTree, ConfigTreeOptions, IncludeOrder, absolutize_lexical,
33 collect_template_targets, normalize_lexical, select_template_source,
34};
35
36pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
40
41pub trait ConfigSchema: Config + Sized {
48 fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf>;
61
62 fn template_path_for_section(section_path: &[&str]) -> Option<PathBuf> {
78 let _ = section_path;
79 None
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum ConfigFormat {
86 Yaml,
89 Toml,
91 Json,
93}
94
95impl ConfigFormat {
96 pub fn from_path(path: impl AsRef<Path>) -> Self {
108 match path.as_ref().extension().and_then(OsStr::to_str) {
109 Some("toml") => Self::Toml,
110 Some("json" | "json5") => Self::Json,
111 Some("yaml" | "yml") | Some(_) | None => Self::Yaml,
112 }
113 }
114
115 pub fn as_file_format(self) -> FileFormat {
121 match self {
122 Self::Yaml => FileFormat::Yaml,
123 Self::Toml => FileFormat::Toml,
124 Self::Json => FileFormat::Json5,
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct ConfigTemplateTarget {
132 pub path: PathBuf,
134 pub content: String,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct ConfigSchemaTarget {
141 pub path: PathBuf,
143 pub content: String,
145}
146
147#[derive(Clone)]
156pub struct ConfiqueEnvProvider {
157 env: Env,
158 path_to_env: Arc<HashMap<String, String>>,
159}
160
161impl ConfiqueEnvProvider {
162 pub fn new<S>() -> Self
172 where
173 S: Config,
174 {
175 let mut env_to_path = HashMap::<String, String>::new();
176 let mut path_to_env = HashMap::<String, String>::new();
177
178 collect_env_mapping(&S::META, "", &mut env_to_path, &mut path_to_env);
179
180 let env_to_path = Arc::new(env_to_path);
181 let path_to_env = Arc::new(path_to_env);
182 let map_for_filter = Arc::clone(&env_to_path);
183
184 let env = Env::raw().filter_map(move |env_key| {
185 let lookup_key = env_key.as_str().to_ascii_uppercase();
186
187 map_for_filter
188 .get(&lookup_key)
189 .cloned()
190 .map(Uncased::from_owned)
191 });
192
193 Self { env, path_to_env }
194 }
195}
196
197impl Provider for ConfiqueEnvProvider {
198 fn metadata(&self) -> Metadata {
199 let path_to_env = Arc::clone(&self.path_to_env);
200
201 Metadata::named("environment variable").interpolater(move |_profile, keys| {
202 let path = keys.join(".");
203
204 path_to_env.get(&path).cloned().unwrap_or(path)
205 })
206 }
207
208 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
209 self.env.data()
210 }
211}
212
213pub fn load_config<S>(path: impl AsRef<Path>) -> ConfigResult<S>
237where
238 S: ConfigSchema,
239{
240 let (config, _) = load_config_with_figment::<S>(path)?;
241 Ok(config)
242}
243
244pub fn load_config_with_figment<S>(path: impl AsRef<Path>) -> ConfigResult<(S, Figment)>
262where
263 S: ConfigSchema,
264{
265 let figment = build_config_figment::<S>(path)?;
266 let config = load_config_from_figment::<S>(&figment)?;
267
268 Ok((config, figment))
269}
270
271pub fn build_config_figment<S>(path: impl AsRef<Path>) -> ConfigResult<Figment>
288where
289 S: ConfigSchema,
290{
291 let path = path.as_ref();
292 load_dotenv_for_path(path)?;
293
294 let tree = load_layer_tree::<S>(path)?;
295 let mut figment = Figment::new();
296
297 for node in tree.nodes().iter().rev() {
298 figment = merge_file_provider(figment, node.path());
299 }
300
301 Ok(figment.merge(ConfiqueEnvProvider::new::<S>()))
302}
303
304pub fn load_config_from_figment<S>(figment: &Figment) -> ConfigResult<S>
321where
322 S: ConfigSchema,
323{
324 let runtime_layer: <S as Config>::Layer = figment.extract()?;
325 let config = S::from_layer(runtime_layer.with_fallback(S::Layer::default_values()))?;
326
327 trace_config_sources::<S>(figment);
328
329 Ok(config)
330}
331
332pub fn load_layer<S>(path: &Path) -> ConfigResult<<S as Config>::Layer>
347where
348 S: ConfigSchema,
349{
350 Ok(figment_for_file(path).extract()?)
351}
352
353fn load_layer_tree<S>(path: &Path) -> ConfigResult<ConfigTree<<S as Config>::Layer>>
354where
355 S: ConfigSchema,
356{
357 Ok(ConfigTreeOptions::default()
358 .include_order(IncludeOrder::Reverse)
359 .load(
360 path,
361 |path| -> ConfigResult<ConfigSource<<S as Config>::Layer>> {
362 let layer = load_layer::<S>(path)?;
363 let include_paths = S::include_paths(&layer);
364 Ok(ConfigSource::new(layer, include_paths))
365 },
366 )?)
367}
368
369fn merge_file_provider(figment: Figment, path: &Path) -> Figment {
370 match ConfigFormat::from_path(path) {
371 ConfigFormat::Yaml => figment.merge(Yaml::file_exact(path)),
372 ConfigFormat::Toml => figment.merge(Toml::file_exact(path)),
373 ConfigFormat::Json => figment.merge(Json::file_exact(path)),
374 }
375}
376
377fn figment_for_file(path: &Path) -> Figment {
378 merge_file_provider(Figment::new(), path)
379}
380
381fn root_config_schema<S>() -> ConfigResult<Value>
382where
383 S: JsonSchema,
384{
385 let generator = SchemaSettings::draft07().into_generator();
386 let schema = generator.into_root_schema_for::<S>();
387 let mut schema = serde_json::to_value(schema)?;
388 remove_required_recursively(&mut schema);
389
390 Ok(schema)
391}
392
393fn schema_json(schema: &Value) -> ConfigResult<String> {
394 let mut json = serde_json::to_string_pretty(schema)?;
395 ensure_single_trailing_newline(&mut json);
396 Ok(json)
397}
398
399fn remove_required_recursively(value: &mut Value) {
400 match value {
401 Value::Object(object) => {
402 object.remove("required");
403
404 for (key, child) in object.iter_mut() {
405 if is_schema_map_key(key) {
406 remove_required_from_schema_map(child);
407 } else {
408 remove_required_recursively(child);
409 }
410 }
411 }
412 Value::Array(items) => {
413 for item in items {
414 remove_required_recursively(item);
415 }
416 }
417 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
418 }
419}
420
421fn is_schema_map_key(key: &str) -> bool {
422 matches!(
423 key,
424 "$defs" | "definitions" | "properties" | "patternProperties"
425 )
426}
427
428fn remove_required_from_schema_map(value: &mut Value) {
429 match value {
430 Value::Object(object) => {
431 for schema in object.values_mut() {
432 remove_required_recursively(schema);
433 }
434 }
435 _ => remove_required_recursively(value),
436 }
437}
438
439fn section_schema_for_path(root_schema: &Value, section_path: &[&str]) -> Option<Value> {
440 let mut current = root_schema;
441
442 for section in section_path {
443 current = current.get("properties")?.get(*section)?;
444 current = resolve_schema_reference(root_schema, current).unwrap_or(current);
445 }
446
447 Some(standalone_section_schema(root_schema, current))
448}
449
450fn resolve_schema_reference<'a>(root_schema: &'a Value, schema: &'a Value) -> Option<&'a Value> {
451 if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
452 return resolve_json_pointer_ref(root_schema, reference);
453 }
454
455 schema
456 .get("allOf")
457 .and_then(Value::as_array)
458 .and_then(|schemas| schemas.first())
459 .and_then(|schema| schema.get("$ref"))
460 .and_then(Value::as_str)
461 .and_then(|reference| resolve_json_pointer_ref(root_schema, reference))
462}
463
464fn resolve_json_pointer_ref<'a>(root_schema: &'a Value, reference: &str) -> Option<&'a Value> {
465 let pointer = reference.strip_prefix('#')?;
466 root_schema.pointer(pointer)
467}
468
469fn standalone_section_schema(root_schema: &Value, section_schema: &Value) -> Value {
470 let mut section_schema = section_schema.clone();
471 let Some(object) = section_schema.as_object_mut() else {
472 return section_schema;
473 };
474
475 if let Some(schema_uri) = root_schema.get("$schema") {
476 object
477 .entry("$schema".to_owned())
478 .or_insert_with(|| schema_uri.clone());
479 }
480
481 if let Some(definitions) = root_schema.get("definitions") {
482 object
483 .entry("definitions".to_owned())
484 .or_insert_with(|| definitions.clone());
485 }
486
487 if let Some(defs) = root_schema.get("$defs") {
488 object
489 .entry("$defs".to_owned())
490 .or_insert_with(|| defs.clone());
491 }
492
493 section_schema
494}
495
496fn schema_path_for_section(root_schema_path: &Path, section_path: &[&str]) -> PathBuf {
497 let Some((last, parents)) = section_path.split_last() else {
498 return root_schema_path.to_path_buf();
499 };
500
501 let mut path = root_schema_path
502 .parent()
503 .unwrap_or_else(|| Path::new("."))
504 .to_path_buf();
505
506 for parent in parents {
507 path.push(*parent);
508 }
509
510 path.push(format!("{}.schema.json", *last));
511 path
512}
513
514fn schema_for_output_path<S>(
515 full_schema: &Value,
516 section_path: &[&'static str],
517) -> ConfigResult<Value>
518where
519 S: ConfigSchema,
520{
521 let mut schema = if section_path.is_empty() {
522 full_schema.clone()
523 } else {
524 section_schema_for_path(full_schema, section_path).ok_or_else(|| {
525 std::io::Error::new(
526 std::io::ErrorKind::InvalidData,
527 format!(
528 "failed to extract JSON Schema for config section {}",
529 section_path.join(".")
530 ),
531 )
532 })?
533 };
534
535 remove_child_section_properties::<S>(&mut schema, section_path);
536 prune_unused_schema_maps(&mut schema);
537
538 Ok(schema)
539}
540
541fn remove_child_section_properties<S>(schema: &mut Value, section_path: &[&'static str])
542where
543 S: ConfigSchema,
544{
545 let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else {
546 return;
547 };
548
549 for child_section_path in immediate_child_section_paths(&S::META, section_path) {
550 if let Some(child_name) = child_section_path.last() {
551 properties.remove(*child_name);
552 }
553 }
554}
555
556fn prune_unused_schema_maps(schema: &mut Value) {
557 let mut definitions = BTreeSet::new();
558 let mut defs = BTreeSet::new();
559
560 collect_schema_refs(schema, false, &mut definitions, &mut defs);
561
562 loop {
563 let previous_len = definitions.len() + defs.len();
564 collect_transitive_schema_refs(schema, &mut definitions, &mut defs);
565
566 if definitions.len() + defs.len() == previous_len {
567 break;
568 }
569 }
570
571 retain_schema_map(schema, "definitions", &definitions);
572 retain_schema_map(schema, "$defs", &defs);
573}
574
575fn collect_transitive_schema_refs(
576 schema: &Value,
577 definitions: &mut BTreeSet<String>,
578 defs: &mut BTreeSet<String>,
579) {
580 let current_definitions = definitions.clone();
581 let current_defs = defs.clone();
582 let mut referenced_definitions = BTreeSet::new();
583 let mut referenced_defs = BTreeSet::new();
584
585 if let Some(schema_map) = schema.get("definitions").and_then(Value::as_object) {
586 for name in ¤t_definitions {
587 if let Some(schema) = schema_map.get(name) {
588 collect_schema_refs(
589 schema,
590 true,
591 &mut referenced_definitions,
592 &mut referenced_defs,
593 );
594 }
595 }
596 }
597
598 if let Some(schema_map) = schema.get("$defs").and_then(Value::as_object) {
599 for name in ¤t_defs {
600 if let Some(schema) = schema_map.get(name) {
601 collect_schema_refs(
602 schema,
603 true,
604 &mut referenced_definitions,
605 &mut referenced_defs,
606 );
607 }
608 }
609 }
610
611 definitions.extend(referenced_definitions);
612 defs.extend(referenced_defs);
613}
614
615fn collect_schema_refs(
616 value: &Value,
617 include_schema_maps: bool,
618 definitions: &mut BTreeSet<String>,
619 defs: &mut BTreeSet<String>,
620) {
621 match value {
622 Value::Object(object) => {
623 if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
624 collect_schema_ref(reference, definitions, defs);
625 }
626
627 for (key, child) in object {
628 if !include_schema_maps && matches!(key.as_str(), "definitions" | "$defs") {
629 continue;
630 }
631
632 collect_schema_refs(child, include_schema_maps, definitions, defs);
633 }
634 }
635 Value::Array(items) => {
636 for item in items {
637 collect_schema_refs(item, include_schema_maps, definitions, defs);
638 }
639 }
640 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
641 }
642}
643
644fn collect_schema_ref(
645 reference: &str,
646 definitions: &mut BTreeSet<String>,
647 defs: &mut BTreeSet<String>,
648) {
649 if let Some(name) = schema_ref_name(reference, "#/definitions/") {
650 definitions.insert(name);
651 } else if let Some(name) = schema_ref_name(reference, "#/$defs/") {
652 defs.insert(name);
653 }
654}
655
656fn schema_ref_name(reference: &str, prefix: &str) -> Option<String> {
657 let name = reference.strip_prefix(prefix)?.split('/').next()?;
658 Some(decode_json_pointer_token(name))
659}
660
661fn decode_json_pointer_token(token: &str) -> String {
662 token.replace("~1", "/").replace("~0", "~")
663}
664
665fn retain_schema_map(schema: &mut Value, key: &str, used_names: &BTreeSet<String>) {
666 let Some(object) = schema.as_object_mut() else {
667 return;
668 };
669
670 let Some(schema_map) = object.get_mut(key).and_then(Value::as_object_mut) else {
671 return;
672 };
673
674 schema_map.retain(|name, _| used_names.contains(name));
675
676 if schema_map.is_empty() {
677 object.remove(key);
678 }
679}
680
681pub fn write_config_schema<S>(output_path: impl AsRef<Path>) -> ConfigResult<()>
702where
703 S: JsonSchema,
704{
705 let schema = root_config_schema::<S>()?;
706 let json = schema_json(&schema)?;
707
708 write_template(output_path.as_ref(), &json)
709}
710
711pub fn config_schema_targets_for_path<S>(
732 output_path: impl AsRef<Path>,
733) -> ConfigResult<Vec<ConfigSchemaTarget>>
734where
735 S: ConfigSchema + JsonSchema,
736{
737 let output_path = output_path.as_ref();
738 let full_schema = root_config_schema::<S>()?;
739 let root_schema = schema_for_output_path::<S>(&full_schema, &[])?;
740 let mut targets = vec![ConfigSchemaTarget {
741 path: output_path.to_path_buf(),
742 content: schema_json(&root_schema)?,
743 }];
744
745 for section_path in nested_section_paths(&S::META) {
746 let schema_path = schema_path_for_section(output_path, §ion_path);
747 let section_schema = schema_for_output_path::<S>(&full_schema, §ion_path)?;
748
749 targets.push(ConfigSchemaTarget {
750 path: schema_path,
751 content: schema_json(§ion_schema)?,
752 });
753 }
754
755 Ok(targets)
756}
757
758pub fn write_config_schemas<S>(output_path: impl AsRef<Path>) -> ConfigResult<()>
778where
779 S: ConfigSchema + JsonSchema,
780{
781 for target in config_schema_targets_for_path::<S>(output_path)? {
782 write_template(&target.path, &target.content)?;
783 }
784
785 Ok(())
786}
787
788pub fn template_for_path<S>(path: impl AsRef<Path>) -> ConfigResult<String>
804where
805 S: ConfigSchema,
806{
807 let template = match ConfigFormat::from_path(path.as_ref()) {
808 ConfigFormat::Yaml => confique::yaml::template::<S>(yaml_options()),
809 ConfigFormat::Toml => confique::toml::template::<S>(toml_options()),
810 ConfigFormat::Json => confique::json5::template::<S>(json5_options()),
811 };
812
813 Ok(template)
814}
815
816pub fn template_targets_for_paths<S>(
838 config_path: impl AsRef<Path>,
839 output_path: impl AsRef<Path>,
840) -> ConfigResult<Vec<ConfigTemplateTarget>>
841where
842 S: ConfigSchema,
843{
844 let output_path = output_path.as_ref();
845 let source_path = select_template_source(config_path, output_path);
846 let root_source_path = absolutize_lexical(source_path)?;
847 let output_base_dir = output_path.parent().unwrap_or_else(|| Path::new("."));
848
849 let template_targets = collect_template_targets(
850 &root_source_path,
851 output_path,
852 |node_source_path| -> ConfigResult<Vec<PathBuf>> {
853 let mut include_paths = template_source_include_paths::<S>(node_source_path)?;
854
855 if include_paths.is_empty() {
856 include_paths =
857 default_child_include_paths::<S>(&root_source_path, node_source_path);
858 }
859
860 Ok(include_paths)
861 },
862 )?;
863
864 let split_paths = template_targets
865 .iter()
866 .filter_map(|target| {
867 section_path_for_target::<S>(output_base_dir, target.target_path())
868 .filter(|section_path| !section_path.is_empty())
869 })
870 .collect::<Vec<_>>();
871
872 template_targets
873 .into_iter()
874 .map(|target| {
875 let (_, target_path, include_paths) = target.into_parts();
876 let section_path =
877 section_path_for_target::<S>(output_base_dir, &target_path).unwrap_or_default();
878 Ok(ConfigTemplateTarget {
879 content: template_for_target::<S>(
880 &target_path,
881 &include_paths,
882 §ion_path,
883 &split_paths,
884 )?,
885 path: target_path,
886 })
887 })
888 .collect()
889}
890
891pub fn template_targets_for_paths_with_schema<S>(
915 config_path: impl AsRef<Path>,
916 output_path: impl AsRef<Path>,
917 schema_path: impl AsRef<Path>,
918) -> ConfigResult<Vec<ConfigTemplateTarget>>
919where
920 S: ConfigSchema,
921{
922 let output_path = output_path.as_ref();
923 let output_base_dir = output_path.parent().unwrap_or_else(|| Path::new("."));
924 let schema_path = schema_path.as_ref();
925
926 template_targets_for_paths::<S>(config_path, output_path)?
927 .into_iter()
928 .map(|mut target| {
929 let schema_path =
930 schema_path_for_template_target::<S>(output_base_dir, &target.path, schema_path);
931 target.content =
932 template_with_schema_directive(&target.path, &schema_path, &target.content)?;
933 Ok(target)
934 })
935 .collect()
936}
937
938pub fn write_config_templates<S>(
956 config_path: impl AsRef<Path>,
957 output_path: impl AsRef<Path>,
958) -> ConfigResult<()>
959where
960 S: ConfigSchema,
961{
962 for target in template_targets_for_paths::<S>(config_path, output_path)? {
963 write_template(&target.path, &target.content)?;
964 }
965
966 Ok(())
967}
968
969pub fn write_config_templates_with_schema<S>(
993 config_path: impl AsRef<Path>,
994 output_path: impl AsRef<Path>,
995 schema_path: impl AsRef<Path>,
996) -> ConfigResult<()>
997where
998 S: ConfigSchema,
999{
1000 for target in
1001 template_targets_for_paths_with_schema::<S>(config_path, output_path, schema_path)?
1002 {
1003 write_template(&target.path, &target.content)?;
1004 }
1005
1006 Ok(())
1007}
1008
1009pub(crate) fn write_template(path: &Path, content: &str) -> ConfigResult<()> {
1020 if let Some(parent) = path
1021 .parent()
1022 .filter(|parent| !parent.as_os_str().is_empty())
1023 {
1024 fs::create_dir_all(parent)?;
1025 }
1026
1027 fs::write(path, content)?;
1028 Ok(())
1029}
1030
1031pub(crate) fn resolve_config_template_output(output: Option<PathBuf>) -> ConfigResult<PathBuf> {
1042 let current_dir = std::env::current_dir()?;
1043 let output = output.unwrap_or_else(|| PathBuf::from("config.example.yaml"));
1044 let output = if output.is_absolute() {
1045 output
1046 } else {
1047 current_dir.join(output)
1048 };
1049
1050 Ok(normalize_lexical(output))
1051}
1052
1053fn template_source_include_paths<S>(path: &Path) -> ConfigResult<Vec<PathBuf>>
1054where
1055 S: ConfigSchema,
1056{
1057 if !path.exists() {
1058 return Ok(Vec::new());
1059 }
1060
1061 match load_layer::<S>(path) {
1062 Ok(layer) => Ok(S::include_paths(&layer)),
1063 Err(_) => load_include_paths_only(path),
1064 }
1065}
1066
1067fn load_include_paths_only(path: &Path) -> ConfigResult<Vec<PathBuf>> {
1068 match figment_for_file(path).extract_inner::<Vec<PathBuf>>("include") {
1069 Ok(paths) => Ok(paths),
1070 Err(error) if error.missing() => Ok(Vec::new()),
1071 Err(error) => Err(error.into()),
1072 }
1073}
1074
1075fn schema_path_for_template_target<S>(
1076 root_base_dir: &Path,
1077 target_path: &Path,
1078 root_schema_path: &Path,
1079) -> PathBuf
1080where
1081 S: ConfigSchema,
1082{
1083 section_path_for_target::<S>(root_base_dir, target_path)
1084 .filter(|section_path| !section_path.is_empty())
1085 .map(|section_path| schema_path_for_section(root_schema_path, §ion_path))
1086 .unwrap_or_else(|| root_schema_path.to_path_buf())
1087}
1088
1089fn template_with_schema_directive(
1090 template_path: &Path,
1091 schema_path: &Path,
1092 content: &str,
1093) -> ConfigResult<String> {
1094 let schema_ref = schema_reference_for_path(template_path, schema_path)?;
1095 let directive = match ConfigFormat::from_path(template_path) {
1096 ConfigFormat::Yaml => Some(format!("# yaml-language-server: $schema={schema_ref}")),
1097 ConfigFormat::Toml => Some(format!("#:schema {schema_ref}")),
1098 ConfigFormat::Json => None,
1099 };
1100
1101 let Some(directive) = directive else {
1102 return Ok(content.to_owned());
1103 };
1104
1105 Ok(format!("{directive}\n\n{content}"))
1106}
1107
1108fn schema_reference_for_path(template_path: &Path, schema_path: &Path) -> ConfigResult<String> {
1109 let template_path = absolutize_lexical(template_path)?;
1110 let schema_path = absolutize_lexical(schema_path)?;
1111 let template_dir = template_path.parent().unwrap_or_else(|| Path::new("."));
1112 let relative_path = relative_path_from(&schema_path, template_dir);
1113 Ok(render_schema_reference(&relative_path))
1114}
1115
1116fn relative_path_from(path: &Path, base: &Path) -> PathBuf {
1117 let path_components = path.components().collect::<Vec<_>>();
1118 let base_components = base.components().collect::<Vec<_>>();
1119
1120 let mut common_len = 0;
1121 while common_len < path_components.len()
1122 && common_len < base_components.len()
1123 && path_components[common_len] == base_components[common_len]
1124 {
1125 common_len += 1;
1126 }
1127
1128 if common_len == 0 {
1129 return path.to_path_buf();
1130 }
1131
1132 let mut relative = PathBuf::new();
1133 for component in &base_components[common_len..] {
1134 if matches!(component, Component::Normal(_)) {
1135 relative.push("..");
1136 }
1137 }
1138
1139 for component in &path_components[common_len..] {
1140 relative.push(component.as_os_str());
1141 }
1142
1143 if relative.as_os_str().is_empty() {
1144 PathBuf::from(".")
1145 } else {
1146 relative
1147 }
1148}
1149
1150fn render_schema_reference(path: &Path) -> String {
1151 let value = path.to_string_lossy().replace('\\', "/");
1152 if path.is_absolute() || value.starts_with("../") || value.starts_with("./") {
1153 value
1154 } else {
1155 format!("./{value}")
1156 }
1157}
1158
1159fn template_for_target<S>(
1160 path: &Path,
1161 include_paths: &[PathBuf],
1162 section_path: &[&'static str],
1163 split_paths: &[Vec<&'static str>],
1164) -> ConfigResult<String>
1165where
1166 S: ConfigSchema,
1167{
1168 if ConfigFormat::from_path(path) != ConfigFormat::Yaml || split_paths.is_empty() {
1169 return template_for_path_with_includes::<S>(path, include_paths);
1170 }
1171
1172 Ok(render_yaml_template(
1173 &S::META,
1174 include_paths,
1175 section_path,
1176 split_paths,
1177 ))
1178}
1179
1180fn default_child_include_paths<S>(root_source_path: &Path, node_source_path: &Path) -> Vec<PathBuf>
1181where
1182 S: ConfigSchema,
1183{
1184 let root_base_dir = root_source_path.parent().unwrap_or_else(|| Path::new("."));
1185 let section_path =
1186 section_path_for_target::<S>(root_base_dir, node_source_path).unwrap_or_default();
1187 let source_base_dir = node_source_path.parent().unwrap_or_else(|| Path::new("."));
1188
1189 immediate_child_section_paths(&S::META, §ion_path)
1190 .into_iter()
1191 .map(|child_section_path| {
1192 let child_path =
1193 root_base_dir.join(template_path_for_section::<S>(&child_section_path));
1194 path_relative_to(&child_path, source_base_dir)
1195 })
1196 .collect()
1197}
1198
1199fn collect_env_mapping(
1200 meta: &'static Meta,
1201 prefix: &str,
1202 env_to_path: &mut HashMap<String, String>,
1203 path_to_env: &mut HashMap<String, String>,
1204) {
1205 for field in meta.fields {
1206 let path = if prefix.is_empty() {
1207 field.name.to_owned()
1208 } else {
1209 format!("{prefix}.{}", field.name)
1210 };
1211
1212 match field.kind {
1213 FieldKind::Leaf { env: Some(env), .. } => {
1214 env_to_path.insert(env.to_ascii_uppercase(), path.clone());
1215 path_to_env.insert(path, env.to_owned());
1216 }
1217 FieldKind::Leaf { env: None, .. } => {}
1218 FieldKind::Nested { meta } => {
1219 collect_env_mapping(meta, &path, env_to_path, path_to_env);
1220 }
1221 }
1222 }
1223}
1224
1225fn load_dotenv_for_path(path: &Path) -> ConfigResult<()> {
1226 let path = absolutize_lexical(path)?;
1227 let mut current_dir = path.parent();
1228
1229 while let Some(dir) = current_dir {
1230 let dotenv_path = dir.join(".env");
1231 if dotenv_path.try_exists()? {
1232 dotenvy::from_path(&dotenv_path)?;
1233 break;
1234 }
1235 current_dir = dir.parent();
1236 }
1237
1238 Ok(())
1239}
1240
1241fn section_path_for_target<S>(root_base_dir: &Path, target_path: &Path) -> Option<Vec<&'static str>>
1242where
1243 S: ConfigSchema,
1244{
1245 let normalized_target = normalize_lexical(target_path);
1246
1247 for section_path in nested_section_paths(&S::META) {
1248 let section_target =
1249 normalize_lexical(root_base_dir.join(template_path_for_section::<S>(§ion_path)));
1250 if section_target == normalized_target {
1251 return Some(section_path);
1252 }
1253 }
1254
1255 infer_section_path_from_path::<S>(target_path)
1256}
1257
1258fn template_path_for_section<S>(section_path: &[&str]) -> PathBuf
1259where
1260 S: ConfigSchema,
1261{
1262 if let Some(path) = S::template_path_for_section(section_path) {
1263 return path;
1264 }
1265
1266 let Some((last, parent_path)) = section_path.split_last() else {
1267 return PathBuf::new();
1268 };
1269
1270 if parent_path.is_empty() {
1271 return PathBuf::from("config").join(format!("{last}.yaml"));
1272 }
1273
1274 let parent_template_path = template_path_for_section::<S>(parent_path);
1275 parent_template_path
1276 .with_extension("")
1277 .join(format!("{last}.yaml"))
1278}
1279
1280fn path_relative_to(path: &Path, base: &Path) -> PathBuf {
1281 match path.strip_prefix(base) {
1282 Ok(relative) if !relative.as_os_str().is_empty() => relative.to_path_buf(),
1283 _ => path.to_path_buf(),
1284 }
1285}
1286
1287fn nested_section_paths(meta: &'static Meta) -> Vec<Vec<&'static str>> {
1288 let mut paths = Vec::new();
1289 collect_nested_section_paths(meta, &mut Vec::new(), &mut paths);
1290 paths
1291}
1292
1293fn collect_nested_section_paths(
1294 meta: &'static Meta,
1295 prefix: &mut Vec<&'static str>,
1296 paths: &mut Vec<Vec<&'static str>>,
1297) {
1298 for field in meta.fields {
1299 if let FieldKind::Nested { meta } = field.kind {
1300 prefix.push(field.name);
1301 paths.push(prefix.clone());
1302 collect_nested_section_paths(meta, prefix, paths);
1303 prefix.pop();
1304 }
1305 }
1306}
1307
1308fn immediate_child_section_paths(
1309 meta: &'static Meta,
1310 section_path: &[&'static str],
1311) -> Vec<Vec<&'static str>> {
1312 let Some(section_meta) = meta_at_path(meta, section_path) else {
1313 return Vec::new();
1314 };
1315
1316 section_meta
1317 .fields
1318 .iter()
1319 .filter_map(|field| match field.kind {
1320 FieldKind::Nested { .. } => {
1321 let mut path = section_path.to_vec();
1322 path.push(field.name);
1323 Some(path)
1324 }
1325 FieldKind::Leaf { .. } => None,
1326 })
1327 .collect()
1328}
1329
1330pub fn trace_config_sources<S>(figment: &Figment)
1348where
1349 S: ConfigSchema,
1350{
1351 if !tracing::enabled!(tracing::Level::TRACE) {
1352 return;
1353 }
1354
1355 for path in leaf_config_paths(&S::META) {
1356 let source = config_source_for_path(figment, &path);
1357 trace!(target: "rust_config_tree::config", config_key = %path, source = %source, "config source");
1358 }
1359}
1360
1361fn config_source_for_path(figment: &Figment, path: &str) -> String {
1362 match figment.find_metadata(path) {
1363 Some(metadata) => render_metadata(metadata, path),
1364 None => "confique default or unset optional field".to_owned(),
1365 }
1366}
1367
1368fn render_metadata(metadata: &Metadata, path: &str) -> String {
1369 match &metadata.source {
1370 Some(Source::File(path)) => format!("{} `{}`", metadata.name, path.display()),
1371 Some(Source::Custom(value)) => format!("{} `{value}`", metadata.name),
1372 Some(Source::Code(location)) => {
1373 format!("{} {}:{}", metadata.name, location.file(), location.line())
1374 }
1375 Some(_) => metadata.name.to_string(),
1376 None => {
1377 let parts = path.split('.').collect::<Vec<_>>();
1378 let native = metadata.interpolate(&Profile::Default, &parts);
1379
1380 format!("{} `{native}`", metadata.name)
1381 }
1382 }
1383}
1384
1385fn leaf_config_paths(meta: &'static Meta) -> Vec<String> {
1386 let mut paths = Vec::new();
1387 collect_leaf_config_paths(meta, "", &mut paths);
1388 paths
1389}
1390
1391fn collect_leaf_config_paths(meta: &'static Meta, prefix: &str, paths: &mut Vec<String>) {
1392 for field in meta.fields {
1393 let path = if prefix.is_empty() {
1394 field.name.to_owned()
1395 } else {
1396 format!("{prefix}.{}", field.name)
1397 };
1398
1399 match field.kind {
1400 FieldKind::Leaf { .. } => paths.push(path),
1401 FieldKind::Nested { meta } => collect_leaf_config_paths(meta, &path, paths),
1402 }
1403 }
1404}
1405
1406fn infer_section_path_from_path<S>(path: &Path) -> Option<Vec<&'static str>>
1407where
1408 S: ConfigSchema,
1409{
1410 let path_tokens = normalized_path_tokens(path);
1411 let file_token = path
1412 .file_stem()
1413 .and_then(OsStr::to_str)
1414 .map(normalize_token)
1415 .unwrap_or_default();
1416
1417 nested_section_paths(&S::META)
1418 .into_iter()
1419 .filter_map(|section_path| {
1420 let score = section_path_score(§ion_path, &path_tokens, &file_token);
1421 (score > 0).then_some((score, section_path))
1422 })
1423 .max_by_key(|(score, section_path)| (*score, section_path.len()))
1424 .map(|(_, section_path)| section_path)
1425}
1426
1427fn normalized_path_tokens(path: &Path) -> Vec<String> {
1428 path.components()
1429 .filter_map(|component| component.as_os_str().to_str())
1430 .map(|component| {
1431 Path::new(component)
1432 .file_stem()
1433 .and_then(OsStr::to_str)
1434 .unwrap_or(component)
1435 })
1436 .map(normalize_token)
1437 .filter(|component| !component.is_empty())
1438 .collect()
1439}
1440
1441fn normalize_token(token: &str) -> String {
1442 token
1443 .chars()
1444 .filter_map(|character| match character {
1445 '-' | ' ' => Some('_'),
1446 '_' => Some('_'),
1447 character if character.is_ascii_alphanumeric() => Some(character.to_ascii_lowercase()),
1448 _ => None,
1449 })
1450 .collect()
1451}
1452
1453fn section_path_score(section_path: &[&str], path_tokens: &[String], file_token: &str) -> usize {
1454 let section_tokens = section_path
1455 .iter()
1456 .map(|segment| normalize_token(segment))
1457 .collect::<Vec<_>>();
1458
1459 if path_tokens.ends_with(§ion_tokens) {
1460 return 1_000 + section_tokens.len();
1461 }
1462
1463 let Some(last_section_token) = section_tokens.last() else {
1464 return 0;
1465 };
1466
1467 if file_token == last_section_token {
1468 return 500 + section_tokens.len();
1469 }
1470
1471 if file_token.starts_with(last_section_token) || last_section_token.starts_with(file_token) {
1472 return 100 + last_section_token.len().min(file_token.len());
1473 }
1474
1475 0
1476}
1477
1478fn meta_at_path(meta: &'static Meta, section_path: &[&str]) -> Option<&'static Meta> {
1479 let mut current_meta = meta;
1480 for section in section_path {
1481 current_meta = current_meta.fields.iter().find_map(|field| {
1482 if field.name != *section {
1483 return None;
1484 }
1485
1486 match field.kind {
1487 FieldKind::Nested { meta } => Some(meta),
1488 FieldKind::Leaf { .. } => None,
1489 }
1490 })?;
1491 }
1492
1493 Some(current_meta)
1494}
1495
1496fn render_yaml_template(
1497 meta: &'static Meta,
1498 include_paths: &[PathBuf],
1499 section_path: &[&'static str],
1500 split_paths: &[Vec<&'static str>],
1501) -> String {
1502 let mut output = String::new();
1503 if !include_paths.is_empty() {
1504 output.push_str(&render_yaml_include(include_paths));
1505 output.push('\n');
1506 }
1507
1508 if section_path.is_empty() {
1509 render_yaml_fields(
1510 meta,
1511 &mut Vec::new(),
1512 split_paths,
1513 0,
1514 !include_paths.is_empty(),
1515 &mut output,
1516 );
1517 } else {
1518 render_yaml_section(meta, section_path, split_paths, &mut output);
1519 }
1520
1521 ensure_single_trailing_newline(&mut output);
1522 output
1523}
1524
1525fn render_yaml_section(
1526 meta: &'static Meta,
1527 section_path: &[&'static str],
1528 split_paths: &[Vec<&'static str>],
1529 output: &mut String,
1530) {
1531 let mut current_meta = meta;
1532 let mut current_path = Vec::new();
1533
1534 for (depth, section) in section_path.iter().enumerate() {
1535 write_yaml_indent(output, depth);
1536 output.push('#');
1537 output.push_str(section);
1538 output.push_str(":\n");
1539 current_path.push(*section);
1540
1541 let Some(next_meta) = meta_at_path(current_meta, &[*section]) else {
1542 return;
1543 };
1544 current_meta = next_meta;
1545 }
1546
1547 render_yaml_fields(
1548 current_meta,
1549 &mut current_path,
1550 split_paths,
1551 section_path.len(),
1552 false,
1553 output,
1554 );
1555}
1556
1557fn render_yaml_fields(
1558 meta: &'static Meta,
1559 current_path: &mut Vec<&'static str>,
1560 split_paths: &[Vec<&'static str>],
1561 depth: usize,
1562 skip_include_field: bool,
1563 output: &mut String,
1564) {
1565 let mut emitted_anything = false;
1566
1567 for field in meta.fields {
1568 let FieldKind::Leaf { env, kind } = field.kind else {
1569 continue;
1570 };
1571
1572 if skip_include_field && current_path.is_empty() && field.name == "include" {
1573 continue;
1574 }
1575
1576 if emitted_anything {
1577 output.push('\n');
1578 }
1579 emitted_anything = true;
1580 render_yaml_leaf(field.name, field.doc, env, kind, depth, output);
1581 }
1582
1583 for field in meta.fields {
1584 let FieldKind::Nested { meta } = field.kind else {
1585 continue;
1586 };
1587
1588 current_path.push(field.name);
1589 let split_exact = split_paths.iter().any(|path| path == current_path);
1590 let split_descendant = split_paths
1591 .iter()
1592 .any(|path| path.starts_with(current_path) && path.len() > current_path.len());
1593
1594 if split_exact {
1595 current_path.pop();
1596 continue;
1597 }
1598
1599 if emitted_anything {
1600 output.push('\n');
1601 }
1602 emitted_anything = true;
1603
1604 for doc in field.doc {
1605 write_yaml_indent(output, depth);
1606 output.push('#');
1607 output.push_str(doc);
1608 output.push('\n');
1609 }
1610 write_yaml_indent(output, depth);
1611 output.push_str(field.name);
1612 output.push_str(":\n");
1613
1614 let child_split_paths = if split_descendant { split_paths } else { &[] };
1615 render_yaml_fields(
1616 meta,
1617 current_path,
1618 child_split_paths,
1619 depth + 1,
1620 false,
1621 output,
1622 );
1623 current_path.pop();
1624 }
1625}
1626
1627fn render_yaml_leaf(
1628 name: &str,
1629 doc: &[&str],
1630 env: Option<&str>,
1631 kind: LeafKind,
1632 depth: usize,
1633 output: &mut String,
1634) {
1635 let mut emitted_doc_comment = false;
1636 for doc in doc {
1637 write_yaml_indent(output, depth);
1638 output.push('#');
1639 output.push_str(doc);
1640 output.push('\n');
1641 emitted_doc_comment = true;
1642 }
1643
1644 if let Some(env) = env {
1645 if emitted_doc_comment {
1646 write_yaml_indent(output, depth);
1647 output.push_str("#\n");
1648 }
1649 write_yaml_indent(output, depth);
1650 output.push_str("# Can also be specified via environment variable `");
1651 output.push_str(env);
1652 output.push_str("`.\n");
1653 }
1654
1655 match kind {
1656 LeafKind::Optional => {
1657 write_yaml_indent(output, depth);
1658 output.push('#');
1659 output.push_str(name);
1660 output.push_str(":\n");
1661 }
1662 LeafKind::Required { default } => {
1663 write_yaml_indent(output, depth);
1664 match default {
1665 Some(default) => {
1666 output.push_str("# Default value: ");
1667 output.push_str(&render_yaml_expr(&default));
1668 output.push('\n');
1669 write_yaml_indent(output, depth);
1670 output.push('#');
1671 output.push_str(name);
1672 output.push_str(": ");
1673 output.push_str(&render_yaml_expr(&default));
1674 output.push('\n');
1675 }
1676 None => {
1677 output.push_str("# Required! This value must be specified.\n");
1678 write_yaml_indent(output, depth);
1679 output.push('#');
1680 output.push_str(name);
1681 output.push_str(":\n");
1682 }
1683 }
1684 }
1685 }
1686}
1687
1688fn render_yaml_expr(expr: &Expr) -> String {
1689 match expr {
1690 Expr::Str(value) => render_plain_or_quoted_string(value),
1691 Expr::Float(value) => value.to_string(),
1692 Expr::Integer(value) => value.to_string(),
1693 Expr::Bool(value) => value.to_string(),
1694 Expr::Array(items) => {
1695 let items = items
1696 .iter()
1697 .map(render_yaml_expr)
1698 .collect::<Vec<_>>()
1699 .join(", ");
1700 format!("[{items}]")
1701 }
1702 Expr::Map(entries) => {
1703 let entries = entries
1704 .iter()
1705 .map(|entry| {
1706 format!(
1707 "{}: {}",
1708 render_yaml_map_key(&entry.key),
1709 render_yaml_expr(&entry.value)
1710 )
1711 })
1712 .collect::<Vec<_>>()
1713 .join(", ");
1714 format!("{{ {entries} }}")
1715 }
1716 _ => String::new(),
1717 }
1718}
1719
1720fn render_yaml_map_key(key: &MapKey) -> String {
1721 match key {
1722 MapKey::Str(value) => render_plain_or_quoted_string(value),
1723 MapKey::Float(value) => value.to_string(),
1724 MapKey::Integer(value) => value.to_string(),
1725 MapKey::Bool(value) => value.to_string(),
1726 _ => String::new(),
1727 }
1728}
1729
1730fn render_plain_or_quoted_string(value: &str) -> String {
1731 let needs_quotes = value.is_empty()
1732 || value.starts_with([
1733 ' ', '#', '{', '}', '[', ']', ',', '&', '*', '!', '|', '>', '\'', '"',
1734 ])
1735 || value.contains([':', '\n', '\r', '\t']);
1736
1737 if needs_quotes {
1738 quote_path(Path::new(value))
1739 } else {
1740 value.to_owned()
1741 }
1742}
1743
1744fn write_yaml_indent(output: &mut String, depth: usize) {
1745 for _ in 0..depth {
1746 output.push_str(" ");
1747 }
1748}
1749
1750fn ensure_single_trailing_newline(output: &mut String) {
1751 if output.ends_with('\n') {
1752 while output.ends_with("\n\n") {
1753 output.pop();
1754 }
1755 } else {
1756 output.push('\n');
1757 }
1758}
1759
1760fn template_for_path_with_includes<S>(
1761 path: &Path,
1762 include_paths: &[PathBuf],
1763) -> ConfigResult<String>
1764where
1765 S: ConfigSchema,
1766{
1767 let template = template_for_path::<S>(path)?;
1768 if include_paths.is_empty() {
1769 return Ok(template);
1770 }
1771
1772 let template = match ConfigFormat::from_path(path) {
1773 ConfigFormat::Yaml => {
1774 let template = strip_prefix_once(&template, "# Default value: []\n#include: []\n\n");
1775 format!("{}\n{template}", render_yaml_include(include_paths))
1776 }
1777 ConfigFormat::Toml => {
1778 let template = strip_prefix_once(&template, "# Default value: []\n#include = []\n\n");
1779 format!("{}\n{template}", render_toml_include(include_paths))
1780 }
1781 ConfigFormat::Json => {
1782 let body = template.strip_prefix("{\n").unwrap_or(&template);
1783 let body = strip_prefix_once(body, " // Default value: []\n //include: [],\n\n");
1784 format!("{{\n{}\n{body}", render_json5_include(include_paths))
1785 }
1786 };
1787
1788 Ok(template)
1789}
1790
1791fn render_yaml_include(paths: &[PathBuf]) -> String {
1792 let mut out = String::from("include:\n");
1793 for path in paths {
1794 out.push_str(" - ");
1795 out.push_str("e_path(path));
1796 out.push('\n');
1797 }
1798 out
1799}
1800
1801fn render_toml_include(paths: &[PathBuf]) -> String {
1802 let entries = paths
1803 .iter()
1804 .map(|path| quote_path(path))
1805 .collect::<Vec<_>>()
1806 .join(", ");
1807 format!("include = [{entries}]\n")
1808}
1809
1810fn render_json5_include(paths: &[PathBuf]) -> String {
1811 let mut out = String::from(" include: [\n");
1812 for path in paths {
1813 out.push_str(" ");
1814 out.push_str("e_path(path));
1815 out.push_str(",\n");
1816 }
1817 out.push_str(" ],\n");
1818 out
1819}
1820
1821fn quote_path(path: &Path) -> String {
1822 serde_json::to_string(&path.to_string_lossy()).expect("path string serialization cannot fail")
1823}
1824
1825fn strip_prefix_once<'a>(value: &'a str, prefix: &str) -> &'a str {
1826 value.strip_prefix(prefix).unwrap_or(value)
1827}
1828
1829fn yaml_options() -> confique::yaml::FormatOptions {
1830 let mut options = confique::yaml::FormatOptions::default();
1831 options.indent = 2;
1832 options.general.comments = true;
1833 options.general.env_keys = true;
1834 options.general.nested_field_gap = 1;
1835 options
1836}
1837
1838fn toml_options() -> confique::toml::FormatOptions {
1839 let mut options = confique::toml::FormatOptions::default();
1840 options.general.comments = true;
1841 options.general.env_keys = true;
1842 options.general.nested_field_gap = 1;
1843 options
1844}
1845
1846fn json5_options() -> confique::json5::FormatOptions {
1847 let mut options = confique::json5::FormatOptions::default();
1848 options.indent = 2;
1849 options.general.comments = true;
1850 options.general.env_keys = true;
1851 options.general.nested_field_gap = 1;
1852 options
1853}
1854
1855#[cfg(test)]
1856#[path = "unit_tests/config.rs"]
1857mod unit_tests;