apollo_config 0.19.0-rc.2

A library for handling node configuration.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
//! Utils for serializing config objects into flatten map and json file.
//! The elements structure is:
//!
//! ```json
//! "conf1.conf2.conf3.param_name": {
//!     "description": "Param description.",
//!     "value": json_value
//! }
//! ```
//! In addition, supports pointers in the map, with the structure:
//!
//! ```json
//! "conf1.conf2.conf3.param_name": {
//!     "description": "Param description.",
//!     "pointer_target": "target_param_path"
//! }
//! ```
//!
//! Supports required params. A required param has no default value, but the type of value that the
//! user must set:
//! ```json
//! "conf1.conf2.conf3.param_name: {
//!     "description": "Param description.",
//!     "required_type": Number
//! }
//! ```
//!
//! Supports flags for optional params and sub-configs. An optional param / sub-config has an
//! "#is_none" indicator that determines whether to take its value or to deserialize it to None:
//! ```json
//! "conf1.conf2.#is_none": {
//!     "description": "Flag for an optional field.",
//!     "value": true
//! }
//! ```

use std::collections::{BTreeMap, HashSet};

use apollo_infra_utils::dumping::serialize_to_file;
use itertools::chain;
use serde::Serialize;
use serde_json::{json, Value};

use crate::{
    ConfigError,
    ParamPath,
    ParamPrivacy,
    ParamPrivacyInput,
    SerializationType,
    SerializedContent,
    SerializedParam,
    FIELD_SEPARATOR,
    IS_NONE_MARK,
};

/// Type alias for a pointer parameter and its serialized representation.
type PointerTarget = (ParamPath, SerializedParam);

/// Type alias for a set of pointing parameters.
pub type Pointers = HashSet<ParamPath>;

/// Detailing pointers in the config map.
pub type ConfigPointers = Vec<(PointerTarget, Pointers)>;

/// Given a set of paths that are configuration of the same struct type, makes all the paths point
/// to the same target.
pub fn generate_struct_pointer<T: SerializeConfig>(
    target_prefix: ParamPath,
    default_instance: &T,
    pointer_prefixes: HashSet<ParamPath>,
) -> ConfigPointers {
    let mut res = ConfigPointers::new();
    for (param_path, serialized_param) in default_instance.dump() {
        let pointer_target = serialized_param_to_pointer_target(
            target_prefix.clone(),
            &param_path,
            &serialized_param,
        );
        let pointers = pointer_prefixes
            .iter()
            .map(|pointer| chain_param_paths(&[pointer, &param_path]))
            .collect();

        res.push((pointer_target, pointers));
    }
    res
}

/// Generates pointers for an optional struct:
/// - All fields under `target_prefix` (using `default_instance` if provided, otherwise
///   `T::default()`), pointed to by each prefix in `pointer_prefixes`.
/// - The optional flag's default is derived from whether a default instance was provided: `true` if
///   `default_instance` is `None`, otherwise `false`.
pub fn generate_optional_struct_pointer<T: SerializeConfig + Default>(
    target_prefix: ParamPath,
    default_instance: Option<&T>,
    pointer_prefixes: HashSet<ParamPath>,
) -> ConfigPointers {
    // Use provided instance if given; otherwise use a local default that lives for this call.
    let default_instance_value = match default_instance {
        Some(instance) => instance,
        None => &T::default(),
    };
    let mut res = generate_struct_pointer(
        target_prefix.clone(),
        default_instance_value,
        pointer_prefixes.clone(),
    );

    // Optional flag default derives from whether an instance was provided.
    let pointer_target = ser_is_param_none(target_prefix.as_str(), default_instance.is_none());
    let pointing_params: Pointers = pointer_prefixes
        .into_iter()
        .map(|prefix| format!("{prefix}{FIELD_SEPARATOR}{IS_NONE_MARK}"))
        .collect();
    res.push((pointer_target, pointing_params));

    res
}

// Converts a serialized param to a pointer target.
fn serialized_param_to_pointer_target(
    target_prefix: ParamPath,
    param_path: &ParamPath,
    serialized_param: &SerializedParam,
) -> PointerTarget {
    let full_param_path = chain_param_paths(&[&target_prefix, param_path]);
    if serialized_param.is_required() {
        let description = serialized_param
            .description
            .strip_prefix(REQUIRED_PARAM_DESCRIPTION_PREFIX)
            .unwrap_or(&serialized_param.description)
            .trim_start();
        ser_pointer_target_required_param(
            &full_param_path,
            serialized_param.content.get_serialization_type().unwrap(),
            description,
        )
    } else {
        let default_value = match &serialized_param.content {
            SerializedContent::DefaultValue(value) => value,
            SerializedContent::PointerTarget(_) => panic!("Pointers to pointer is not supported."),
            // We already checked that the param is not required, so it must be a generated param.
            SerializedContent::ParamType(_) => {
                panic!("Generated pointer targets are not supported.")
            }
        };
        ser_pointer_target_param(&full_param_path, default_value, &serialized_param.description)
    }
}

fn chain_param_paths(param_paths: &[&str]) -> ParamPath {
    param_paths.join(FIELD_SEPARATOR)
}

/// Serialization for configs.
pub trait SerializeConfig {
    /// Conversion of a configuration to a mapping of flattened parameters to their descriptions and
    /// values.
    /// Note, in the case of None sub configs, their elements will not be included in the flatten
    /// map.
    fn dump(&self) -> BTreeMap<ParamPath, SerializedParam>;

    /// Serialization of a configuration into a JSON file.
    /// Takes a vector of {target pointer params, SerializedParam, and vector of pointing params},
    /// adds the target pointer params with the description and a value, and replaces the value of
    /// the pointing params to contain only the name of the target they point to.
    /// Fails if a param is not pointing to a same-named pointer target nor whitelisted.
    ///
    /// # Example
    ///
    /// ```
    /// # use std::collections::{BTreeMap, HashSet};
    ///
    /// # use apollo_config::dumping::{ser_param, SerializeConfig};
    /// # use apollo_config::{ParamPath, ParamPrivacyInput, SerializedParam};
    /// # use serde::{Deserialize, Serialize};
    /// # use tempfile::TempDir;
    ///
    /// #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
    /// struct ConfigExample {
    ///     key: usize,
    /// }
    ///
    /// impl SerializeConfig for ConfigExample {
    ///     fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
    ///         BTreeMap::from([ser_param(
    ///             "key",
    ///             &self.key,
    ///             "This is key description.",
    ///             ParamPrivacyInput::Public,
    ///         )])
    ///     }
    /// }
    ///
    /// let dir = TempDir::new().unwrap();
    /// let file_path = dir.path().join("config.json");
    /// ConfigExample { key: 42 }.dump_to_file(&vec![], &HashSet::new(), file_path.to_str().unwrap());
    /// ```
    /// Note, in the case of None sub configs, their elements will not be included in the file.
    fn dump_to_file(
        &self,
        config_pointers: &ConfigPointers,
        non_pointer_params: &Pointers,
        file_path: &str,
    ) -> Result<(), ConfigError> {
        let combined_map =
            combine_config_map_and_pointers(self.dump(), config_pointers, non_pointer_params)?;
        serialize_to_file(&combined_map, file_path);
        Ok(())
    }
}

/// Prepends `sub_config_name` to the ParamPath for each entry in `sub_config_dump`.
/// In order to load from a dump properly, `sub_config_name` must match the field's name for the
/// struct this function is called from.
pub fn prepend_sub_config_name(
    sub_config_dump: BTreeMap<ParamPath, SerializedParam>,
    sub_config_name: &str,
) -> BTreeMap<ParamPath, SerializedParam> {
    BTreeMap::from_iter(
        sub_config_dump.into_iter().map(|(field_name, val)| {
            (format!("{sub_config_name}{FIELD_SEPARATOR}{field_name}"), val)
        }),
    )
}

// Serializes a parameter of a config.
fn common_ser_param(
    name: &str,
    content: SerializedContent,
    description: &str,
    privacy: ParamPrivacy,
) -> (String, SerializedParam) {
    (name.to_owned(), SerializedParam { description: description.to_owned(), content, privacy })
}

/// Serializes a single param of a config.
/// The returned pair is designed to be an input to a dumped config map.
pub fn ser_param<T: Serialize>(
    name: &str,
    value: &T,
    description: &str,
    privacy: ParamPrivacyInput,
) -> (String, SerializedParam) {
    common_ser_param(
        name,
        SerializedContent::DefaultValue(json!(value)),
        description,
        privacy.into(),
    )
}

/// Serializes expected type for a single required param of a config.
/// The returned pair is designed to be an input to a dumped config map.
pub fn ser_required_param(
    name: &str,
    serialization_type: SerializationType,
    description: &str,
    privacy: ParamPrivacyInput,
) -> (String, SerializedParam) {
    common_ser_param(
        name,
        SerializedContent::ParamType(serialization_type),
        required_param_description(description).as_str(),
        privacy.into(),
    )
}

/// Serializes expected type for a single param of a config that the system may generate. The
/// generation should be defined as serde default field attribute.
/// The returned pair is designed to be an input to a dumped config map.
pub fn ser_generated_param(
    name: &str,
    serialization_type: SerializationType,
    description: &str,
    privacy: ParamPrivacyInput,
) -> (String, SerializedParam) {
    common_ser_param(
        name,
        SerializedContent::ParamType(serialization_type),
        format!("{description} If no value is provided, the system will generate one.").as_str(),
        privacy.into(),
    )
}

/// Serializes optional sub-config fields (or default fields for None sub-config) and adds an
/// "#is_none" flag.
pub fn ser_optional_sub_config<T: SerializeConfig + Default>(
    optional_config: &Option<T>,
    name: &str,
) -> BTreeMap<ParamPath, SerializedParam> {
    chain!(
        BTreeMap::from_iter([ser_is_param_none(name, optional_config.is_none())]),
        prepend_sub_config_name(
            match optional_config {
                None => T::default().dump(),
                Some(config) => config.dump(),
            },
            name,
        ),
    )
    .collect()
}

/// Serializes optional param value (or default value for None param) and adds an "#is_none" flag.
pub fn ser_optional_param<T: Serialize>(
    optional_param: &Option<T>,
    default_value: T,
    name: &str,
    description: &str,
    privacy: ParamPrivacyInput,
) -> BTreeMap<ParamPath, SerializedParam> {
    BTreeMap::from([
        ser_is_param_none(name, optional_param.is_none()),
        ser_param(
            name,
            match optional_param {
                Some(param) => param,
                None => &default_value,
            },
            description,
            privacy,
        ),
    ])
}

/// Serializes is_none flag for a param.
pub fn ser_is_param_none(name: &str, is_none: bool) -> (String, SerializedParam) {
    common_ser_param(
        format!("{name}{FIELD_SEPARATOR}{IS_NONE_MARK}").as_str(),
        SerializedContent::DefaultValue(json!(is_none)),
        "Flag for an optional field.",
        ParamPrivacy::TemporaryValue,
    )
}

/// Serializes a pointer target param of a config.
///
/// # Example
/// Create config_pointers vector to be used in `dump_to_file`:
/// ```
/// # use apollo_config::dumping::ser_pointer_target_param;
///
/// let pointer_target_param = ser_pointer_target_param(
///     "shared_param",
///     &("param".to_string()),
///     "A string parameter description.",
/// );
/// let pointer_param_paths =
///     vec!["conf1.conf2.same_param".to_owned(), "conf3.same_param".to_owned()];
/// let config_pointers = vec![(pointer_target_param, pointer_param_paths)];
/// ```
pub fn ser_pointer_target_param<T: Serialize>(
    name: &str,
    value: &T,
    description: &str,
) -> (String, SerializedParam) {
    common_ser_param(
        name,
        SerializedContent::DefaultValue(json!(value)),
        description,
        ParamPrivacy::TemporaryValue,
    )
}

/// Serializes a pointer target for a required param of a config.
pub fn ser_pointer_target_required_param(
    name: &str,
    serialization_type: SerializationType,
    description: &str,
) -> (String, SerializedParam) {
    common_ser_param(
        name,
        SerializedContent::ParamType(serialization_type),
        required_param_description(description).as_str(),
        ParamPrivacy::TemporaryValue,
    )
}

/// Takes a config map and a vector of target parameters with their serialized representations.
/// Adds each target param to the config map.
/// Updates entries in the map to point to these targets, replacing values of entries that match
/// the target parameter paths to contain only the name of the target they point to.
/// Fails if a param is not pointing to a same-named pointer target nor whitelisted.
pub fn combine_config_map_and_pointers(
    mut config_map: BTreeMap<ParamPath, SerializedParam>,
    pointers: &ConfigPointers,
    non_pointer_params: &Pointers,
) -> Result<Value, ConfigError> {
    // Update config with target params.
    for ((target_param, serialized_pointer), pointing_params_vec) in pointers {
        // Insert target param.
        config_map.insert(target_param.clone(), serialized_pointer.clone());

        // Update pointing params to point at the target param.
        for pointing_param in pointing_params_vec {
            let pointing_serialized_param =
                config_map.get(pointing_param).ok_or(ConfigError::PointerSourceNotFound {
                    pointing_param: pointing_param.to_owned(),
                })?;
            config_map.insert(
                pointing_param.to_owned(),
                SerializedParam {
                    description: pointing_serialized_param.description.clone(),
                    content: SerializedContent::PointerTarget(target_param.to_owned()),
                    privacy: pointing_serialized_param.privacy.clone(),
                },
            );
        }
    }

    verify_pointing_params_by_name(&config_map, pointers, non_pointer_params);

    Ok(json!(config_map))
}

/// Creates a set of pointing params, ensuring no duplications.
pub fn set_pointing_param_paths(param_path_list: &[&str]) -> Pointers {
    let mut param_paths = HashSet::new();
    for &param_path in param_path_list {
        assert!(
            param_paths.insert(param_path.to_string()),
            "Duplicate parameter path found: {param_path}"
        );
    }
    param_paths
}

/// Prefix for required params description.
pub(crate) const REQUIRED_PARAM_DESCRIPTION_PREFIX: &str = "A required param!";

pub(crate) fn required_param_description(description: &str) -> String {
    format!("{REQUIRED_PARAM_DESCRIPTION_PREFIX} {description}")
}

/// Verifies that params whose name matches a pointer target either point at it, or are whitelisted.
fn verify_pointing_params_by_name(
    config_map: &BTreeMap<ParamPath, SerializedParam>,
    pointers: &ConfigPointers,
    non_pointer_params: &Pointers,
) {
    // Iterate over the config, check that all parameters whose name matches a pointer target either
    // point at it or are in the whitelist.
    config_map.iter().for_each(|(param_path, serialized_param)| {
        for ((target_param, _), _) in pointers {
            // Check if the param name matches a pointer target, and that it is not in the
            // whitelist.
            if param_path.ends_with(format!("{FIELD_SEPARATOR}{target_param}").as_str())
                && !non_pointer_params.contains(param_path)
            {
                // Check that the param points to the target param.
                assert!(
                    serialized_param.content
                        == SerializedContent::PointerTarget(target_param.to_owned()),
                    "The target param {param_path} should point to {target_param}, or to be \
                     whitelisted. You can use set_pointing_param_paths to point it to a value."
                );
            };
        }
    });
}