nimbus_fml/
intermediate_representation.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/. */
4use crate::defaults::{DefaultsHasher, DefaultsMerger, DefaultsValidator};
5use crate::error::FMLError::InvalidFeatureError;
6use crate::error::{FMLError, Result};
7use crate::frontend::{
8    AboutBlock, ExampleBlock, FeatureExampleMetadata, FeatureMetadata, InlineExampleBlock,
9};
10use crate::schema::{SchemaHasher, SchemaValidator, TypeQuery};
11use crate::util::loaders::FilePath;
12use anyhow::{bail, Error, Result as AnyhowResult};
13use serde::{Deserialize, Serialize};
14use serde_json::{Map, Value};
15use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
16use std::fmt::{Display, Formatter};
17
18#[derive(Eq, PartialEq, Hash, Debug, Clone)]
19pub enum TargetLanguage {
20    Kotlin,
21    Swift,
22    IR,
23    ExperimenterYAML,
24    ExperimenterJSON,
25}
26
27impl TargetLanguage {
28    pub fn extension(&self) -> &str {
29        match self {
30            TargetLanguage::Kotlin => "kt",
31            TargetLanguage::Swift => "swift",
32            TargetLanguage::IR => "fml.json",
33            TargetLanguage::ExperimenterJSON => "json",
34            TargetLanguage::ExperimenterYAML => "yaml",
35        }
36    }
37
38    pub fn from_extension(path: &str) -> AnyhowResult<TargetLanguage> {
39        if let Some((_, extension)) = path.rsplit_once('.') {
40            extension.try_into()
41        } else {
42            bail!("Unknown or unsupported target language: \"{}\"", path)
43        }
44    }
45}
46
47impl TryFrom<&str> for TargetLanguage {
48    type Error = Error;
49    fn try_from(value: &str) -> AnyhowResult<Self> {
50        Ok(match value.to_ascii_lowercase().as_str() {
51            "kotlin" | "kt" | "kts" => TargetLanguage::Kotlin,
52            "swift" => TargetLanguage::Swift,
53            "fml.json" => TargetLanguage::IR,
54            "yaml" => TargetLanguage::ExperimenterYAML,
55            "json" => TargetLanguage::ExperimenterJSON,
56            _ => bail!("Unknown or unsupported target language: \"{}\"", value),
57        })
58    }
59}
60
61/// The `TypeRef` enum defines a reference to a type.
62///
63/// Other types will be defined in terms of these enum values.
64///
65/// They represent the types available via the current `Variables` API—
66/// some primitives and structural types— and can be represented by
67/// Kotlin, Swift and JSON Schema.
68///
69#[non_exhaustive]
70#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)]
71pub enum TypeRef {
72    // Current primitives.
73    String,
74    Int,
75    Boolean,
76
77    // String-alias
78    StringAlias(String),
79
80    // Strings can be coerced into a few types.
81    // The types here will require the app's bundle or context to look
82    // up the final value.
83    BundleText,
84    BundleImage,
85
86    Enum(String),
87    // JSON objects can represent a data class.
88    Object(String),
89
90    // JSON objects can also represent a `Map<String, V>` or a `Map` with
91    // keys that can be derived from a string.
92    StringMap(Box<TypeRef>),
93    // We can coerce the String keys into Enums, so this represents that.
94    EnumMap(Box<TypeRef>, Box<TypeRef>),
95
96    List(Box<TypeRef>),
97    Option(Box<TypeRef>),
98}
99
100impl Display for TypeRef {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self {
103            Self::String => f.write_str("String"),
104            Self::Int => f.write_str("Int"),
105            Self::Boolean => f.write_str("Boolean"),
106            Self::BundleImage => f.write_str("Image"),
107            Self::BundleText => f.write_str("Text"),
108            Self::StringAlias(v) => f.write_str(v),
109            Self::Enum(v) => f.write_str(v),
110            Self::Object(v) => f.write_str(v),
111            Self::Option(v) => f.write_fmt(format_args!("Option<{v}>")),
112            Self::List(v) => f.write_fmt(format_args!("List<{v}>")),
113            Self::StringMap(v) => f.write_fmt(format_args!("Map<String, {v}>")),
114            Self::EnumMap(k, v) => f.write_fmt(format_args!("Map<{k}, {v}>")),
115        }
116    }
117}
118
119impl TypeRef {
120    pub(crate) fn supports_prefs(&self) -> bool {
121        match self {
122            Self::Boolean | Self::String | Self::Int | Self::StringAlias(_) | Self::BundleText => {
123                true
124            }
125            // There may be a chance that we can get Self::Option to work, but not at this time.
126            // This may be done by adding a branch to this match and adding a `preference_getter` to
127            // the `OptionalCodeType`.
128            _ => false,
129        }
130    }
131
132    pub(crate) fn name(&self) -> Option<&str> {
133        match self {
134            Self::Enum(s) | Self::Object(s) | Self::StringAlias(s) => Some(s),
135            _ => None,
136        }
137    }
138}
139
140/**
141 * An identifier derived from a `FilePath` of a top-level or importable FML file.
142 *
143 * An FML module is the conceptual FML file (and included FML files) that a single
144 * Kotlin or Swift file. It can be imported by other FML modules.
145 *
146 * It is somewhat distinct from the `FilePath` enum for three reasons:
147 *
148 * - a file path can specify a non-canonical representation of the path
149 * - a file path is difficult to serialize/deserialize
150 * - a module identifies the cluster of FML files that map to a single generated
151 *   Kotlin or Swift file; this difference can be seen as: files can be included,
152 *   modules can be imported.
153 */
154#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone, Serialize, Deserialize)]
155pub enum ModuleId {
156    Local(String),
157    Remote(String),
158}
159
160impl Default for ModuleId {
161    fn default() -> Self {
162        Self::Local("none".to_string())
163    }
164}
165
166impl TryFrom<&FilePath> for ModuleId {
167    type Error = FMLError;
168    fn try_from(path: &FilePath) -> Result<Self> {
169        Ok(match path {
170            FilePath::Local(p) => {
171                // We do this map_err here because the IO Error message that comes out of `canonicalize`
172                // doesn't include the problematic file path.
173                let p = p.canonicalize().map_err(|e| {
174                    FMLError::InvalidPath(format!("{}: {}", e, p.as_path().display()))
175                })?;
176                ModuleId::Local(p.display().to_string())
177            }
178            FilePath::Remote(u) => ModuleId::Remote(u.to_string()),
179            FilePath::GitHub(p) => ModuleId::Remote(p.default_download_url_as_str()),
180        })
181    }
182}
183
184impl Display for ModuleId {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        f.write_str(match self {
187            ModuleId::Local(s) | ModuleId::Remote(s) => s,
188        })
189    }
190}
191
192pub trait TypeFinder {
193    fn all_types(&self) -> HashSet<TypeRef> {
194        let mut types = HashSet::new();
195        self.find_types(&mut types);
196        types
197    }
198
199    fn find_types(&self, types: &mut HashSet<TypeRef>);
200}
201
202impl TypeFinder for TypeRef {
203    fn find_types(&self, types: &mut HashSet<TypeRef>) {
204        if types.insert(self.clone()) {
205            match self {
206                TypeRef::List(v) | TypeRef::Option(v) | TypeRef::StringMap(v) => {
207                    v.find_types(types)
208                }
209                TypeRef::EnumMap(k, v) => {
210                    k.find_types(types);
211                    v.find_types(types);
212                }
213                _ => {}
214            }
215        }
216    }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
220pub struct FeatureManifest {
221    #[serde(skip)]
222    pub(crate) id: ModuleId,
223
224    #[serde(skip_serializing_if = "Option::is_none")]
225    #[serde(default)]
226    pub(crate) channel: Option<String>,
227
228    #[serde(rename = "enums")]
229    #[serde(default)]
230    pub(crate) enum_defs: BTreeMap<String, EnumDef>,
231    #[serde(rename = "objects")]
232    #[serde(default)]
233    pub(crate) obj_defs: BTreeMap<String, ObjectDef>,
234    #[serde(rename = "features")]
235    pub(crate) feature_defs: BTreeMap<String, FeatureDef>,
236    #[serde(default)]
237    pub(crate) about: AboutBlock,
238
239    #[serde(default)]
240    pub(crate) imported_features: BTreeMap<ModuleId, BTreeSet<String>>,
241
242    #[serde(default)]
243    pub(crate) all_imports: BTreeMap<ModuleId, FeatureManifest>,
244}
245
246impl TypeFinder for FeatureManifest {
247    fn find_types(&self, types: &mut HashSet<TypeRef>) {
248        for e in self.enum_defs.values() {
249            e.find_types(types);
250        }
251        for o in self.iter_object_defs() {
252            o.find_types(types);
253        }
254        for f in self.iter_feature_defs() {
255            f.find_types(types);
256        }
257    }
258}
259
260#[cfg(test)]
261impl FeatureManifest {
262    pub(crate) fn add_feature(&mut self, feature: FeatureDef) {
263        self.feature_defs.insert(feature.name(), feature);
264    }
265}
266
267impl FeatureManifest {
268    pub(crate) fn new(
269        id: ModuleId,
270        channel: Option<&str>,
271        features: BTreeMap<String, FeatureDef>,
272        enums: BTreeMap<String, EnumDef>,
273        objects: BTreeMap<String, ObjectDef>,
274        about: AboutBlock,
275    ) -> Self {
276        Self {
277            id,
278            channel: channel.map(str::to_string),
279            about,
280            enum_defs: enums,
281            obj_defs: objects,
282            feature_defs: features,
283
284            ..Default::default()
285        }
286    }
287
288    #[allow(unused)]
289    pub(crate) fn validate_manifest_for_lang(&self, lang: &TargetLanguage) -> Result<()> {
290        if !&self.about.supports(lang) {
291            return Err(FMLError::ValidationError(
292                "about".to_string(),
293                format!(
294                    "Manifest file {file} is unable to generate {lang} files",
295                    file = &self.id,
296                    lang = &lang.extension(),
297                ),
298            ));
299        }
300        for child in self.all_imports.values() {
301            child.validate_manifest_for_lang(lang)?;
302        }
303        Ok(())
304    }
305
306    pub fn validate_manifest(&self) -> Result<()> {
307        // We then validate that each type_ref is valid
308        self.validate_schema()?;
309        self.validate_defaults()?;
310
311        // Validating the imported manifests.
312        // This is not only validating the well formed-ness of the imported manifests
313        // but also the defaults that are sent into the child manifests.
314        for child in self.all_imports.values() {
315            child.validate_manifest()?;
316        }
317        Ok(())
318    }
319
320    fn validate_schema(&self) -> Result<(), FMLError> {
321        let validator = SchemaValidator::new(&self.enum_defs, &self.obj_defs);
322        for object in self.iter_object_defs() {
323            validator.validate_object_def(object)?;
324        }
325        for feature_def in self.iter_feature_defs() {
326            validator.validate_feature_def(feature_def)?;
327        }
328        validator.validate_prefs(self)?;
329        Ok(())
330    }
331
332    fn validate_defaults(&self) -> Result<()> {
333        let validator = DefaultsValidator::new(&self.enum_defs, &self.obj_defs);
334        for object in self.iter_object_defs() {
335            validator.validate_object_def(object)?;
336        }
337        for feature in self.iter_feature_defs() {
338            validator.validate_feature_def(feature)?;
339        }
340        Ok(())
341    }
342
343    pub fn iter_enum_defs(&self) -> impl Iterator<Item = &EnumDef> {
344        self.enum_defs.values()
345    }
346
347    pub fn iter_all_enum_defs(&self) -> impl Iterator<Item = (&FeatureManifest, &EnumDef)> {
348        let enums = self.iter_enum_defs().map(move |o| (self, o));
349        let imported: Vec<_> = self
350            .all_imports
351            .values()
352            .flat_map(|fm| fm.iter_all_enum_defs())
353            .collect();
354        enums.chain(imported)
355    }
356
357    pub fn iter_object_defs(&self) -> impl Iterator<Item = &ObjectDef> {
358        self.obj_defs.values()
359    }
360
361    pub fn iter_all_object_defs(&self) -> impl Iterator<Item = (&FeatureManifest, &ObjectDef)> {
362        let objects = self.iter_object_defs().map(move |o| (self, o));
363        let imported: Vec<_> = self
364            .all_imports
365            .values()
366            .flat_map(|fm| fm.iter_all_object_defs())
367            .collect();
368        objects.chain(imported)
369    }
370
371    pub fn iter_feature_defs(&self) -> impl Iterator<Item = &FeatureDef> {
372        self.feature_defs.values()
373    }
374
375    pub fn iter_gecko_prefs(&self) -> impl Iterator<Item = &GeckoPrefDef> {
376        self.iter_feature_defs()
377            .filter(|f| f.has_gecko_prefs())
378            .flat_map(|f| {
379                f.feature_mapped_to_prop_and_gecko_pref()
380                    .iter()
381                    .flat_map(|p| p.1.clone())
382                    .collect::<Vec<_>>()
383            })
384            .map(|p| p.1)
385    }
386
387    pub fn iter_features_with_prefs(
388        &self,
389    ) -> impl Iterator<Item = (String, Vec<(String, &GeckoPrefDef)>)> {
390        self.iter_feature_defs()
391            .filter(|f| f.has_gecko_prefs())
392            .flat_map(|f| f.feature_mapped_to_prop_and_gecko_pref())
393    }
394
395    pub fn iter_all_feature_defs(&self) -> impl Iterator<Item = (&FeatureManifest, &FeatureDef)> {
396        let features = self.iter_feature_defs().map(move |f| (self, f));
397        let imported: Vec<_> = self
398            .all_imports
399            .values()
400            .flat_map(|fm| fm.iter_all_feature_defs())
401            .collect();
402        features.chain(imported)
403    }
404
405    #[allow(unused)]
406    pub(crate) fn iter_imported_files(&self) -> Vec<ImportedModule> {
407        let map = &self.all_imports;
408
409        self.imported_features
410            .iter()
411            .filter_map(|(id, features)| {
412                let fm = map.get(id).to_owned()?;
413                Some(ImportedModule::new(fm, features))
414            })
415            .collect()
416    }
417
418    pub fn find_object(&self, nm: &str) -> Option<&ObjectDef> {
419        self.obj_defs.get(nm)
420    }
421
422    pub fn find_enum(&self, nm: &str) -> Option<&EnumDef> {
423        self.enum_defs.get(nm)
424    }
425
426    pub fn get_feature(&self, nm: &str) -> Option<&FeatureDef> {
427        self.feature_defs.get(nm)
428    }
429
430    pub fn get_coenrolling_feature_ids(&self) -> Vec<String> {
431        self.iter_all_feature_defs()
432            .filter(|(_, f)| f.allow_coenrollment())
433            .map(|(_, f)| f.name())
434            .collect()
435    }
436
437    pub fn find_feature(&self, nm: &str) -> Option<(&FeatureManifest, &FeatureDef)> {
438        if let Some(f) = self.get_feature(nm) {
439            Some((self, f))
440        } else {
441            self.all_imports.values().find_map(|fm| fm.find_feature(nm))
442        }
443    }
444
445    pub fn find_import(&self, id: &ModuleId) -> Option<&FeatureManifest> {
446        self.all_imports.get(id)
447    }
448
449    pub fn default_json(&self) -> Value {
450        Value::Object(
451            self.iter_all_feature_defs()
452                .map(|(_, f)| (f.name(), f.default_json()))
453                .collect(),
454        )
455    }
456
457    /// This function is used to validate a new value for a feature. It accepts a feature name and
458    /// a feature value, and returns a Result containing a FeatureDef.
459    ///
460    /// If the value is invalid for the feature, it will return an Err result.
461    ///
462    /// If the value is valid for the feature, it will return an Ok result with a new FeatureDef
463    /// with the supplied feature value applied to the feature's property defaults.
464    pub fn validate_feature_config(
465        &self,
466        feature_name: &str,
467        feature_value: Value,
468    ) -> Result<FeatureDef> {
469        let (manifest, feature_def) = self
470            .find_feature(feature_name)
471            .ok_or_else(|| InvalidFeatureError(feature_name.to_string()))?;
472
473        let merger = DefaultsMerger::new(&manifest.obj_defs, Default::default(), None);
474        let merged_value = merger.merge_feature_config(feature_def, &feature_value);
475
476        let validator = DefaultsValidator::new(&manifest.enum_defs, &manifest.obj_defs);
477        let errors = validator.get_errors(feature_def, &merged_value, &feature_value);
478        validator.guard_errors(feature_def, &merged_value, errors)?;
479
480        let mut feature_def = feature_def.clone();
481        merger.overwrite_defaults(&mut feature_def, &merged_value);
482        Ok(feature_def)
483    }
484
485    #[allow(dead_code)]
486    #[cfg(feature = "client-lib")]
487    pub(crate) fn merge_and_errors(
488        &self,
489        feature_def: &FeatureDef,
490        feature_value: &Value,
491    ) -> (Value, Vec<crate::editing::FeatureValidationError>) {
492        let merger = DefaultsMerger::new(&self.obj_defs, Default::default(), None);
493        let merged_value = merger.merge_feature_config(feature_def, feature_value);
494
495        let validator = DefaultsValidator::new(&self.enum_defs, &self.obj_defs);
496        let errors = validator.get_errors(feature_def, &merged_value, feature_value);
497        (merged_value, errors)
498    }
499}
500
501impl FeatureManifest {
502    pub(crate) fn feature_types(&self, feature_def: &FeatureDef) -> HashSet<TypeRef> {
503        TypeQuery::new(&self.obj_defs).all_types(feature_def)
504    }
505
506    pub(crate) fn feature_schema_hash(&self, feature_def: &FeatureDef) -> String {
507        let hasher = SchemaHasher::new(&self.enum_defs, &self.obj_defs);
508        let hash = hasher.hash(feature_def) & 0xffffffff;
509        format!("{hash:x}")
510    }
511
512    pub(crate) fn feature_defaults_hash(&self, feature_def: &FeatureDef) -> String {
513        let hasher = DefaultsHasher::new(&self.obj_defs);
514        let hash = hasher.hash(feature_def) & 0xffffffff;
515        format!("{hash:x}")
516    }
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
520pub struct FeatureDef {
521    pub(crate) name: String,
522    #[serde(flatten)]
523    pub(crate) metadata: FeatureMetadata,
524    pub(crate) props: Vec<PropDef>,
525    pub(crate) allow_coenrollment: bool,
526    #[serde(default)]
527    #[serde(skip_serializing_if = "Vec::is_empty")]
528    pub(crate) examples: Vec<FeatureExample>,
529}
530
531impl FeatureDef {
532    pub fn new(name: &str, doc: &str, props: Vec<PropDef>, allow_coenrollment: bool) -> Self {
533        Self {
534            name: name.into(),
535            metadata: FeatureMetadata {
536                description: doc.into(),
537                ..Default::default()
538            },
539            props,
540            allow_coenrollment,
541            ..Default::default()
542        }
543    }
544    pub fn name(&self) -> String {
545        self.name.clone()
546    }
547    pub fn doc(&self) -> String {
548        self.metadata.description.clone()
549    }
550    pub fn props(&self) -> Vec<PropDef> {
551        self.props.clone()
552    }
553    pub fn allow_coenrollment(&self) -> bool {
554        self.allow_coenrollment
555    }
556
557    pub fn default_json(&self) -> Value {
558        let mut props = Map::new();
559
560        for prop in self.props().iter() {
561            props.insert(prop.name(), prop.default());
562        }
563
564        Value::Object(props)
565    }
566
567    pub fn has_prefs(&self) -> bool {
568        self.props.iter().any(|p| p.has_prefs())
569    }
570
571    pub fn has_gecko_prefs(&self) -> bool {
572        self.props.iter().any(|p| p.has_gecko_prefs())
573    }
574
575    pub fn get_string_aliases(&self) -> HashMap<&str, &PropDef> {
576        let mut res: HashMap<_, _> = Default::default();
577        for p in &self.props {
578            if let Some(TypeRef::StringAlias(s)) = &p.string_alias {
579                res.insert(s.as_str(), p);
580            }
581        }
582        res
583    }
584
585    pub fn get_prop(&self, name: &str) -> Option<&PropDef> {
586        self.props.iter().find(|p| p.name == name)
587    }
588
589    pub fn feature_mapped_to_prop_and_gecko_pref(
590        &self,
591    ) -> Vec<(String, Vec<(String, &GeckoPrefDef)>)> {
592        self.props
593            .iter()
594            .filter(|p| p.has_gecko_prefs() && p.gecko_pref.is_some())
595            .map(|p| (p.gecko_pref.as_ref(), p.name()))
596            .map(|(p, n)| (p, (n, self.name())))
597            .rfold(Vec::new(), |mut acc: Vec<(String, Vec<(String, &GeckoPrefDef)>)>, (pref, (prop, feature)): (Option<&GeckoPrefDef>, (String, String))| {
598                match acc.iter_mut().find(|p| p.0 == feature) {
599                    Some((_, ref mut props)) => {
600                        props.push((prop, pref.unwrap()));
601                    }
602                    None => {
603                        let props = vec![(prop, pref.unwrap())];
604                        acc.push((feature, props));
605                    }
606                }
607                acc
608            })
609    }
610}
611
612impl TypeFinder for FeatureDef {
613    fn find_types(&self, types: &mut HashSet<TypeRef>) {
614        for p in self.props() {
615            p.find_types(types);
616        }
617    }
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
621pub struct EnumDef {
622    pub name: String,
623    pub doc: String,
624    pub variants: Vec<VariantDef>,
625}
626
627impl EnumDef {
628    pub fn name(&self) -> String {
629        self.name.clone()
630    }
631    pub fn doc(&self) -> String {
632        self.doc.clone()
633    }
634    pub fn variants(&self) -> Vec<VariantDef> {
635        self.variants.clone()
636    }
637}
638
639impl TypeFinder for EnumDef {
640    fn find_types(&self, types: &mut HashSet<TypeRef>) {
641        types.insert(TypeRef::Enum(self.name()));
642    }
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
646pub struct VariantDef {
647    pub(crate) name: String,
648    pub(crate) doc: String,
649}
650impl VariantDef {
651    pub fn new(name: &str, doc: &str) -> Self {
652        Self {
653            name: name.into(),
654            doc: doc.into(),
655        }
656    }
657    pub fn name(&self) -> String {
658        self.name.clone()
659    }
660    pub fn doc(&self) -> String {
661        self.doc.clone()
662    }
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
666pub struct ObjectDef {
667    pub(crate) name: String,
668    pub(crate) doc: String,
669    pub(crate) props: Vec<PropDef>,
670}
671
672impl ObjectDef {
673    pub(crate) fn name(&self) -> String {
674        self.name.clone()
675    }
676    pub(crate) fn doc(&self) -> String {
677        self.doc.clone()
678    }
679    pub fn props(&self) -> Vec<PropDef> {
680        self.props.clone()
681    }
682
683    pub(crate) fn find_prop(&self, nm: &str) -> PropDef {
684        self.props
685            .iter()
686            .find(|p| p.name == nm)
687            .unwrap_or_else(|| unreachable!("Can't find {}. This is a bug in FML", nm))
688            .clone()
689    }
690}
691impl TypeFinder for ObjectDef {
692    fn find_types(&self, types: &mut HashSet<TypeRef>) {
693        for p in self.props() {
694            p.find_types(types);
695        }
696    }
697}
698
699#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
700#[serde(rename_all = "lowercase")]
701pub enum PrefBranch {
702    Default,
703    User,
704}
705
706impl Display for PrefBranch {
707    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
708        match self {
709            PrefBranch::Default => f.write_str("default"),
710            PrefBranch::User => f.write_str("user"),
711        }
712    }
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
716pub struct GeckoPrefDef {
717    pub(crate) pref: String,
718    pub(crate) branch: PrefBranch,
719}
720
721impl GeckoPrefDef {
722    pub fn pref(&self) -> String {
723        self.pref.clone()
724    }
725    pub fn branch(&self) -> PrefBranch {
726        self.branch.clone()
727    }
728}
729
730#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
731pub struct PropDef {
732    pub(crate) name: String,
733    pub(crate) doc: String,
734    #[serde(rename = "type")]
735    pub(crate) typ: TypeRef,
736    pub(crate) default: Literal,
737    #[serde(default)]
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub(crate) pref_key: Option<String>,
740    #[serde(default)]
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub(crate) gecko_pref: Option<GeckoPrefDef>,
743    #[serde(default)]
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub(crate) string_alias: Option<TypeRef>,
746}
747
748impl PropDef {
749    pub fn name(&self) -> String {
750        self.name.clone()
751    }
752    pub fn doc(&self) -> String {
753        self.doc.clone()
754    }
755    pub fn typ(&self) -> TypeRef {
756        self.typ.clone()
757    }
758    pub fn default(&self) -> Literal {
759        self.default.clone()
760    }
761    pub fn has_prefs(&self) -> bool {
762        self.pref_key.is_some() && self.typ.supports_prefs()
763    }
764    pub fn has_gecko_prefs(&self) -> bool {
765        self.gecko_pref.is_some() && self.typ.supports_prefs()
766    }
767    pub fn pref_key(&self) -> Option<String> {
768        self.pref_key.clone()
769    }
770    pub fn gecko_pref(&self) -> Option<GeckoPrefDef> {
771        self.gecko_pref.clone()
772    }
773}
774
775impl TypeFinder for PropDef {
776    fn find_types(&self, types: &mut HashSet<TypeRef>) {
777        self.typ.find_types(types);
778    }
779}
780
781pub type Literal = Value;
782
783#[derive(Debug, Clone)]
784pub(crate) struct ImportedModule<'a> {
785    pub(crate) fm: &'a FeatureManifest,
786    features: &'a BTreeSet<String>,
787}
788
789impl<'a> ImportedModule<'a> {
790    pub(crate) fn new(fm: &'a FeatureManifest, features: &'a BTreeSet<String>) -> Self {
791        Self { fm, features }
792    }
793
794    pub(crate) fn about(&self) -> &AboutBlock {
795        &self.fm.about
796    }
797
798    pub(crate) fn features(&self) -> Vec<&'a FeatureDef> {
799        let fm = self.fm;
800        self.features
801            .iter()
802            .filter_map(|f| fm.get_feature(f))
803            .collect()
804    }
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
808pub(crate) struct FeatureExample {
809    pub(crate) metadata: FeatureExampleMetadata,
810    pub(crate) value: Value,
811}
812
813impl From<&ExampleBlock> for FeatureExample {
814    fn from(value: &ExampleBlock) -> Self {
815        match value {
816            ExampleBlock::Inline(InlineExampleBlock { metadata, value }) => Self {
817                metadata: metadata.to_owned(),
818                value: value.to_owned(),
819            },
820            _ => unreachable!(
821                "Examples should have been inlined by now. This is a bug in nimbus-fml"
822            ),
823        }
824    }
825}
826
827#[cfg(test)]
828pub mod unit_tests {
829    use serde_json::json;
830
831    use super::*;
832    use crate::error::Result;
833    use crate::fixtures::intermediate_representation::get_simple_homescreen_feature;
834
835    #[test]
836    fn can_ir_represent_smoke_test() -> Result<()> {
837        let reference_manifest = get_simple_homescreen_feature();
838        let json_string = serde_json::to_string(&reference_manifest)?;
839        let manifest_from_json: FeatureManifest = serde_json::from_str(&json_string)?;
840
841        assert_eq!(reference_manifest, manifest_from_json);
842
843        Ok(())
844    }
845
846    #[test]
847    fn validate_good_feature_manifest() -> Result<()> {
848        let fm = get_simple_homescreen_feature();
849        fm.validate_manifest()
850    }
851
852    #[test]
853    fn validate_allow_coenrollment() -> Result<()> {
854        let mut fm = get_simple_homescreen_feature();
855        fm.add_feature(FeatureDef::new(
856            "some_def",
857            "my lovely qtest doc",
858            vec![PropDef::new(
859                "some prop",
860                &TypeRef::String,
861                &json!("default"),
862            )],
863            true,
864        ));
865        fm.validate_manifest()?;
866        let coenrolling_ids = fm.get_coenrolling_feature_ids();
867        assert_eq!(coenrolling_ids, vec!["some_def".to_string()]);
868
869        Ok(())
870    }
871}
872
873#[cfg(test)]
874mod imports_tests {
875    use super::*;
876
877    use serde_json::json;
878
879    use crate::fixtures::intermediate_representation::{
880        get_feature_manifest, get_one_prop_feature_manifest,
881        get_one_prop_feature_manifest_with_imports,
882    };
883
884    #[test]
885    fn test_iter_object_defs_deep_iterates_on_all_imports() -> Result<()> {
886        let prop_i = PropDef::new(
887            "key_i",
888            &TypeRef::Object("SampleObjImported".into()),
889            &json!({
890                "string": "bobo",
891            }),
892        );
893        let obj_defs_i = vec![ObjectDef::new(
894            "SampleObjImported",
895            &[PropDef::new("string", &TypeRef::String, &json!("a string"))],
896        )];
897        let fm_i = get_one_prop_feature_manifest(obj_defs_i, vec![], &prop_i);
898
899        let prop = PropDef::new(
900            "key",
901            &TypeRef::Object("SampleObj".into()),
902            &json!({
903                "string": "bobo",
904            }),
905        );
906        let obj_defs = vec![ObjectDef::new(
907            "SampleObj",
908            &[PropDef::new("string", &TypeRef::String, &json!("a string"))],
909        )];
910        let fm = get_one_prop_feature_manifest_with_imports(
911            obj_defs,
912            vec![],
913            &prop,
914            BTreeMap::from([(ModuleId::Local("test".into()), fm_i)]),
915        );
916
917        let names: Vec<String> = fm.iter_all_object_defs().map(|(_, o)| o.name()).collect();
918
919        assert_eq!(names[0], "SampleObj".to_string());
920        assert_eq!(names[1], "SampleObjImported".to_string());
921
922        Ok(())
923    }
924
925    #[test]
926    fn test_iter_feature_defs_deep_iterates_on_all_imports() -> Result<()> {
927        let prop_i = PropDef::new("key_i", &TypeRef::String, &json!("string"));
928        let fm_i = get_one_prop_feature_manifest(vec![], vec![], &prop_i);
929
930        let prop = PropDef::new("key", &TypeRef::String, &json!("string"));
931        let fm = get_one_prop_feature_manifest_with_imports(
932            vec![],
933            vec![],
934            &prop,
935            BTreeMap::from([(ModuleId::Local("test".into()), fm_i)]),
936        );
937
938        let names: Vec<String> = fm
939            .iter_all_feature_defs()
940            .map(|(_, f)| f.props[0].name())
941            .collect();
942
943        assert_eq!(names[0], "key".to_string());
944        assert_eq!(names[1], "key_i".to_string());
945
946        Ok(())
947    }
948
949    #[test]
950    fn test_find_feature_deep_finds_across_all_imports() -> Result<()> {
951        let fm_i = get_feature_manifest(
952            vec![],
953            vec![],
954            vec![FeatureDef {
955                name: "feature_i".into(),
956                ..Default::default()
957            }],
958            BTreeMap::new(),
959        );
960
961        let fm = get_feature_manifest(
962            vec![],
963            vec![],
964            vec![FeatureDef {
965                name: "feature".into(),
966                ..Default::default()
967            }],
968            BTreeMap::from([(ModuleId::Local("test".into()), fm_i)]),
969        );
970
971        let feature = fm.find_feature("feature_i");
972
973        assert!(feature.is_some());
974
975        Ok(())
976    }
977
978    #[test]
979    fn test_get_coenrolling_feature_finds_across_all_imports() -> Result<()> {
980        let fm_i = get_feature_manifest(
981            vec![],
982            vec![],
983            vec![
984                FeatureDef {
985                    name: "coenrolling_import_1".into(),
986                    allow_coenrollment: true,
987                    ..Default::default()
988                },
989                FeatureDef {
990                    name: "coenrolling_import_2".into(),
991                    allow_coenrollment: true,
992                    ..Default::default()
993                },
994            ],
995            BTreeMap::new(),
996        );
997
998        let fm = get_feature_manifest(
999            vec![],
1000            vec![],
1001            vec![
1002                FeatureDef {
1003                    name: "coenrolling_feature".into(),
1004                    allow_coenrollment: true,
1005                    ..Default::default()
1006                },
1007                FeatureDef {
1008                    name: "non_coenrolling_feature".into(),
1009                    allow_coenrollment: false,
1010                    ..Default::default()
1011                },
1012            ],
1013            BTreeMap::from([(ModuleId::Local("test".into()), fm_i)]),
1014        );
1015
1016        let coenrolling_features = fm.get_coenrolling_feature_ids();
1017        let expected = vec![
1018            "coenrolling_feature".to_string(),
1019            "coenrolling_import_1".to_string(),
1020            "coenrolling_import_2".to_string(),
1021        ];
1022
1023        assert_eq!(coenrolling_features, expected);
1024
1025        Ok(())
1026    }
1027
1028    #[test]
1029    fn test_no_coenrolling_feature_finds_across_all_imports() -> Result<()> {
1030        let fm_i = get_feature_manifest(
1031            vec![],
1032            vec![],
1033            vec![FeatureDef {
1034                name: "not_coenrolling_import".into(),
1035                allow_coenrollment: false,
1036                ..Default::default()
1037            }],
1038            BTreeMap::new(),
1039        );
1040
1041        let fm = get_feature_manifest(
1042            vec![],
1043            vec![],
1044            vec![
1045                FeatureDef {
1046                    name: "non_coenrolling_feature_1".into(),
1047                    allow_coenrollment: false,
1048                    ..Default::default()
1049                },
1050                FeatureDef {
1051                    name: "non_coenrolling_feature_2".into(),
1052                    allow_coenrollment: false,
1053                    ..Default::default()
1054                },
1055            ],
1056            BTreeMap::from([(ModuleId::Local("test".into()), fm_i)]),
1057        );
1058
1059        let coenrolling_features = fm.get_coenrolling_feature_ids();
1060        let expected: Vec<String> = vec![];
1061
1062        assert_eq!(coenrolling_features, expected);
1063
1064        Ok(())
1065    }
1066
1067    #[test]
1068    fn test_default_json_works_across_all_imports() -> Result<()> {
1069        let fm_i = get_feature_manifest(
1070            vec![],
1071            vec![],
1072            vec![FeatureDef {
1073                name: "feature_i".into(),
1074                props: vec![PropDef::new(
1075                    "prop_i_1",
1076                    &TypeRef::String,
1077                    &json!("prop_i_1_value"),
1078                )],
1079                ..Default::default()
1080            }],
1081            BTreeMap::new(),
1082        );
1083
1084        let fm = get_feature_manifest(
1085            vec![],
1086            vec![],
1087            vec![FeatureDef {
1088                name: "feature".into(),
1089                props: vec![PropDef::new(
1090                    "prop_1",
1091                    &TypeRef::String,
1092                    &json!("prop_1_value"),
1093                )],
1094                ..Default::default()
1095            }],
1096            BTreeMap::from([(ModuleId::Local("test".into()), fm_i)]),
1097        );
1098
1099        let json = fm.default_json();
1100        assert_eq!(
1101            json.get("feature_i").unwrap().get("prop_i_1").unwrap(),
1102            &json!("prop_i_1_value")
1103        );
1104        assert_eq!(
1105            json.get("feature").unwrap().get("prop_1").unwrap(),
1106            &json!("prop_1_value")
1107        );
1108
1109        Ok(())
1110    }
1111}
1112
1113#[cfg(test)]
1114mod feature_config_tests {
1115    use serde_json::json;
1116
1117    use super::*;
1118    use crate::fixtures::intermediate_representation::get_feature_manifest;
1119
1120    #[test]
1121    fn test_validate_feature_config_success() -> Result<()> {
1122        let fm = get_feature_manifest(
1123            vec![],
1124            vec![],
1125            vec![FeatureDef {
1126                name: "feature".into(),
1127                props: vec![PropDef::new(
1128                    "prop_1",
1129                    &TypeRef::String,
1130                    &json!("prop_1_value"),
1131                )],
1132                ..Default::default()
1133            }],
1134            BTreeMap::new(),
1135        );
1136
1137        let result = fm.validate_feature_config("feature", json!({ "prop_1": "new value" }))?;
1138        assert_eq!(result.props[0].default, json!("new value"));
1139
1140        Ok(())
1141    }
1142
1143    #[test]
1144    fn test_validate_feature_config_invalid_feature_name() -> Result<()> {
1145        let fm = get_feature_manifest(
1146            vec![],
1147            vec![],
1148            vec![FeatureDef {
1149                name: "feature".into(),
1150                props: vec![PropDef::new(
1151                    "prop_1",
1152                    &TypeRef::String,
1153                    &json!("prop_1_value"),
1154                )],
1155                ..Default::default()
1156            }],
1157            BTreeMap::new(),
1158        );
1159
1160        let result = fm.validate_feature_config("feature-1", json!({ "prop_1": "new value" }));
1161        assert!(result.is_err());
1162        assert_eq!(
1163            result.err().unwrap().to_string(),
1164            "Feature `feature-1` not found on manifest".to_string()
1165        );
1166
1167        Ok(())
1168    }
1169
1170    #[test]
1171    fn test_validate_feature_config_invalid_feature_prop_name() -> Result<()> {
1172        let fm = get_feature_manifest(
1173            vec![],
1174            vec![],
1175            vec![FeatureDef {
1176                name: "feature".into(),
1177                props: vec![PropDef::new(
1178                    "prop_1",
1179                    &TypeRef::Option(Box::new(TypeRef::String)),
1180                    &Value::Null,
1181                )],
1182                ..Default::default()
1183            }],
1184            BTreeMap::new(),
1185        );
1186
1187        let result = fm.validate_feature_config("feature", json!({"prop": "new value"}));
1188        assert!(result.is_err());
1189        assert_eq!(
1190            result.err().unwrap().to_string(),
1191            "Validation Error at features/feature: Invalid property \"prop\"; did you mean \"prop_1\"?"
1192        );
1193
1194        Ok(())
1195    }
1196
1197    #[test]
1198    fn test_validate_feature_config_invalid_feature_prop_value() -> Result<()> {
1199        let fm = get_feature_manifest(
1200            vec![],
1201            vec![],
1202            vec![FeatureDef {
1203                name: "feature".into(),
1204                props: vec![PropDef::new(
1205                    "prop_1",
1206                    &TypeRef::String,
1207                    &json!("prop_1_value"),
1208                )],
1209                ..Default::default()
1210            }],
1211            BTreeMap::new(),
1212        );
1213
1214        let result = fm.validate_feature_config(
1215            "feature",
1216            json!({
1217                "prop_1": 1,
1218            }),
1219        );
1220        assert!(result.is_err());
1221        assert_eq!(
1222            result.err().unwrap().to_string(),
1223            "Validation Error at features/feature.prop_1: Invalid value 1 for type String"
1224                .to_string()
1225        );
1226
1227        Ok(())
1228    }
1229
1230    #[test]
1231    fn test_validate_feature_config_errors_on_invalid_object_prop() -> Result<()> {
1232        let obj_defs = vec![ObjectDef::new(
1233            "SampleObj",
1234            &[PropDef::new("string", &TypeRef::String, &json!("a string"))],
1235        )];
1236        let fm = get_feature_manifest(
1237            obj_defs,
1238            vec![],
1239            vec![FeatureDef {
1240                name: "feature".into(),
1241                props: vec![PropDef::new(
1242                    "prop_1",
1243                    &TypeRef::Object("SampleObj".into()),
1244                    &json!({
1245                        "string": "a value"
1246                    }),
1247                )],
1248                ..Default::default()
1249            }],
1250            BTreeMap::new(),
1251        );
1252
1253        let result = fm.validate_feature_config(
1254            "feature",
1255            json!({
1256                "prop_1": {
1257                    "invalid-prop": "invalid-prop value"
1258                }
1259            }),
1260        );
1261
1262        assert!(result.is_err());
1263        assert_eq!(
1264            result.err().unwrap().to_string(),
1265            "Validation Error at features/feature.prop_1#SampleObj: Invalid property \"invalid-prop\"; did you mean \"string\"?"
1266        );
1267
1268        Ok(())
1269    }
1270}