smart_config/
visit.rs

1//! Visitor pattern for configs.
2
3use std::{any, any::Any, mem};
4
5use crate::{SerializerOptions, metadata::ConfigMetadata, utils::JsonObject, value::Pointer};
6
7/// Visitor of configuration parameters in a particular configuration.
8#[doc(hidden)] // API is not stable yet
9pub trait ConfigVisitor {
10    /// Visits an enumeration tag in the configuration, if the config is an enumeration.
11    /// Called once per configuration before any other calls.
12    fn visit_tag(&mut self, variant_index: usize);
13
14    /// Visits a parameter providing its value for inspection. This will be called for all params in a struct config,
15    /// and for params associated with the active tag variant in an enum config.
16    fn visit_param(&mut self, param_index: usize, value: &dyn any::Any);
17
18    /// Visits a nested configuration. Similarly to params, this will be called for all nested / flattened configs in a struct config,
19    /// and just for ones associated with the active tag variant in an enum config.
20    fn visit_nested_config(&mut self, config_index: usize, config: &dyn VisitConfig);
21
22    /// Visits an optional nested configuration.
23    ///
24    /// The default implementation calls [`Self::visit_nested_config()`] if `config` is `Some(_)`,
25    /// and does nothing if it is `None`.
26    fn visit_nested_opt_config(&mut self, config_index: usize, config: Option<&dyn VisitConfig>) {
27        if let Some(config) = config {
28            self.visit_nested_config(config_index, config);
29        }
30    }
31}
32
33/// Configuration that can be visited (e.g., to inspect its parameters in a generic way).
34///
35/// This is a supertrait for [`DescribeConfig`](trait@crate::DescribeConfig) that is automatically derived
36/// via [`derive(DescribeConfig)`](macro@crate::DescribeConfig).
37pub trait VisitConfig {
38    /// Performs the visit.
39    fn visit_config(&self, visitor: &mut dyn ConfigVisitor);
40}
41
42/// Serializing [`ConfigVisitor`]. Can be used to serialize configs to the JSON object model.
43#[derive(Debug)]
44pub(crate) struct Serializer {
45    metadata: &'static ConfigMetadata,
46    // Only filled when serializing into a flat object.
47    current_prefix: Option<String>,
48    json: JsonObject,
49    options: SerializerOptions,
50}
51
52impl Serializer {
53    /// Creates a serializer dynamically.
54    pub(crate) fn new(
55        metadata: &'static ConfigMetadata,
56        prefix: &str,
57        options: SerializerOptions,
58    ) -> Self {
59        Self {
60            metadata,
61            current_prefix: options.flat.then(|| prefix.to_owned()),
62            json: serde_json::Map::new(),
63            options,
64        }
65    }
66
67    /// Unwraps the contained JSON model.
68    pub(crate) fn into_inner(self) -> JsonObject {
69        self.json
70    }
71
72    fn insert(&mut self, param_name: &str, value: serde_json::Value) {
73        let key = if let Some(prefix) = &self.current_prefix {
74            Pointer(prefix).join(param_name)
75        } else {
76            param_name.to_owned()
77        };
78        self.json.insert(key, value);
79    }
80}
81
82impl ConfigVisitor for Serializer {
83    fn visit_tag(&mut self, variant_index: usize) {
84        let tag = self.metadata.tag.unwrap();
85        let tag_variant = &tag.variants[variant_index];
86
87        let should_insert = !self.options.diff_with_default
88            || tag
89                .default_variant
90                .is_none_or(|default_variant| default_variant.rust_name != tag_variant.rust_name);
91        if should_insert {
92            self.insert(tag.param.name, tag_variant.name.into());
93        }
94    }
95
96    fn visit_param(&mut self, param_index: usize, value: &dyn Any) {
97        let param = &self.metadata.params[param_index];
98        // TODO: this exposes secret values, but we cannot easily avoid serialization because of `should_insert` filtering below.
99        let mut value = param.deserializer.serialize_param(value);
100
101        // If a parameter has a fallback, it should be inserted regardless of whether it has the default value;
102        // otherwise, since fallbacks have higher priority than defaults, the parameter value may be unexpected after parsing
103        // the produced JSON.
104        let should_insert = !self.options.diff_with_default
105            || param.fallback.is_some()
106            || param.default_value_json().as_ref() != Some(&value);
107        if should_insert {
108            if let (Some(placeholder), true) = (
109                &self.options.secret_placeholder,
110                param.type_description().contains_secrets(),
111            ) {
112                value = placeholder.clone().into();
113            }
114            self.insert(param.name, value);
115        }
116    }
117
118    fn visit_nested_config(&mut self, config_index: usize, config: &dyn VisitConfig) {
119        let nested_metadata = &self.metadata.nested_configs[config_index];
120        let prev_metadata = mem::replace(&mut self.metadata, nested_metadata.meta);
121
122        if nested_metadata.name.is_empty() {
123            config.visit_config(self);
124        } else if let Some(prefix) = &mut self.current_prefix {
125            let new_prefix = Pointer(prefix).join(nested_metadata.name);
126            let prev_prefix = mem::replace(prefix, new_prefix);
127            config.visit_config(self);
128            self.current_prefix = Some(prev_prefix);
129        } else {
130            let mut prev_json = mem::take(&mut self.json);
131            config.visit_config(self);
132
133            let nested_json = mem::take(&mut self.json);
134            let should_insert = !self.options.diff_with_default || !nested_json.is_empty();
135            if should_insert {
136                prev_json.insert(nested_metadata.name.to_owned(), nested_json.into());
137            }
138            self.json = prev_json;
139        }
140
141        self.metadata = prev_metadata;
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use std::collections::HashMap;
148
149    use super::*;
150    use crate::{
151        DescribeConfig,
152        metadata::ConfigMetadata,
153        testonly::{ConfigWithNesting, DefaultingConfig, EnumConfig, NestedConfig, SimpleEnum},
154    };
155
156    #[derive(Debug)]
157    struct PersistingVisitor {
158        metadata: &'static ConfigMetadata,
159        tag: Option<&'static str>,
160        param_values: HashMap<&'static str, serde_json::Value>,
161    }
162
163    impl PersistingVisitor {
164        fn new(metadata: &'static ConfigMetadata) -> Self {
165            Self {
166                metadata,
167                tag: None,
168                param_values: HashMap::new(),
169            }
170        }
171    }
172
173    impl ConfigVisitor for PersistingVisitor {
174        fn visit_tag(&mut self, variant_index: usize) {
175            assert!(self.tag.is_none());
176            self.tag = Some(self.metadata.tag.unwrap().variants[variant_index].rust_name);
177        }
178
179        fn visit_param(&mut self, param_index: usize, value: &dyn any::Any) {
180            let param = &self.metadata.params[param_index];
181            let prev_value = self
182                .param_values
183                .insert(param.name, param.deserializer.serialize_param(value));
184            assert!(
185                prev_value.is_none(),
186                "Param value {} is visited twice",
187                param.name
188            );
189        }
190
191        fn visit_nested_config(&mut self, _config_index: usize, _config: &dyn VisitConfig) {
192            // Do nothing
193        }
194    }
195
196    #[test]
197    fn visiting_struct_config() {
198        let config = DefaultingConfig::default();
199        let mut visitor = PersistingVisitor::new(&DefaultingConfig::DESCRIPTION);
200        config.visit_config(&mut visitor);
201
202        assert_eq!(visitor.tag, None);
203        assert_eq!(
204            visitor.param_values,
205            HashMap::from([
206                ("float", serde_json::Value::Null),
207                ("set", serde_json::json!([])),
208                ("int", 12_u32.into()),
209                ("url", "https://example.com/".into())
210            ])
211        );
212    }
213
214    #[test]
215    fn visiting_enum_config() {
216        let config = EnumConfig::First;
217        let mut visitor = PersistingVisitor::new(&EnumConfig::DESCRIPTION);
218        config.visit_config(&mut visitor);
219        assert_eq!(visitor.tag, Some("First"));
220        assert_eq!(visitor.param_values, HashMap::new());
221
222        let config = EnumConfig::Nested(NestedConfig::default_nested());
223        let mut visitor = PersistingVisitor::new(&EnumConfig::DESCRIPTION);
224        config.visit_config(&mut visitor);
225        assert_eq!(visitor.tag, Some("Nested"));
226        assert_eq!(visitor.param_values, HashMap::new());
227
228        let config = EnumConfig::WithFields {
229            string: Some("test".to_owned()),
230            flag: true,
231            set: [1].into(),
232        };
233        let mut visitor = PersistingVisitor::new(&EnumConfig::DESCRIPTION);
234        config.visit_config(&mut visitor);
235        assert_eq!(visitor.tag, Some("WithFields"));
236        assert_eq!(
237            visitor.param_values,
238            HashMap::from([
239                ("string", "test".into()),
240                ("flag", true.into()),
241                ("set", serde_json::json!([1]))
242            ])
243        );
244    }
245
246    #[test]
247    fn serializing_config() {
248        let config = EnumConfig::WithFields {
249            string: Some("test".to_owned()),
250            flag: true,
251            set: [1].into(),
252        };
253        let json = SerializerOptions::default().serialize(&config);
254        assert_eq!(
255            serde_json::Value::from(json),
256            serde_json::json!({
257                "type": "WithFields",
258                "string": "test",
259                "flag": true,
260                "set": [1]
261            })
262        );
263    }
264
265    #[test]
266    fn serializing_nested_config() {
267        let config = ConfigWithNesting {
268            value: 23,
269            merged: String::new(),
270            nested: NestedConfig {
271                simple_enum: SimpleEnum::First,
272                other_int: 42,
273                map: HashMap::new(),
274            },
275        };
276
277        let json = SerializerOptions::default().serialize(&config);
278        assert_eq!(
279            serde_json::Value::from(json),
280            serde_json::json!({
281                "value": 23,
282                "merged": "",
283                "nested": {
284                    "renamed": "first",
285                    "other_int": 42,
286                    "map": {},
287                },
288            })
289        );
290
291        let json = SerializerOptions::diff_with_default().serialize(&config);
292        assert_eq!(
293            serde_json::Value::from(json),
294            serde_json::json!({
295                "value": 23,
296                "nested": {
297                    "renamed": "first",
298                },
299            })
300        );
301
302        let json = SerializerOptions::default().flat(true).serialize(&config);
303        assert_eq!(
304            serde_json::Value::from(json),
305            serde_json::json!({
306                "value": 23,
307                "merged": "",
308                "nested.renamed": "first",
309                "nested.other_int": 42,
310                "nested.map": {},
311            })
312        );
313
314        let json = SerializerOptions::diff_with_default()
315            .flat(true)
316            .serialize(&config);
317        assert_eq!(
318            serde_json::Value::from(json),
319            serde_json::json!({
320                "value": 23,
321                "nested.renamed": "first",
322            })
323        );
324    }
325}