nimbus_fml/
frontend.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, HashSet};
6
7use email_address::EmailAddress;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use url::Url;
11
12use crate::{
13    defaults::DefaultsMerger,
14    error::Result,
15    intermediate_representation::{
16        EnumDef, FeatureDef, FeatureManifest, GeckoPrefDef, ModuleId, ObjectDef, PropDef,
17        TargetLanguage, TypeRef, VariantDef,
18    },
19    parser::get_typeref_from_string,
20};
21
22#[derive(Debug, Deserialize, Serialize, Clone)]
23#[serde(deny_unknown_fields)]
24pub(crate) struct EnumVariantBody {
25    pub(crate) description: String,
26}
27
28#[derive(Debug, Deserialize, Serialize, Clone)]
29#[serde(deny_unknown_fields)]
30pub(crate) struct EnumBody {
31    pub(crate) description: String,
32    pub(crate) variants: BTreeMap<String, EnumVariantBody>,
33}
34
35#[derive(Debug, Deserialize, Serialize, Clone)]
36#[serde(deny_unknown_fields)]
37#[serde(rename_all = "kebab-case")]
38pub(crate) struct FeatureFieldBody {
39    #[serde(flatten)]
40    pub(crate) field: FieldBody,
41
42    #[serde(default)]
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub(crate) pref_key: Option<String>,
45
46    #[serde(default)]
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub(crate) gecko_pref: Option<GeckoPrefDef>,
49
50    #[serde(default)]
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub(crate) string_alias: Option<String>,
53}
54
55#[derive(Debug, Deserialize, Serialize, Clone)]
56#[serde(deny_unknown_fields)]
57pub(crate) struct FieldBody {
58    pub(crate) description: String,
59    #[serde(rename = "type")]
60    pub(crate) variable_type: String,
61    pub(crate) default: Option<serde_json::Value>,
62}
63
64#[derive(Debug, Deserialize, Serialize, Clone)]
65#[serde(deny_unknown_fields)]
66pub(crate) struct ObjectBody {
67    pub(crate) description: String,
68    // We need these in a deterministic order, so they are stable across multiple
69    // runs of the same manifests.
70    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
71    pub(crate) fields: BTreeMap<String, FieldBody>,
72}
73
74#[derive(Debug, Deserialize, Serialize, Clone, Default)]
75#[serde(deny_unknown_fields)]
76pub(crate) struct Types {
77    #[serde(default)]
78    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
79    pub(crate) enums: BTreeMap<String, EnumBody>,
80    #[serde(default)]
81    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
82    pub(crate) objects: BTreeMap<String, ObjectBody>,
83}
84
85#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
86#[serde(deny_unknown_fields)]
87pub(crate) struct AboutBlock {
88    pub(crate) description: String,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    #[serde(alias = "kotlin", alias = "android")]
91    pub(crate) kotlin_about: Option<KotlinAboutBlock>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    #[serde(alias = "swift", alias = "ios")]
94    pub(crate) swift_about: Option<SwiftAboutBlock>,
95}
96
97impl AboutBlock {
98    pub(crate) fn is_includable(&self) -> bool {
99        self.kotlin_about.is_none() && self.swift_about.is_none()
100    }
101
102    #[allow(unused)]
103    pub(crate) fn supports(&self, lang: &TargetLanguage) -> bool {
104        match lang {
105            TargetLanguage::Kotlin => self.kotlin_about.is_some(),
106            TargetLanguage::Swift => self.swift_about.is_some(),
107            TargetLanguage::IR => true,
108            TargetLanguage::ExperimenterYAML => true,
109            TargetLanguage::ExperimenterJSON => true,
110        }
111    }
112
113    #[allow(unused)]
114    pub fn description_only(&self) -> Self {
115        AboutBlock {
116            description: self.description.clone(),
117            kotlin_about: None,
118            swift_about: None,
119        }
120    }
121}
122
123#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
124pub(crate) struct SwiftAboutBlock {
125    pub(crate) module: String,
126    pub(crate) class: String,
127}
128
129#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
130pub(crate) struct KotlinAboutBlock {
131    pub(crate) package: String,
132    pub(crate) class: String,
133}
134
135#[derive(Debug, Deserialize, Serialize, Clone, Default)]
136pub(crate) struct ImportBlock {
137    pub(crate) path: String,
138    pub(crate) channel: String,
139    #[serde(default)]
140    pub(crate) features: BTreeMap<String, FeatureAdditionChoices>,
141}
142
143#[derive(Debug, Deserialize, Serialize, Clone, Default)]
144pub(crate) struct FeatureAdditions {
145    #[serde(default)]
146    pub(crate) defaults: Vec<DefaultBlock>,
147    #[serde(default)]
148    pub(crate) examples: Vec<ExampleBlock>,
149}
150
151#[derive(Debug, Deserialize, Serialize, Clone)]
152#[serde(untagged)]
153pub(crate) enum FeatureAdditionChoices {
154    AllDefaults(Vec<DefaultOrExampleBlock>),
155    FeatureAdditions(FeatureAdditions),
156}
157
158#[derive(Debug, Deserialize, Serialize, Clone)]
159#[serde(untagged)]
160pub(crate) enum DefaultOrExampleBlock {
161    Default(DefaultBlock),
162    LabelledDefault(LabelledDefaultBlock),
163    LabelledExample(LabelledExampleBlock),
164}
165
166#[derive(Debug, Deserialize, Serialize, Clone)]
167pub(crate) struct LabelledExampleBlock {
168    pub(crate) example: ExampleBlock,
169}
170
171#[derive(Debug, Deserialize, Serialize, Clone)]
172pub(crate) struct LabelledDefaultBlock {
173    #[serde(alias = "defaults")]
174    pub(crate) default: DefaultBlock,
175}
176
177// Rationalize all the different ways we can express imported
178// defaults and examples for a feature into a single way that the parser
179// can use to merge.
180impl From<FeatureAdditionChoices> for FeatureAdditions {
181    fn from(choices: FeatureAdditionChoices) -> Self {
182        match choices {
183            FeatureAdditionChoices::FeatureAdditions(a) => a,
184            FeatureAdditionChoices::AllDefaults(list) => {
185                let mut examples = Vec::new();
186                let mut defaults = Vec::new();
187                for addition in list {
188                    match addition {
189                        DefaultOrExampleBlock::Default(default)
190                        | DefaultOrExampleBlock::LabelledDefault(LabelledDefaultBlock {
191                            default,
192                        }) => defaults.push(default),
193                        DefaultOrExampleBlock::LabelledExample(LabelledExampleBlock {
194                            example,
195                        }) => examples.push(example),
196                    }
197                }
198
199                FeatureAdditions { examples, defaults }
200            }
201        }
202    }
203}
204
205impl From<FeatureAdditions> for FeatureAdditionChoices {
206    fn from(additions: FeatureAdditions) -> Self {
207        FeatureAdditionChoices::FeatureAdditions(additions)
208    }
209}
210
211#[derive(Debug, Deserialize, Serialize, Clone)]
212#[serde(deny_unknown_fields)]
213#[serde(rename_all = "kebab-case")]
214pub(crate) struct FeatureBody {
215    #[serde(flatten)]
216    pub(crate) metadata: FeatureMetadata,
217    // We need these in a deterministic order, so they are stable across multiple
218    // runs of the same manifests:
219    // 1. Swift insists on args in the same order they were declared.
220    // 2. imported features are declared and constructed in different runs of the tool.
221    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
222    pub(crate) variables: BTreeMap<String, FeatureFieldBody>,
223    #[serde(alias = "defaults")]
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub(crate) default: Option<Vec<DefaultBlock>>,
226    #[serde(default)]
227    #[serde(skip_serializing_if = "std::ops::Not::not")]
228    pub(crate) allow_coenrollment: bool,
229
230    #[serde(default)]
231    #[serde(skip_serializing_if = "Vec::is_empty")]
232    pub(crate) examples: Vec<ExampleBlock>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
236#[serde(deny_unknown_fields)]
237#[serde(rename_all = "kebab-case")]
238pub(crate) struct FeatureMetadata {
239    pub(crate) description: String,
240    /// A list of named URLs to documentation for this feature.
241    #[serde(default)]
242    #[serde(skip_serializing_if = "Vec::is_empty")]
243    pub(crate) documentation: Vec<DocumentationLink>,
244    /// A list of contacts (engineers, product owners) who can be contacted for
245    /// help with this feature. Specifically for QA questions.
246    #[serde(default)]
247    #[serde(alias = "owners", alias = "owner")]
248    #[serde(skip_serializing_if = "Vec::is_empty")]
249    pub(crate) contacts: Vec<EmailAddress>,
250    /// Where should QA file issues for this feature?
251    #[serde(default)]
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub(crate) meta_bug: Option<Url>,
254    /// What Glean events can the feature produce?
255    /// These should be links to a Glean dictionary.
256    #[serde(default)]
257    #[serde(skip_serializing_if = "Vec::is_empty")]
258    pub(crate) events: Vec<Url>,
259    /// A link to a Web based configuration UI for this feature.
260    /// This UI should produce the valid JSON instead of typing it
261    /// by hand.
262    #[serde(default)]
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub(crate) configurator: Option<Url>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
268#[serde(deny_unknown_fields)]
269pub(crate) struct DocumentationLink {
270    pub(crate) name: String,
271    pub(crate) url: Url,
272}
273
274#[derive(Debug, Deserialize, Serialize, Clone, Default)]
275#[serde(deny_unknown_fields)]
276pub struct ManifestFrontEnd {
277    #[serde(default)]
278    pub(crate) version: String,
279    #[serde(default)]
280    pub(crate) about: Option<AboutBlock>,
281
282    #[serde(default)]
283    #[serde(skip_serializing_if = "Vec::is_empty")]
284    pub(crate) channels: Vec<String>,
285
286    #[serde(default)]
287    #[serde(alias = "include")]
288    #[serde(skip_serializing_if = "Vec::is_empty")]
289    pub(crate) includes: Vec<String>,
290
291    #[serde(default)]
292    #[serde(alias = "import")]
293    #[serde(skip_serializing_if = "Vec::is_empty")]
294    pub(crate) imports: Vec<ImportBlock>,
295
296    #[serde(default)]
297    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
298    pub(crate) features: BTreeMap<String, FeatureBody>,
299
300    // We'd like to get rid of the `types` property,
301    // but we need to keep supporting it.
302    #[serde(default)]
303    #[serde(rename = "types")]
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub(crate) legacy_types: Option<Types>,
306
307    // If a types attribute isn't explicitly expressed,
308    // then we should assume that we use the flattened version.
309    #[serde(default)]
310    #[serde(flatten)]
311    pub(crate) types: Types,
312}
313
314impl ManifestFrontEnd {
315    pub fn channels(&self) -> Vec<String> {
316        self.channels.clone()
317    }
318
319    pub fn includes(&self) -> Vec<String> {
320        self.includes.clone()
321    }
322
323    /// Retrieves all the types represented in the Manifest
324    ///
325    /// # Returns
326    /// Returns a [`std::collections::HashMap<String,TypeRef>`] where
327    /// the key is the name of the type, and the TypeRef represents the type itself
328    fn get_types(&self) -> HashMap<String, TypeRef> {
329        let mut res: HashMap<_, _> = Default::default();
330
331        let types = self.legacy_types.as_ref().unwrap_or(&self.types);
332        for s in types.enums.keys() {
333            res.insert(s.clone(), TypeRef::Enum(s.clone()));
334        }
335
336        for s in types.objects.keys() {
337            res.insert(s.clone(), TypeRef::Object(s.clone()));
338        }
339
340        for f in self.features.values() {
341            for p in f.variables.values() {
342                if let Some(s) = &p.string_alias {
343                    res.insert(s.clone(), TypeRef::StringAlias(s.clone()));
344                }
345            }
346        }
347        res
348    }
349
350    fn get_prop_def_from_feature_field(&self, nm: &str, body: &FeatureFieldBody) -> PropDef {
351        let mut prop = self.get_prop_def_from_field(nm, &body.field);
352        prop.pref_key.clone_from(&body.pref_key);
353        prop.gecko_pref.clone_from(&body.gecko_pref);
354        if let Some(s) = &body.string_alias {
355            prop.string_alias = Some(TypeRef::StringAlias(s.clone()));
356        }
357        prop
358    }
359
360    /// Transforms a front-end field definition, a tuple of [`String`] and [`FieldBody`],
361    /// into a [`PropDef`]
362    ///
363    /// # Arguments
364    /// - `field`: The [`(&String, &FieldBody)`] tuple to get the propdef from
365    ///
366    /// # Returns
367    /// return the IR [`PropDef`]
368    fn get_prop_def_from_field(&self, nm: &str, body: &FieldBody) -> PropDef {
369        let types = self.get_types();
370        PropDef {
371            name: nm.into(),
372            doc: body.description.clone(),
373            typ: match get_typeref_from_string(body.variable_type.to_owned(), &types) {
374                Ok(type_ref) => type_ref,
375                Err(e) => {
376                    // Try matching against the user defined types
377                    match types.get(&body.variable_type) {
378                        Some(type_ref) => type_ref.to_owned(),
379                        None => panic!(
380                            "{}\n{} is not a valid FML type or user defined type",
381                            e, body.variable_type
382                        ),
383                    }
384                }
385            },
386            default: json!(body.default),
387            string_alias: None,
388            pref_key: Default::default(),
389            gecko_pref: Default::default(),
390        }
391    }
392
393    /// Retrieves all the feature definitions represented in the manifest
394    ///
395    /// # Returns
396    /// Returns a [`std::collections::BTreeMap<String, FeatureDef>`]
397    fn get_feature_defs(&self, merger: &DefaultsMerger) -> Result<BTreeMap<String, FeatureDef>> {
398        let mut features: BTreeMap<_, _> = Default::default();
399        for (nm, body) in &self.features {
400            let mut fields: Vec<_> = Default::default();
401            for (fnm, field) in &body.variables {
402                fields.push(self.get_prop_def_from_feature_field(fnm, field));
403            }
404            let examples = body.examples.iter().map(Into::into).collect();
405
406            let mut def = FeatureDef {
407                name: nm.clone(),
408                metadata: body.metadata.clone(),
409                props: fields,
410                allow_coenrollment: body.allow_coenrollment,
411                examples,
412            };
413            merger.merge_feature_defaults(&mut def, &body.default)?;
414            features.insert(nm.to_owned(), def);
415        }
416        Ok(features)
417    }
418
419    /// Retrieves all the Object type definitions represented in the manifest
420    ///
421    /// # Returns
422    /// Returns a [`std::collections::BTreeMap<String. ObjectDef>`]
423    fn get_objects(&self) -> BTreeMap<String, ObjectDef> {
424        let types = self.legacy_types.as_ref().unwrap_or(&self.types);
425        let mut objs: BTreeMap<_, _> = Default::default();
426        for (nm, body) in &types.objects {
427            let mut fields: Vec<_> = Default::default();
428            for (fnm, field) in &body.fields {
429                fields.push(self.get_prop_def_from_field(fnm, field));
430            }
431            objs.insert(
432                nm.to_owned(),
433                ObjectDef {
434                    name: nm.clone(),
435                    doc: body.description.clone(),
436                    props: fields,
437                },
438            );
439        }
440        objs
441    }
442
443    /// Retrieves all the Enum type definitions represented in the manifest
444    ///
445    /// # Returns
446    /// Returns a [`std::collections::BTreeMap<String, EnumDef>`]
447    fn get_enums(&self) -> BTreeMap<String, EnumDef> {
448        let types = self.legacy_types.as_ref().unwrap_or(&self.types);
449        let mut enums: BTreeMap<_, _> = Default::default();
450        for (name, body) in &types.enums {
451            let mut variants: Vec<_> = Default::default();
452            for (v_name, v_body) in &body.variants {
453                variants.push(VariantDef {
454                    name: v_name.clone(),
455                    doc: v_body.description.clone(),
456                });
457            }
458            enums.insert(
459                name.to_owned(),
460                EnumDef {
461                    name: name.clone(),
462                    doc: body.description.clone(),
463                    variants,
464                },
465            );
466        }
467        enums
468    }
469
470    pub(crate) fn get_intermediate_representation(
471        &self,
472        id: &ModuleId,
473        channel: Option<&str>,
474    ) -> Result<FeatureManifest> {
475        let enums = self.get_enums();
476        let objects = self.get_objects();
477        let merger =
478            DefaultsMerger::new(&objects, self.channels.clone(), channel.map(str::to_string));
479
480        let features = self.get_feature_defs(&merger)?;
481
482        let about = match &self.about {
483            Some(a) => a.clone(),
484            None => Default::default(),
485        };
486
487        Ok(FeatureManifest::new(
488            id.clone(),
489            channel,
490            features,
491            enums,
492            objects,
493            about,
494        ))
495    }
496}
497
498#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
499#[serde(untagged)]
500pub enum ExampleBlock {
501    /// This is the complete example inlined into the manifest file.
502    Inline(InlineExampleBlock),
503    /// This is the name, description and URL inlined into the manifest, but a path to a JSON file containing the feature configuration.
504    Partial(PartialExampleBlock),
505    /// This is a path to a YAML or JSON file containing the name, description, and URL as well as the actual feature configuration.
506    Path(PathOnly),
507    /// This is a path to a YAML or JSON file containing the name, description, and URL as well as the actual feature configuration.
508    BarePath(String),
509}
510
511#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
512#[serde(deny_unknown_fields)]
513pub struct InlineExampleBlock {
514    #[serde(flatten)]
515    pub(crate) metadata: FeatureExampleMetadata,
516    pub(crate) value: serde_json::Value,
517}
518
519#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
520#[serde(deny_unknown_fields)]
521pub struct PartialExampleBlock {
522    #[serde(flatten)]
523    pub(crate) metadata: FeatureExampleMetadata,
524    pub(crate) path: String,
525}
526
527#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
528#[serde(deny_unknown_fields)]
529pub struct PathOnly {
530    pub(crate) path: String,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
534#[serde(deny_unknown_fields)]
535pub struct FeatureExampleMetadata {
536    pub(crate) name: String,
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub(crate) description: Option<String>,
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub(crate) url: Option<Url>,
541}
542
543#[derive(Debug, Deserialize, Serialize, Clone)]
544pub struct DefaultBlock {
545    #[serde(skip_serializing_if = "Option::is_none")]
546    pub(crate) channel: Option<String>,
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub(crate) channels: Option<Vec<String>>,
549    pub(crate) value: serde_json::Value,
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub(crate) targeting: Option<String>,
552}
553
554impl DefaultBlock {
555    pub fn merge_channels(&self) -> Option<Vec<String>> {
556        let mut res = HashSet::new();
557
558        if let Some(channels) = self.channels.clone() {
559            res.extend(channels)
560        }
561
562        if let Some(channel) = &self.channel {
563            res.extend(
564                channel
565                    .split(',')
566                    .filter(|channel_name| !channel_name.is_empty())
567                    .map(|channel_name| channel_name.trim().to_string())
568                    .collect::<HashSet<String>>(),
569            )
570        }
571
572        let res: Vec<String> = res.into_iter().collect();
573        if res.is_empty() {
574            None
575        } else {
576            Some(res)
577        }
578    }
579}
580
581impl From<serde_json::Value> for DefaultBlock {
582    fn from(value: serde_json::Value) -> Self {
583        Self {
584            value,
585            channels: None,
586            channel: None,
587            targeting: None,
588        }
589    }
590}
591
592#[cfg(test)]
593mod about_block {
594    use super::*;
595
596    #[test]
597    fn test_parsing_about_block() -> Result<()> {
598        let about = AboutBlock {
599            kotlin_about: Some(KotlinAboutBlock {
600                package: "com.example".to_string(),
601                class: "KotlinAbout".to_string(),
602            }),
603            ..Default::default()
604        };
605
606        let yaml = serde_yaml::to_value(&about)?;
607
608        let rehydrated = serde_yaml::from_value(yaml)?;
609
610        assert_eq!(about, rehydrated);
611
612        Ok(())
613    }
614}
615
616#[cfg(test)]
617mod default_block {
618    use super::*;
619
620    #[test]
621    fn test_merge_channels_none_when_empty() {
622        let input: DefaultBlock = serde_json::from_value(json!(
623            {
624                "channel": "",
625                "channels": [],
626                "value": {
627                    "button-color": "green"
628                }
629            }
630        ))
631        .unwrap();
632        assert!(input.merge_channels().is_none())
633    }
634
635    #[test]
636    fn test_merge_channels_merged_when_present() {
637        let input: DefaultBlock = serde_json::from_value(json!(
638            {
639                "channel": "a, b",
640                "channels": ["c"],
641                "value": {
642                    "button-color": "green"
643                }
644            }
645        ))
646        .unwrap();
647        let res = input.merge_channels();
648        assert!(res.is_some());
649        let res = res.unwrap();
650        assert!(res.contains(&"a".to_string()));
651        assert!(res.contains(&"b".to_string()));
652        assert!(res.contains(&"c".to_string()));
653    }
654
655    #[test]
656    fn test_merge_channels_merged_without_duplicates() {
657        let input: DefaultBlock = serde_json::from_value(json!(
658            {
659                "channel": "a, a",
660                "channels": ["a"],
661                "value": {
662                    "button-color": "green"
663                }
664            }
665        ))
666        .unwrap();
667        let res = input.merge_channels();
668        assert!(res.is_some());
669        let res = res.unwrap();
670        assert!(res.contains(&"a".to_string()));
671        assert!(res.len() == 1)
672    }
673}
674
675#[cfg(test)]
676mod feature_metadata {
677    use super::*;
678    use std::str::FromStr;
679
680    impl DocumentationLink {
681        fn new(nm: &str, url: &str) -> Result<Self> {
682            Ok(Self {
683                name: nm.to_string(),
684                url: Url::from_str(url)?,
685            })
686        }
687    }
688
689    #[test]
690    fn test_happy_path() -> Result<()> {
691        let fm = serde_json::from_str::<FeatureMetadata>(
692            r#"{
693            "description": "A description",
694            "meta-bug": "https://example.com/EXP-23",
695            "contacts": [
696                "jdoe@example.com"
697            ],
698            "documentation": [
699                {
700                    "name": "User documentation",
701                    "url": "https://example.info/my-feature"
702                }
703            ],
704            "events": [
705                "https://example.com/glean/dictionary/button-pressed"
706            ],
707            "configurator": "https://auth.example.com/my-feature/configuration-ui"
708        }"#,
709        )?;
710        assert_eq!(
711            fm,
712            FeatureMetadata {
713                description: "A description".to_string(),
714                meta_bug: Some(Url::from_str("https://example.com/EXP-23")?),
715                contacts: vec![EmailAddress::from_str("jdoe@example.com")?],
716                documentation: vec![DocumentationLink::new(
717                    "User documentation",
718                    "https://example.info/my-feature"
719                )?],
720                configurator: Some(Url::from_str(
721                    "https://auth.example.com/my-feature/configuration-ui"
722                )?),
723                events: vec![Url::from_str(
724                    "https://example.com/glean/dictionary/button-pressed"
725                )?,],
726            }
727        );
728
729        let fm = serde_json::from_str::<FeatureMetadata>(
730            r#"{
731            "description": "A description",
732            "meta-bug": "https://example.com/EXP-23",
733            "documentation": [
734                {
735                    "name": "User documentation",
736                    "url": "https://example.info/my-feature"
737                }
738            ]
739        }"#,
740        )?;
741        assert_eq!(
742            fm,
743            FeatureMetadata {
744                description: "A description".to_string(),
745                meta_bug: Some(Url::from_str("https://example.com/EXP-23")?),
746                contacts: Default::default(),
747                documentation: vec![DocumentationLink::new(
748                    "User documentation",
749                    "https://example.info/my-feature"
750                )?],
751                ..Default::default()
752            }
753        );
754
755        let fm = serde_json::from_str::<FeatureMetadata>(
756            r#"{
757            "description": "A description",
758            "contacts": [
759                "jdoe@example.com"
760            ]
761        }"#,
762        )?;
763        assert_eq!(
764            fm,
765            FeatureMetadata {
766                description: "A description".to_string(),
767                contacts: vec![EmailAddress::from_str("jdoe@example.com")?],
768                ..Default::default()
769            }
770        );
771
772        Ok(())
773    }
774
775    #[test]
776    fn test_invalid_email_addresses() -> Result<()> {
777        let fm = serde_json::from_str::<FeatureMetadata>(
778            r#"{
779            "description": "A description",
780            "contacts": [
781                "Not an email address"
782            ],
783        }"#,
784        );
785        assert!(fm.is_err());
786        Ok(())
787    }
788
789    #[test]
790    fn test_invalid_urls() -> Result<()> {
791        let fm = serde_json::from_str::<FeatureMetadata>(
792            r#"{
793            "description": "A description",
794            "documentation": [
795                "Not a url"
796            ],
797        }"#,
798        );
799        assert!(fm.is_err());
800
801        let fm = serde_json::from_str::<FeatureMetadata>(
802            r#"{
803            "description": "A description",
804            "meta-bug": "Not a url"
805        }"#,
806        );
807        assert!(fm.is_err());
808        Ok(())
809    }
810}
811
812#[cfg(test)]
813mod example_block {
814    use super::*;
815
816    #[test]
817    fn test_inline() -> Result<()> {
818        let v1 = json!({ "my-int": 1 });
819        let input = json!({
820            "value": v1.clone(),
821            "name": "An example example",
822            "description": "A paragraph of description",
823        });
824
825        let frontend: ExampleBlock = serde_json::from_value(input)?;
826        assert_eq!(
827            frontend,
828            ExampleBlock::Inline(InlineExampleBlock {
829                value: v1,
830                metadata: FeatureExampleMetadata {
831                    name: "An example example".to_owned(),
832                    description: Some("A paragraph of description".to_owned()),
833                    url: None
834                }
835            })
836        );
837
838        Ok(())
839    }
840
841    #[test]
842    fn test_partial_inline() -> Result<()> {
843        let input = json!({
844            "name": "An example example",
845            "description": "A paragraph of description",
846            "path": "./path/to-example.json"
847        });
848
849        let frontend: ExampleBlock = serde_json::from_value(input)?;
850        assert_eq!(
851            frontend,
852            ExampleBlock::Partial(PartialExampleBlock {
853                path: "./path/to-example.json".to_owned(),
854                metadata: FeatureExampleMetadata {
855                    name: "An example example".to_owned(),
856                    description: Some("A paragraph of description".to_owned()),
857                    url: None
858                }
859            })
860        );
861
862        Ok(())
863    }
864
865    #[test]
866    fn test_path_only() -> Result<()> {
867        let input = json!({
868            "path": "./path/to-example-with-name-and-description.yaml"
869        });
870
871        let frontend: ExampleBlock = serde_json::from_value(input)?;
872        assert_eq!(
873            frontend,
874            ExampleBlock::Path(PathOnly {
875                path: "./path/to-example-with-name-and-description.yaml".to_owned(),
876            })
877        );
878
879        Ok(())
880    }
881    #[test]
882    fn test_bare_path() -> Result<()> {
883        let input = json!("./path/to-example-with-name-and-description.yaml");
884
885        let frontend: ExampleBlock = serde_json::from_value(input)?;
886        assert_eq!(
887            frontend,
888            ExampleBlock::BarePath("./path/to-example-with-name-and-description.yaml".to_owned(),)
889        );
890
891        Ok(())
892    }
893}
894
895#[cfg(test)]
896mod feature_additions {
897    use super::*;
898
899    #[test]
900    fn test_legacy_case() -> Result<()> {
901        let v1 = json!({ "my-int": 1 });
902        let v2 = json!({ "my-int": 2 });
903        let input = json!([
904            { "value": v1.clone(), "channel": "a-channel" },
905            { "value": v2.clone() },
906        ]);
907
908        let frontend: FeatureAdditionChoices = serde_json::from_value(input)?;
909        let observed: FeatureAdditions = frontend.into();
910
911        assert_eq!(observed.defaults.len(), 2);
912        assert_eq!(
913            observed.defaults[0].channel.as_ref().unwrap().as_str(),
914            "a-channel"
915        );
916        assert_eq!(observed.defaults[0].value, v1);
917
918        assert_eq!(observed.defaults[1].channel, None);
919        assert_eq!(observed.defaults[1].value, v2);
920
921        Ok(())
922    }
923
924    #[test]
925    fn test_easy_addition_example() -> Result<()> {
926        let v1 = json!({ "my-int": 1 });
927        let v2 = json!({ "my-int": 2 });
928        let input = json!([
929            { "value": v1.clone(), "channel": "a-channel" },
930            { "example": { "name": "Example example", "value": v2.clone() } },
931            { "example": { "name": "Example example", "path": "./path/to-example.json"} },
932            { "example": { "path": "./path/to-example.yaml"} },
933            { "example": "./path/to-example.yaml" },
934        ]);
935
936        let frontend: FeatureAdditionChoices = serde_json::from_value(input)?;
937        let observed: FeatureAdditions = frontend.into();
938
939        assert_eq!(observed.defaults.len(), 1);
940        assert_eq!(
941            observed.defaults[0].channel.as_ref().unwrap().as_str(),
942            "a-channel"
943        );
944        assert_eq!(observed.defaults[0].value, v1);
945
946        assert_eq!(observed.examples.len(), 4);
947        assert!(matches!(&observed.examples[0], ExampleBlock::Inline(..)));
948        assert!(matches!(&observed.examples[1], ExampleBlock::Partial(..)));
949        assert!(matches!(&observed.examples[2], ExampleBlock::Path(..)));
950        assert!(matches!(&observed.examples[3], ExampleBlock::BarePath(..)));
951
952        Ok(())
953    }
954
955    #[test]
956    fn test_list_of_defaults_or_examples() -> Result<()> {
957        let v1 = json!({ "my-int": 1 });
958        let v2 = json!({ "my-int": 2 });
959        let input = json!([
960            { "default": { "value": v1.clone(), "channel": "a-channel" } },
961            { "example": { "name": "Example example", "value": v2.clone() } },
962        ]);
963
964        let frontend: FeatureAdditionChoices = serde_json::from_value(input)?;
965        let observed: FeatureAdditions = frontend.into();
966
967        assert_eq!(observed.defaults.len(), 1);
968        assert_eq!(
969            observed.defaults[0].channel.as_ref().unwrap().as_str(),
970            "a-channel"
971        );
972        assert_eq!(observed.defaults[0].value, v1);
973
974        assert_eq!(observed.examples.len(), 1);
975        assert!(matches!(&observed.examples[0], ExampleBlock::Inline(..)));
976
977        Ok(())
978    }
979
980    #[test]
981    fn test_list_of_defaults_and_list_of_examples() -> Result<()> {
982        let v1 = json!({ "my-int": 1 });
983        let v2 = json!({ "my-int": 2 });
984        let input = json!({
985            "defaults": [ { "value": v1.clone(), "channel": "a-channel" } ],
986            "examples": [ { "name": "Example example", "value": v2.clone() } ],
987        });
988
989        let frontend: FeatureAdditionChoices = serde_json::from_value(input)?;
990        let observed: FeatureAdditions = frontend.into();
991
992        assert_eq!(observed.defaults.len(), 1);
993        assert_eq!(
994            observed.defaults[0].channel.as_ref().unwrap().as_str(),
995            "a-channel"
996        );
997        assert_eq!(observed.defaults[0].value, v1);
998
999        assert_eq!(observed.examples.len(), 1);
1000        assert!(matches!(&observed.examples[0], ExampleBlock::Inline(..)));
1001
1002        Ok(())
1003    }
1004}