返回 CodeWhale
resolve.rs
根目录 / crates / config / src / resolve.rs
1 //! Ranked settings resolution with provenance.
2 //!
3 //! One rank table for every setting: the highest-ranked layer that names a
4 //! scalar wins outright (scalars replace, never merge); map layers
5 //! deep-merge with the highest rank winning per leaf; secrets travel by
6 //! reference only — a [`SecretRef`] names the source and id, never the
7 //! value, so a resolved row can say *which* secret won without carrying it.
8 //!
9 //! Layers with no producer today (managed policy files, project settings)
10 //! simply contribute no entries; the rank is the contract, and an empty
11 //! layer abstains rather than vetoing.
12
13 use std::collections::BTreeMap;
14
15 /// Where a setting value came from, highest rank first.
16 ///
17 /// The order is the product rule: policy beats the CLI flag, the flag beats
18 /// a session override, overrides beat files, and files beat the default.
19 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20 pub enum Layer {
21 /// Organization / managed policy. No file producer yet.
22 ManagedPolicy,
23 /// Per-run `-c key=value` CLI flag (lands with the config CLI slice).
24 CliFlag,
25 /// Session override (`/set`, `/config <key> <value>` without `--save`).
26 SessionOverride,
27 /// Project settings. No file producer yet.
28 ProjectConfig,
29 /// `settings.toml` (user config).
30 UserConfig,
31 /// The schema default.
32 Default,
33 }
34
35 impl Layer {
36 /// Stable wire name for receipts and row labels.
37 #[must_use]
38 pub fn name(self) -> &'static str {
39 match self {
40 Layer::ManagedPolicy => "policy",
41 Layer::CliFlag => "cli",
42 Layer::SessionOverride => "session",
43 Layer::ProjectConfig => "project",
44 Layer::UserConfig => "user",
45 Layer::Default => "default",
46 }
47 }
48 }
49
50 /// A secret by reference: the layer that resolved it plus where to find it.
51 /// The value itself never enters the resolver, a row, or a receipt.
52 #[derive(Debug, Clone, PartialEq, Eq)]
53 pub struct SecretRef {
54 /// Which layer named this secret.
55 pub layer: Layer,
56 /// Where it lives: `env:DASHSCOPE_API_KEY`, `keychain:codewhale/...`, …
57 pub reference: String,
58 }
59
60 impl SecretRef {
61 #[must_use]
62 pub fn new(layer: Layer, reference: impl Into<String>) -> Self {
63 Self {
64 layer,
65 reference: reference.into(),
66 }
67 }
68 }
69
70 /// The winning layer for one scalar key, with every layer that named it.
71 /// Empty input means the schema default wins unopposed.
72 #[must_use]
73 pub fn resolve_scalar<'a>(
74 candidates: impl IntoIterator<Item = (Layer, &'a str)>,
75 ) -> (Layer, Vec<Layer>) {
76 let mut best: Option<Layer> = None;
77 let mut named: Vec<Layer> = Vec::new();
78 for (layer, _) in candidates {
79 if !named.contains(&layer) {
80 named.push(layer);
81 }
82 if best.is_none_or(|top| layer < top) {
83 best = Some(layer);
84 }
85 }
86 named.sort();
87 (best.unwrap_or(Layer::Default), named)
88 }
89
90 /// Deep-merge map layers: every key present anywhere survives, and per key
91 /// the highest-ranked layer's value wins. Lower layers contribute only the
92 /// keys nobody above them named.
93 #[must_use]
94 pub fn merge_maps(layers: &BTreeMap<Layer, BTreeMap<String, String>>) -> BTreeMap<String, String> {
95 let mut merged = BTreeMap::new();
96 let mut order: Vec<Layer> = layers.keys().copied().collect();
97 order.sort();
98 for layer in order {
99 if let Some(map) = layers.get(&layer) {
100 for (key, value) in map {
101 merged.entry(key.clone()).or_insert_with(|| value.clone());
102 }
103 }
104 }
105 merged
106 }
107
108 /// Which layer supplied each merged key. Same walk as [`merge_maps`], kept
109 /// as a separate pass so a row can name the winner per key.
110 #[must_use]
111 pub fn merge_provenance(
112 layers: &BTreeMap<Layer, BTreeMap<String, String>>,
113 ) -> BTreeMap<String, Layer> {
114 let mut provenance = BTreeMap::new();
115 let mut order: Vec<Layer> = layers.keys().copied().collect();
116 order.sort();
117 for layer in order {
118 if let Some(map) = layers.get(&layer) {
119 for key in map.keys() {
120 provenance.entry(key.clone()).or_insert(layer);
121 }
122 }
123 }
124 provenance
125 }
126
127 #[cfg(test)]
128 mod tests {
129 use super::*;
130
131 #[test]
132 fn rank_order_is_policy_over_cli_over_session_over_files_over_default() {
133 let layers = [
134 Layer::ManagedPolicy,
135 Layer::CliFlag,
136 Layer::SessionOverride,
137 Layer::ProjectConfig,
138 Layer::UserConfig,
139 Layer::Default,
140 ];
141 let mut sorted = layers;
142 sorted.sort();
143 assert_eq!(sorted, layers);
144 }
145
146 #[test]
147 fn scalar_resolution_picks_the_highest_ranked_layer() {
148 let (winner, named) = resolve_scalar([
149 (Layer::Default, "bottom"),
150 (Layer::UserConfig, "user"),
151 (Layer::SessionOverride, "session"),
152 ]);
153 assert_eq!(winner, Layer::SessionOverride);
154 assert_eq!(
155 named,
156 vec![Layer::SessionOverride, Layer::UserConfig, Layer::Default]
157 );
158 }
159
160 #[test]
161 fn scalar_resolution_with_no_candidates_falls_back_to_default() {
162 let (winner, named) = resolve_scalar([]);
163 assert_eq!(winner, Layer::Default);
164 assert!(named.is_empty());
165 }
166
167 #[test]
168 fn scalar_values_replace_never_merge() {
169 // The loser's text is evidence only; the winner's value is untouched.
170 let (winner, _) = resolve_scalar([(Layer::UserConfig, "user"), (Layer::CliFlag, "cli")]);
171 assert_eq!(winner, Layer::CliFlag);
172 }
173
174 #[test]
175 fn map_merge_keeps_every_key_with_highest_rank_winning_per_key() {
176 let layers = BTreeMap::from([
177 (
178 Layer::UserConfig,
179 BTreeMap::from([
180 ("a".to_string(), "user-a".to_string()),
181 ("b".to_string(), "user-b".to_string()),
182 ]),
183 ),
184 (
185 Layer::ProjectConfig,
186 BTreeMap::from([("b".to_string(), "project-b".to_string())]),
187 ),
188 ]);
189 let merged = merge_maps(&layers);
190 assert_eq!(merged.get("a").map(String::as_str), Some("user-a"));
191 assert_eq!(merged.get("b").map(String::as_str), Some("project-b"));
192 let provenance = merge_provenance(&layers);
193 assert_eq!(provenance.get("a"), Some(&Layer::UserConfig));
194 assert_eq!(provenance.get("b"), Some(&Layer::ProjectConfig));
195 }
196
197 #[test]
198 fn secret_refs_carry_no_value() {
199 let secret = SecretRef::new(Layer::UserConfig, "env:DASHSCOPE_API_KEY");
200 let debug = format!("{secret:?}");
201 assert!(debug.contains("env:DASHSCOPE_API_KEY"));
202 assert_eq!(secret.layer, Layer::UserConfig);
203 }
204
205 #[test]
206 fn layer_names_are_stable() {
207 assert_eq!(Layer::ManagedPolicy.name(), "policy");
208 assert_eq!(Layer::CliFlag.name(), "cli");
209 assert_eq!(Layer::SessionOverride.name(), "session");
210 assert_eq!(Layer::ProjectConfig.name(), "project");
211 assert_eq!(Layer::UserConfig.name(), "user");
212 assert_eq!(Layer::Default.name(), "default");
213 }
214 }
215
215 lines RUST