nimbus/defaults.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use crate::error::Result;
6
7/// Simple trait to allow merging of similar objects.
8///
9/// Different names might be more applicable: merging, defaulting, patching.
10///
11/// In all cases: the `defaults` method takes a reference to a `self` and
12/// a `fallback`. The `self` acts as a patch over the `fallback`, and a new
13/// version is the result.
14///
15/// Implementations of the trait can error. In the case of recursive implementations,
16/// other implementations may catch and recover from the error, or propagate it.
17///
18/// Context: Feature JSON is used to configure a application feature.
19/// If a value is needed, the application provides a default.
20/// A rollout changes this default.
21pub trait Defaults {
22 fn defaults(&self, fallback: &Self) -> Result<Self>
23 where
24 Self: Sized;
25}
26
27impl<T: Defaults + Clone> Defaults for Option<T> {
28 fn defaults(&self, fallback: &Self) -> Result<Self> {
29 Ok(match (self, fallback) {
30 (Some(a), Some(b)) => Some(a.defaults(b)?),
31 (Some(_), None) => self.clone(),
32 _ => fallback.clone(),
33 })
34 }
35}
36
37use serde_json::{Map, Value};
38/// We implement https://datatracker.ietf.org/doc/html/rfc7396
39/// such that self is patching the fallback.
40/// The result is the patched object.
41///
42/// * If a self value is null, we take that to equivalent to a delete.
43/// * If both self and fallback are objects, we recursively patch.
44/// * If it exists in either in self or clone, then it is included
45/// * in the result.
46/// * If it exists in both, then we take the self version.
47impl Defaults for Value {
48 fn defaults(&self, fallback: &Self) -> Result<Self> {
49 Ok(match (self, fallback) {
50 (Value::Object(a), Value::Object(b)) => Value::Object(a.defaults(b)?),
51 (Value::Null, _) => fallback.to_owned(),
52 _ => self.to_owned(),
53 })
54 }
55}
56
57impl Defaults for Map<String, Value> {
58 fn defaults(&self, fallback: &Self) -> Result<Self> {
59 let mut map = self.clone();
60 for (k, fb) in fallback {
61 match map.get(k) {
62 Some(existing) if existing.is_null() => {
63 map.remove(k);
64 }
65 Some(existing) => {
66 // JSON merging should't error, so there'll be
67 // nothing to propagate.
68 map[k] = existing.defaults(fb)?;
69 }
70 _ => {
71 map.insert(k.clone(), fb.clone());
72 }
73 };
74 }
75 Ok(map)
76 }
77}
78
79use std::collections::HashMap;
80/// Merge the two `HashMap`s, with self acting as the dominant
81/// of the two.
82///
83/// Nimbus' use case is to be merging data coming from the outside,
84/// we should not allow a bad merge to bring the whole system down.
85///
86/// Where values merging fails, we go with the newest version.
87impl<T: Defaults + Clone> Defaults for HashMap<String, T> {
88 fn defaults(&self, fallback: &Self) -> Result<Self> {
89 let mut map = self.clone();
90 for (k, fb) in fallback {
91 match map.get(k) {
92 Some(existing) => {
93 // if we merged with fb without errors,
94 if let Ok(v) = existing.defaults(fb) {
95 map.insert(k.clone(), v);
96 } // otherwise use the self value, without merging.
97 }
98 _ => {
99 map.insert(k.clone(), fb.clone());
100 }
101 }
102 }
103 Ok(map)
104 }
105}