nimbus_fml/defaults/
merger.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 http://mozilla.org/MPL/2.0/. */
4
5use std::collections::{BTreeMap, HashMap};
6
7use serde_json::{json, Value};
8
9use crate::{
10    error::{FMLError, Result},
11    frontend::DefaultBlock,
12    intermediate_representation::{FeatureDef, ObjectDef, PropDef, TypeRef},
13};
14
15pub struct DefaultsMerger<'object> {
16    objects: &'object BTreeMap<String, ObjectDef>,
17
18    supported_channels: Vec<String>,
19    channel: Option<String>,
20}
21
22impl<'object> DefaultsMerger<'object> {
23    pub fn new(
24        objects: &'object BTreeMap<String, ObjectDef>,
25        supported_channels: Vec<String>,
26        channel: Option<String>,
27    ) -> Self {
28        Self {
29            objects,
30            supported_channels,
31            channel,
32        }
33    }
34
35    #[cfg(test)]
36    pub fn new_with_channel(
37        objects: &'object BTreeMap<String, ObjectDef>,
38        supported_channels: Vec<String>,
39        channel: String,
40    ) -> Self {
41        Self::new(objects, supported_channels, Some(channel.to_string()))
42    }
43
44    fn collect_feature_defaults(&self, feature: &FeatureDef) -> serde_json::Value {
45        self.collect_props_defaults(&feature.props)
46    }
47
48    fn collect_object_defaults(&self, name: &str) -> serde_json::Value {
49        let obj = self
50            .objects
51            .get(name)
52            .unwrap_or_else(|| panic!("Object named {} is not defined", name));
53
54        self.collect_props_defaults(&obj.props)
55    }
56
57    fn collect_props_defaults(&self, props: &Vec<PropDef>) -> Value {
58        let mut res = serde_json::value::Map::new();
59        for p in props {
60            res.insert(p.name(), self.collect_prop_defaults(&p.typ, &p.default));
61        }
62        serde_json::Value::Object(res)
63    }
64
65    fn collect_prop_defaults(&self, typ: &TypeRef, v: &serde_json::Value) -> serde_json::Value {
66        match typ {
67            TypeRef::Object(name) => merge_two_defaults(&self.collect_object_defaults(name), v),
68            TypeRef::EnumMap(_, v_type) => self.collect_map_defaults(v_type, v),
69            TypeRef::StringMap(v_type) => self.collect_map_defaults(v_type, v),
70            _ => v.clone(),
71        }
72    }
73
74    fn collect_map_defaults(&self, v_type: &TypeRef, obj: &serde_json::Value) -> serde_json::Value {
75        let map = obj
76            .as_object()
77            .unwrap_or_else(|| panic!("Expected a JSON object as a default"));
78        let mut res = serde_json::value::Map::new();
79        for (k, v) in map {
80            let collected = self.collect_prop_defaults(v_type, v);
81            res.insert(k.clone(), collected);
82        }
83        serde_json::Value::Object(res)
84    }
85
86    /// Transforms a feature definition with unmerged defaults into a feature
87    /// definition with its defaults merged.
88    ///
89    /// # How the algorithm works:
90    /// There are two types of defaults:
91    /// 1. Field level defaults
92    /// 1. Feature level defaults, that are listed by channel
93    ///
94    /// The algorithm gathers the field level defaults first, they are the base
95    /// defaults. Then, it gathers the feature level defaults and merges them by
96    /// calling [`collect_channel_defaults`]. Finally, it overwrites any common
97    /// defaults between the merged feature level defaults and the field level defaults
98    ///
99    /// # Example:
100    /// Assume we have the following feature manifest
101    /// ```yaml
102    ///  variables:
103    ///   positive:
104    ///   description: This is a positive button
105    ///   type: Button
106    ///   default:
107    ///     {
108    ///       "label": "Ok then",
109    ///       "color": "blue"
110    ///     }
111    ///  default:
112    ///      - channel: release
113    ///      value: {
114    ///        "positive": {
115    ///          "color": "green"
116    ///        }
117    ///      }
118    ///      - value: {
119    ///      "positive": {
120    ///        "alt-text": "Go Ahead!"
121    ///      }
122    /// }
123    /// ```
124    ///
125    /// The result of the algorithm would be a default that looks like:
126    /// ```yaml
127    /// variables:
128    ///     positive:
129    ///     default:
130    ///     {
131    ///         "label": "Ok then",
132    ///         "color": "green",
133    ///         "alt-text": "Go Ahead!"
134    ///     }
135    ///
136    /// ```
137    ///
138    /// - The `label` comes from the original field level default
139    /// - The `color` comes from the `release` channel feature level default
140    /// - The `alt-text` comes from the feature level default with no channel (that applies to all channels)
141    ///
142    /// # Arguments
143    /// - `feature_def`: a [`FeatureDef`] representing the feature definition to transform
144    /// - `channel`: a [`Option<&String>`] representing the channel to merge back into the field variables.
145    ///   If the `channel` is `None` we default to using the `release` channel.
146    /// - `supported_channels`: a [`&[String]`] representing the channels that are supported by the manifest
147    ///
148    /// # Returns
149    /// Returns a transformed [`FeatureDef`] with its defaults merged
150    pub fn merge_feature_defaults(
151        &self,
152        feature_def: &mut FeatureDef,
153        defaults: &Option<Vec<DefaultBlock>>,
154    ) -> Result<(), FMLError> {
155        let variable_defaults = self.collect_feature_defaults(feature_def);
156        let defaults_to_merge = self.channel_specific_defaults(defaults)?;
157        let merged = merge_two_defaults(&variable_defaults, &defaults_to_merge);
158
159        self.overwrite_defaults(feature_def, &merged);
160        Ok(())
161    }
162
163    /// Mutates a FeatureDef by changing the defaults to the `merged` value.
164    ///
165    /// This does not do any _merging_ of defaults with the passed value:
166    /// you can get the merged value from `merge_feature_config`.
167    ///
168    /// It also does not attempt to validate the keys against the property:
169    /// this is done in the `get_errors` of `DefaultsValidator`, more specifically, the
170    /// `validate_props_types` method.
171    ///
172    /// The `merged` value is expected to be a JSON object.
173    pub(crate) fn overwrite_defaults(&self, feature_def: &mut FeatureDef, merged: &Value) {
174        let map = merged.as_object().expect("`merged` value not a map");
175
176        for p in &mut feature_def.props {
177            if let Some(v) = map.get(&p.name) {
178                p.default = v.clone();
179            }
180        }
181    }
182
183    fn channel_specific_defaults(&self, defaults: &Option<Vec<DefaultBlock>>) -> Result<Value> {
184        let supported_channels = self.supported_channels.as_slice();
185        let channel = &self.channel;
186        if let Some(channel) = channel {
187            if !supported_channels.iter().any(|c| c == channel) {
188                return Err(FMLError::InvalidChannelError(
189                    channel.into(),
190                    supported_channels.into(),
191                ));
192            }
193        }
194        let empty_object = json!({});
195        if let Some(defaults) = defaults {
196            // No channel is represented by an unlikely string.
197            let no_channel = "NO CHANNEL SPECIFIED".to_string();
198            let merged_defaults =
199                collect_channel_defaults(defaults, supported_channels, &no_channel)?;
200            let channel = self.channel.as_ref().unwrap_or(&no_channel);
201            let merged = merged_defaults[channel].clone();
202            Ok(merged)
203        } else {
204            Ok(empty_object)
205        }
206    }
207
208    /// A convenience method to get the defaults from the feature, and merger it
209    /// with the passed value.
210    pub(crate) fn merge_feature_config(&self, feature_def: &FeatureDef, value: &Value) -> Value {
211        let defaults = self.collect_feature_defaults(feature_def);
212        merge_two_defaults(&defaults, value)
213    }
214}
215
216/// Merges two [`serde_json::Value`]s into one
217///
218/// # Arguments:
219/// - `old_default`: a reference to a [`serde_json::Value`], that represents the old default
220/// - `new_default`: a reference to a [`serde_json::Value`], that represents the new default, this takes
221///   precedence over the `old_default` if they have conflicting fields
222///
223/// # Returns
224/// A merged [`serde_json::Value`] that contains all fields from `old_default` and `new_default`, merging
225/// where there is a conflict. If the `old_default` and `new_default` are not both objects, this function
226/// returns the `new_default`
227fn merge_two_defaults(
228    old_default: &serde_json::Value,
229    new_default: &serde_json::Value,
230) -> serde_json::Value {
231    use serde_json::Value::Object;
232    match (old_default.clone(), new_default.clone()) {
233        (Object(old), Object(new)) => {
234            let mut merged = serde_json::Map::new();
235            for (key, val) in old {
236                merged.insert(key, val);
237            }
238            for (key, val) in new {
239                if let Some(old_val) = merged.get(&key).cloned() {
240                    merged.insert(key, merge_two_defaults(&old_val, &val));
241                } else {
242                    merged.insert(key, val);
243                }
244            }
245            Object(merged)
246        }
247        (_, new) => new,
248    }
249}
250
251/// Collects the channel defaults of the feature manifest
252/// and merges them by channel
253///
254/// **NOTE**: defaults with no channel apply to **all** channels
255///
256/// # Arguments
257/// - `defaults`: a [`serde_json::Value`] representing the array of defaults
258///
259/// # Returns
260/// Returns a [`std::collections::HashMap<String, serde_json::Value>`] representing
261/// the merged defaults. The key is the name of the channel and the value is the
262/// merged json.
263///
264/// # Errors
265/// Will return errors in the following cases (not exhaustive):
266/// - The `defaults` argument is not an array
267/// - There is a `channel` in the `defaults` argument that doesn't
268///   exist in the `channels` argument
269fn collect_channel_defaults(
270    defaults: &[DefaultBlock],
271    channels: &[String],
272    no_channel: &str,
273) -> Result<HashMap<String, serde_json::Value>> {
274    // We initialize the map to have an entry for every valid channel
275    let mut channel_map = channels
276        .iter()
277        .map(|channel_name| (channel_name.clone(), json!({})))
278        .collect::<HashMap<_, _>>();
279    channel_map.insert(no_channel.to_string(), json!({}));
280    for default in defaults {
281        if let Some(channels_for_default) = &default.merge_channels() {
282            for channel in channels_for_default {
283                if let Some(old_default) = channel_map.get(channel).cloned() {
284                    if default.targeting.is_none() {
285                        // TODO: we currently ignore any defaults with targeting involved
286                        let merged = merge_two_defaults(&old_default, &default.value);
287                        channel_map.insert(channel.clone(), merged);
288                    }
289                } else {
290                    return Err(FMLError::InvalidChannelError(
291                        channel.into(),
292                        channels.into(),
293                    ));
294                }
295            }
296        // This is a default with no channel, so it applies to all channels
297        } else {
298            channel_map = channel_map
299                .into_iter()
300                .map(|(channel, old_default)| {
301                    (channel, merge_two_defaults(&old_default, &default.value))
302                })
303                .collect();
304        }
305    }
306    Ok(channel_map)
307}
308
309#[cfg(test)]
310mod unit_tests {
311    use crate::intermediate_representation::PropDef;
312
313    use super::*;
314    use serde_json::json;
315
316    #[test]
317    fn test_merge_two_defaults_both_objects_no_intersection() -> Result<()> {
318        let old_default = json!({
319            "button-color": "blue",
320            "dialog_option": "greetings",
321            "is_enabled": false,
322            "num_items": 5
323        });
324        let new_default = json!({
325            "new_homepage": true,
326            "item_order": ["first", "second", "third"],
327        });
328        let merged = merge_two_defaults(&old_default, &new_default);
329        assert_eq!(
330            json!({
331                "button-color": "blue",
332                "dialog_option": "greetings",
333                "is_enabled": false,
334                "num_items": 5,
335                "new_homepage": true,
336                "item_order": ["first", "second", "third"],
337            }),
338            merged
339        );
340        Ok(())
341    }
342
343    #[test]
344    fn test_merge_two_defaults_intersecting_different_types() -> Result<()> {
345        // if there is an intersection, but they are different types, we just take the new one
346        let old_default = json!({
347            "button-color": "blue",
348            "dialog_option": "greetings",
349            "is_enabled": {
350                "value": false
351            },
352            "num_items": 5
353        });
354        let new_default = json!({
355            "new_homepage": true,
356            "is_enabled": true,
357            "item_order": ["first", "second", "third"],
358        });
359        let merged = merge_two_defaults(&old_default, &new_default);
360        assert_eq!(
361            json!({
362                "button-color": "blue",
363                "dialog_option": "greetings",
364                "is_enabled": true,
365                "num_items": 5,
366                "new_homepage": true,
367                "item_order": ["first", "second", "third"],
368            }),
369            merged
370        );
371        Ok(())
372    }
373
374    #[test]
375    fn test_merge_two_defaults_non_map_intersection() -> Result<()> {
376        // if they intersect on both key and type, but the type intersected is not an object, we just take the new one
377        let old_default = json!({
378            "button-color": "blue",
379            "dialog_option": "greetings",
380            "is_enabled": false,
381            "num_items": 5
382        });
383        let new_default = json!({
384            "button-color": "green",
385            "new_homepage": true,
386            "is_enabled": true,
387            "num_items": 10,
388            "item_order": ["first", "second", "third"],
389        });
390        let merged = merge_two_defaults(&old_default, &new_default);
391        assert_eq!(
392            json!({
393                "button-color": "green",
394                "dialog_option": "greetings",
395                "is_enabled": true,
396                "num_items": 10,
397                "new_homepage": true,
398                "item_order": ["first", "second", "third"],
399            }),
400            merged
401        );
402        Ok(())
403    }
404
405    #[test]
406    fn test_merge_two_defaults_map_intersection_recursive_merge() -> Result<()> {
407        // if they intersect on both key and type, but the type intersected is not an object, we just take the new one
408        let old_default = json!({
409            "button-color": "blue",
410            "dialog_item": {
411                "title": "hello",
412                "message": "bobo",
413                "priority": 10,
414            },
415            "is_enabled": false,
416            "num_items": 5
417        });
418        let new_default = json!({
419            "button-color": "green",
420            "new_homepage": true,
421            "is_enabled": true,
422            "dialog_item": {
423                "message": "fofo",
424                "priority": 11,
425                "subtitle": "hey there"
426            },
427            "num_items": 10,
428            "item_order": ["first", "second", "third"],
429        });
430        let merged = merge_two_defaults(&old_default, &new_default);
431        assert_eq!(
432            json!({
433                "button-color": "green",
434                "dialog_item": {
435                    "title": "hello",
436                    "message": "fofo",
437                    "priority": 11,
438                    "subtitle": "hey there"
439                },
440                "is_enabled": true,
441                "num_items": 10,
442                "new_homepage": true,
443                "item_order": ["first", "second", "third"],
444            }),
445            merged
446        );
447        Ok(())
448    }
449
450    #[test]
451    fn test_merge_two_defaults_highlevel_non_maps() -> Result<()> {
452        let old_default = json!(["array", "json"]);
453        let new_default = json!(["another", "array"]);
454        let merged = merge_two_defaults(&old_default, &new_default);
455        assert_eq!(json!(["another", "array"]), merged);
456        Ok(())
457    }
458
459    #[test]
460    fn test_channel_defaults_channels_no_merging() -> Result<()> {
461        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
462            {
463                "channel": "release",
464                "value": {
465                    "button-color": "green"
466                }
467            },
468            {
469                "channel": "nightly",
470                "value": {
471                    "button-color": "dark-green"
472                }
473            },
474            {
475                "channel": "beta",
476                "value": {
477                    "button-color": "light-green"
478                }
479            }
480        ]))?;
481        let res = collect_channel_defaults(
482            &input,
483            &[
484                "release".to_string(),
485                "nightly".to_string(),
486                "beta".to_string(),
487            ],
488            "",
489        )?;
490        assert_eq!(
491            vec![
492                (
493                    "release".to_string(),
494                    json!({
495                        "button-color": "green"
496                    })
497                ),
498                (
499                    "nightly".to_string(),
500                    json!({
501                        "button-color": "dark-green"
502                    })
503                ),
504                (
505                    "beta".to_string(),
506                    json!({
507                        "button-color": "light-green"
508                    })
509                ),
510                ("".to_string(), json!({}),),
511            ]
512            .into_iter()
513            .collect::<HashMap<_, _>>(),
514            res
515        );
516        Ok(())
517    }
518
519    #[test]
520    fn test_channel_defaults_channels_merging_same_channel() -> Result<()> {
521        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
522            {
523                "channel": "release",
524                "value": {
525                    "button-color": "green"
526                }
527            },
528            {
529                "channel": "nightly",
530                "value": {
531                    "button-color": "dark-green",
532                    "title": "heya"
533                }
534            },
535            {
536                "channel": "beta",
537                "value": {
538                    "button-color": "light-green"
539                }
540            },
541            {
542                "channel": "nightly",
543                "value": {
544                    "button-color": "dark-red",
545                    "subtitle": "hello",
546                }
547            },
548            {
549                "channel": "beta",
550                "value": {
551                    "title": "hello there"
552                }
553            }
554        ]))?;
555        let res = collect_channel_defaults(
556            &input,
557            &[
558                "release".to_string(),
559                "nightly".to_string(),
560                "beta".to_string(),
561            ],
562            "",
563        )?;
564        assert_eq!(
565            vec![
566                (
567                    "release".to_string(),
568                    json!({
569                        "button-color": "green"
570                    })
571                ),
572                (
573                    "nightly".to_string(),
574                    json!({
575                        "button-color": "dark-red",
576                        "title": "heya",
577                        "subtitle": "hello"
578                    })
579                ),
580                (
581                    "beta".to_string(),
582                    json!({
583                        "button-color": "light-green",
584                        "title": "hello there"
585                    })
586                ),
587                ("".to_string(), json!({}),),
588            ]
589            .into_iter()
590            .collect::<HashMap<_, _>>(),
591            res
592        );
593        Ok(())
594    }
595
596    #[test]
597    fn test_channel_defaults_no_channel_applies_to_all() -> Result<()> {
598        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
599            {
600                "channel": "release",
601                "value": {
602                    "button-color": "green"
603                }
604            },
605            {
606                "channel": "nightly",
607                "value": {
608                    "button-color": "dark-green"
609                }
610            },
611            {
612                "channel": "beta",
613                "value": {
614                    "button-color": "light-green"
615                }
616            },
617            {
618                "value": {
619                    "title": "heya"
620                }
621            }
622        ]))?;
623        let res = collect_channel_defaults(
624            &input,
625            &[
626                "release".to_string(),
627                "nightly".to_string(),
628                "beta".to_string(),
629            ],
630            "",
631        )?;
632        assert_eq!(
633            vec![
634                (
635                    "release".to_string(),
636                    json!({
637                        "button-color": "green",
638                        "title": "heya"
639                    })
640                ),
641                (
642                    "nightly".to_string(),
643                    json!({
644                        "button-color": "dark-green",
645                        "title": "heya"
646                    })
647                ),
648                (
649                    "beta".to_string(),
650                    json!({
651                        "button-color": "light-green",
652                        "title": "heya"
653                    })
654                ),
655                (
656                    "".to_string(),
657                    json!({
658                        "title": "heya",
659                    }),
660                )
661            ]
662            .into_iter()
663            .collect::<HashMap<_, _>>(),
664            res
665        );
666        Ok(())
667    }
668
669    #[test]
670    fn test_channel_defaults_no_channel_overwrites_all() -> Result<()> {
671        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
672            {
673                "channel": "release",
674                "value": {
675                    "button-color": "green"
676                }
677            },
678            {
679                "channel": "nightly",
680                "value": {
681                    "button-color": "dark-green"
682                }
683            },
684            {
685                "channel": "beta",
686                "value": {
687                    "button-color": "light-green"
688                }
689            },
690            {
691                "value": {
692                    "button-color": "red"
693                }
694            }
695        ]))?;
696        let res = collect_channel_defaults(
697            &input,
698            &[
699                "release".to_string(),
700                "nightly".to_string(),
701                "beta".to_string(),
702            ],
703            "",
704        )?;
705        assert_eq!(
706            vec![
707                (
708                    "release".to_string(),
709                    json!({
710                        "button-color": "red"
711                    })
712                ),
713                (
714                    "nightly".to_string(),
715                    json!({
716                        "button-color": "red"
717                    })
718                ),
719                (
720                    "beta".to_string(),
721                    json!({
722                        "button-color": "red"
723                    })
724                ),
725                (
726                    "".to_string(),
727                    json!({
728                        "button-color": "red",
729                    }),
730                )
731            ]
732            .into_iter()
733            .collect::<HashMap<_, _>>(),
734            res
735        );
736        Ok(())
737    }
738
739    #[test]
740    fn test_channel_defaults_no_channel_gets_overwritten_if_followed_by_channel() -> Result<()> {
741        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
742            {
743                "channel": "release",
744                "value": {
745                    "button-color": "green"
746                }
747            },
748            {
749                "channel": "nightly",
750                "value": {
751                    "button-color": "dark-green"
752                }
753            },
754            {
755                "channel": "beta",
756                "value": {
757                    "button-color": "light-green"
758                }
759            },
760            {
761                "value": {
762                    "button-color": "red"
763                }
764            },
765            {
766                "channel": "nightly",
767                "value": {
768                    "button-color": "dark-red"
769                }
770            }
771        ]))?;
772        let res = collect_channel_defaults(
773            &input,
774            &[
775                "release".to_string(),
776                "nightly".to_string(),
777                "beta".to_string(),
778            ],
779            "",
780        )?;
781        assert_eq!(
782            vec![
783                (
784                    "release".to_string(),
785                    json!({
786                        "button-color": "red"
787                    })
788                ),
789                (
790                    "nightly".to_string(),
791                    json!({
792                        "button-color": "dark-red"
793                    })
794                ),
795                (
796                    "beta".to_string(),
797                    json!({
798                        "button-color": "red"
799                    })
800                ),
801                (
802                    "".to_string(),
803                    json!({
804                        "button-color": "red",
805                    }),
806                )
807            ]
808            .into_iter()
809            .collect::<HashMap<_, _>>(),
810            res
811        );
812        Ok(())
813    }
814
815    #[test]
816    fn test_channel_defaults_channels_multiple() -> Result<()> {
817        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
818            {
819                "channels": ["release", "beta"],
820                "value": {
821                    "button-color": "green"
822                }
823            },
824        ]))?;
825        let res =
826            collect_channel_defaults(&input, &["release".to_string(), "beta".to_string()], "")?;
827        assert_eq!(
828            vec![
829                (
830                    "release".to_string(),
831                    json!({
832                        "button-color": "green"
833                    })
834                ),
835                (
836                    "beta".to_string(),
837                    json!({
838                        "button-color": "green"
839                    })
840                ),
841                ("".to_string(), json!({}),)
842            ]
843            .into_iter()
844            .collect::<HashMap<_, _>>(),
845            res
846        );
847        Ok(())
848    }
849
850    #[test]
851    fn test_channel_defaults_channel_multiple_merge_channels_multiple() -> Result<()> {
852        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
853            {
854                "channel": "nightly, debug",
855                "channels": ["release", "beta"],
856                "value": {
857                    "button-color": "green"
858                }
859            },
860        ]))?;
861        let res = collect_channel_defaults(
862            &input,
863            &[
864                "release".to_string(),
865                "beta".to_string(),
866                "nightly".to_string(),
867                "debug".to_string(),
868            ],
869            "",
870        )?;
871        assert_eq!(
872            vec![
873                (
874                    "release".to_string(),
875                    json!({
876                        "button-color": "green"
877                    })
878                ),
879                (
880                    "beta".to_string(),
881                    json!({
882                        "button-color": "green"
883                    })
884                ),
885                (
886                    "nightly".to_string(),
887                    json!({
888                        "button-color": "green"
889                    })
890                ),
891                (
892                    "debug".to_string(),
893                    json!({
894                        "button-color": "green"
895                    })
896                ),
897                ("".to_string(), json!({}),)
898            ]
899            .into_iter()
900            .collect::<HashMap<_, _>>(),
901            res
902        );
903        Ok(())
904    }
905
906    #[test]
907    fn test_channel_defaults_fail_if_invalid_channel_supplied() -> Result<()> {
908        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
909            {
910                "channel": "release",
911                "value": {
912                    "button-color": "green"
913                }
914            },
915            {
916                "channel": "nightly",
917                "value": {
918                    "button-color": "dark-green"
919                }
920            },
921            {
922                "channel": "beta",
923                "value": {
924                    "button-color": "light-green"
925                }
926            },
927            {
928                "channel": "bobo",
929                "value": {
930                    "button-color": "no color"
931                }
932            }
933        ]))?;
934        let res = collect_channel_defaults(
935            &input,
936            &[
937                "release".to_string(),
938                "nightly".to_string(),
939                "beta".to_string(),
940            ],
941            "",
942        )
943        .expect_err("Should return error");
944        if let FMLError::InvalidChannelError(channel, _supported) = res {
945            assert!(channel.contains("bobo"));
946        } else {
947            panic!(
948                "Should have returned a InvalidChannelError, returned {:?}",
949                res
950            )
951        }
952        Ok(())
953    }
954
955    #[test]
956    fn test_channel_defaults_empty_default_created_if_none_supplied_in_feature() -> Result<()> {
957        let input: Vec<DefaultBlock> = serde_json::from_value(json!([
958            {
959                "channel": "release",
960                "value": {
961                    "button-color": "green"
962                }
963            },
964            {
965                "channel": "nightly",
966                "value": {
967                    "button-color": "dark-green"
968                }
969            },
970            // No entry fo beta supplied, we will still get an entry in the result
971            // but it will be empty
972        ]))?;
973        let res = collect_channel_defaults(
974            &input,
975            &[
976                "release".to_string(),
977                "nightly".to_string(),
978                "beta".to_string(),
979            ],
980            "",
981        )?;
982        assert_eq!(
983            vec![
984                (
985                    "release".to_string(),
986                    json!({
987                        "button-color": "green"
988                    })
989                ),
990                (
991                    "nightly".to_string(),
992                    json!({
993                        "button-color": "dark-green"
994                    })
995                ),
996                ("beta".to_string(), json!({})),
997                ("".to_string(), json!({}),)
998            ]
999            .into_iter()
1000            .collect::<HashMap<_, _>>(),
1001            res
1002        );
1003        Ok(())
1004    }
1005
1006    #[test]
1007    fn test_merge_feature_default_unsupported_channel() -> Result<()> {
1008        let mut feature_def: FeatureDef = Default::default();
1009        let objects = Default::default();
1010        let merger = DefaultsMerger::new_with_channel(
1011            &objects,
1012            vec!["release".into(), "beta".into()],
1013            "nightly".into(),
1014        );
1015        let err = merger
1016            .merge_feature_defaults(&mut feature_def, &None)
1017            .expect_err("Should return an error");
1018        if let FMLError::InvalidChannelError(channel, _supported) = err {
1019            assert!(channel.contains("nightly"));
1020        } else {
1021            panic!(
1022                "Should have returned an InvalidChannelError, returned: {:?}",
1023                err
1024            );
1025        }
1026        Ok(())
1027    }
1028
1029    #[test]
1030    fn test_merge_feature_default_overwrite_field_default_based_on_channel() -> Result<()> {
1031        let mut feature_def = FeatureDef {
1032            props: vec![PropDef::new(
1033                "button-color",
1034                &TypeRef::String,
1035                &json!("blue"),
1036            )],
1037            ..Default::default()
1038        };
1039        let default_blocks = serde_json::from_value(json!([
1040            {
1041                "channel": "nightly",
1042                "value": {
1043                    "button-color": "dark-green"
1044                }
1045            },
1046            {
1047                "channel": "release",
1048                "value": {
1049                    "button-color": "green"
1050                }
1051            },
1052            {
1053                "channel": "beta",
1054                "value": {
1055                    "button-color": "light-green"
1056                }
1057            },
1058        ]))?;
1059        let objects = Default::default();
1060        let merger = DefaultsMerger::new_with_channel(
1061            &objects,
1062            vec!["release".into(), "beta".into(), "nightly".into()],
1063            "nightly".into(),
1064        );
1065        merger.merge_feature_defaults(&mut feature_def, &default_blocks)?;
1066        assert_eq!(
1067            feature_def.props,
1068            vec![PropDef::new(
1069                "button-color",
1070                &TypeRef::String,
1071                &json!("dark-green"),
1072            )]
1073        );
1074        Ok(())
1075    }
1076
1077    #[test]
1078    fn test_merge_feature_default_field_default_not_overwritten_if_no_feature_default_for_channel(
1079    ) -> Result<()> {
1080        let mut feature_def = FeatureDef {
1081            props: vec![PropDef::new(
1082                "button-color",
1083                &TypeRef::String,
1084                &json!("blue"),
1085            )],
1086            ..Default::default()
1087        };
1088        let default_blocks = serde_json::from_value(json!([{
1089            "channel": "release",
1090            "value": {
1091                "button-color": "green"
1092            }
1093        },
1094        {
1095            "channel": "beta",
1096            "value": {
1097                "button-color": "light-green"
1098            }
1099        }]))?;
1100        let objects = Default::default();
1101        let merger = DefaultsMerger::new_with_channel(
1102            &objects,
1103            vec!["release".into(), "beta".into(), "nightly".into()],
1104            "nightly".into(),
1105        );
1106        merger.merge_feature_defaults(&mut feature_def, &default_blocks)?;
1107        assert_eq!(
1108            feature_def.props,
1109            vec![PropDef::new(
1110                "button-color",
1111                &TypeRef::String,
1112                &json!("blue"),
1113            )]
1114        );
1115        Ok(())
1116    }
1117
1118    #[test]
1119    fn test_merge_feature_default_overwrite_nested_field_default() -> Result<()> {
1120        let mut feature_def = FeatureDef {
1121            props: vec![PropDef::new(
1122                "Dialog",
1123                &TypeRef::String,
1124                &json!({
1125                    "button-color": "blue",
1126                    "title": "hello",
1127                    "inner": {
1128                        "bobo": "fofo",
1129                        "other-field": "other-value"
1130                    }
1131                }),
1132            )],
1133
1134            ..Default::default()
1135        };
1136        let default_blocks = serde_json::from_value(json!([
1137            {
1138                "channel": "nightly",
1139                "value": {
1140                    "Dialog": {
1141                        "button-color": "dark-green",
1142                        "inner": {
1143                            "bobo": "nightly"
1144                        }
1145                    }
1146                }
1147            },
1148            {
1149                "channel": "release",
1150                "value": {
1151                    "Dialog": {
1152                        "button-color": "green",
1153                        "inner": {
1154                            "bobo": "release",
1155                            "new-field": "new-value"
1156                        }
1157                    }
1158                }
1159            },
1160            {
1161                "channel": "beta",
1162                "value": {
1163                    "Dialog": {
1164                        "button-color": "light-green",
1165                        "inner": {
1166                            "bobo": "beta"
1167                        }
1168                    }
1169                }
1170            },
1171        ]))?;
1172        let objects = Default::default();
1173        let merger = DefaultsMerger::new_with_channel(
1174            &objects,
1175            vec!["release".into(), "beta".into(), "nightly".into()],
1176            "release".into(),
1177        );
1178        merger.merge_feature_defaults(&mut feature_def, &default_blocks)?;
1179        assert_eq!(
1180            feature_def.props,
1181            vec![PropDef::new(
1182                "Dialog",
1183                &TypeRef::String,
1184                &json!({
1185                        "button-color": "green",
1186                        "title": "hello",
1187                        "inner": {
1188                            "bobo": "release",
1189                            "other-field": "other-value",
1190                            "new-field": "new-value"
1191                        }
1192                })
1193            )]
1194        );
1195        Ok(())
1196    }
1197
1198    #[test]
1199    fn test_merge_feature_default_overwrite_field_default_based_on_channel_using_only_no_channel_default(
1200    ) -> Result<()> {
1201        let mut feature_def = FeatureDef {
1202            props: vec![PropDef::new(
1203                "button-color",
1204                &TypeRef::String,
1205                &json!("blue"),
1206            )],
1207            ..Default::default()
1208        };
1209        let default_blocks = serde_json::from_value(json!([
1210            // No channel applies to all channel
1211            // so the nightly channel will get this
1212            {
1213                "value": {
1214                    "button-color": "dark-green"
1215                }
1216            },
1217            {
1218                "channel": "release",
1219                "value": {
1220                    "button-color": "green"
1221                }
1222            },
1223            {
1224                "channel": "beta",
1225                "value": {
1226                    "button-color": "light-green"
1227                }
1228            },
1229        ]))?;
1230        let objects = Default::default();
1231        let merger = DefaultsMerger::new_with_channel(
1232            &objects,
1233            vec!["release".into(), "beta".into(), "nightly".into()],
1234            "nightly".into(),
1235        );
1236        merger.merge_feature_defaults(&mut feature_def, &default_blocks)?;
1237        assert_eq!(
1238            feature_def.props,
1239            vec![PropDef::new(
1240                "button-color",
1241                &TypeRef::String,
1242                &json!("dark-green"),
1243            )]
1244        );
1245        Ok(())
1246    }
1247}