nimbus_fml/
parser.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, BTreeSet, HashMap, HashSet};
6
7use serde_json::Value;
8
9use crate::{
10    defaults::DefaultsMerger,
11    error::{FMLError, Result},
12    frontend::{
13        ExampleBlock, FeatureAdditionChoices, FeatureAdditions, ImportBlock, InlineExampleBlock,
14        ManifestFrontEnd, PartialExampleBlock, PathOnly, Types,
15    },
16    intermediate_representation::{FeatureManifest, ModuleId, TypeRef},
17    util::loaders::{FileLoader, FilePath},
18};
19
20fn parse_typeref_string(input: String) -> Result<(String, Option<String>)> {
21    // Split the string into the TypeRef and the name
22    let mut object_type_iter = input.split(&['<', '>'][..]);
23
24    // This should be the TypeRef type (except for )
25    let type_ref_name = object_type_iter.next().unwrap().trim();
26
27    if ["String", "Int", "Boolean"].contains(&type_ref_name) {
28        return Ok((type_ref_name.to_string(), None));
29    }
30
31    // This should be the name or type of the Object
32    match object_type_iter.next() {
33        Some(object_type_name) => Ok((
34            type_ref_name.to_string(),
35            Some(object_type_name.to_string()),
36        )),
37        None => Ok((type_ref_name.to_string(), None)),
38    }
39}
40
41pub(crate) fn get_typeref_from_string(
42    input: String,
43    types: &HashMap<String, TypeRef>,
44) -> Result<TypeRef, FMLError> {
45    let (type_ref, type_name) = parse_typeref_string(input)?;
46
47    Ok(match type_ref.as_str() {
48        "String" => TypeRef::String,
49        "Int" => TypeRef::Int,
50        "Boolean" => TypeRef::Boolean,
51        "BundleText" | "Text" => TypeRef::BundleText,
52        "BundleImage" | "Drawable" | "Image" => TypeRef::BundleImage,
53        "Enum" => TypeRef::Enum(type_name.unwrap()),
54        "Object" => TypeRef::Object(type_name.unwrap()),
55        "List" => TypeRef::List(Box::new(get_typeref_from_string(
56            type_name.unwrap(),
57            types,
58        )?)),
59        "Option" => TypeRef::Option(Box::new(get_typeref_from_string(
60            type_name.unwrap(),
61            types,
62        )?)),
63        "Map" => {
64            // Maps take a little extra massaging to get the key and value types
65            let type_name = type_name.unwrap();
66            let mut map_type_info_iter = type_name.split(',');
67
68            let key_type = map_type_info_iter.next().unwrap().to_string();
69            let value_type = map_type_info_iter.next().unwrap().trim().to_string();
70
71            if key_type.eq("String") {
72                TypeRef::StringMap(Box::new(get_typeref_from_string(value_type, types)?))
73            } else {
74                TypeRef::EnumMap(
75                    Box::new(get_typeref_from_string(key_type, types)?),
76                    Box::new(get_typeref_from_string(value_type, types)?),
77                )
78            }
79        }
80        type_name => types.get(type_name).cloned().ok_or_else(|| {
81            FMLError::TypeParsingError(format!("{type_name} is not a recognized FML type"))
82        })?,
83    })
84}
85
86#[derive(Debug)]
87pub struct Parser {
88    files: FileLoader,
89    source: FilePath,
90}
91
92impl Parser {
93    pub fn new(files: FileLoader, source: FilePath) -> Result<Parser> {
94        Ok(Parser { source, files })
95    }
96
97    pub fn load_frontend(files: FileLoader, source: &str) -> Result<ManifestFrontEnd> {
98        let source = files.file_path(source)?;
99        let parser: Parser = Parser::new(files, source)?;
100        let mut loading = HashSet::new();
101        parser.load_manifest(&parser.source, &mut loading)
102    }
103
104    // This method loads a manifest, including resolving the includes and merging the included files
105    // into this top level one.
106    // It recursively calls itself and then calls `merge_manifest`.
107    pub fn load_manifest(
108        &self,
109        path: &FilePath,
110        loading: &mut HashSet<ModuleId>,
111    ) -> Result<ManifestFrontEnd> {
112        let id: ModuleId = path.try_into()?;
113        let files = &self.files;
114
115        let mut parent = files
116            .read::<ManifestFrontEnd>(path)
117            .map_err(|e| FMLError::FMLModuleError(id.clone(), e.to_string()))?;
118
119        // We canonicalize the paths to the import files really soon after the loading so when we merge
120        // other included files, we cam match up the files that _they_ import, the concatenate the default
121        // blocks for their features.
122        self.canonicalize_import_paths(path, &mut parent.imports)
123            .map_err(|e| FMLError::FMLModuleError(id.clone(), e.to_string()))?;
124
125        self.inline_manifest_resources(path, &mut parent)?;
126
127        loading.insert(id.clone());
128        parent
129            .includes()
130            .iter()
131            .try_fold(parent, |parent: ManifestFrontEnd, f| {
132                let src_path = files.join(path, f)?;
133                let child_id = ModuleId::try_from(&src_path)?;
134                Ok(if !loading.contains(&child_id) {
135                    let manifest = self.load_manifest(&src_path, loading)?;
136                    self.merge_manifest(&src_path, parent, &src_path, manifest)
137                        .map_err(|e| FMLError::FMLModuleError(id.clone(), e.to_string()))?
138                } else {
139                    parent
140                })
141            })
142    }
143
144    // Attempts to merge two manifests: a child into a parent.
145    // The `child_path` is needed to report errors.
146    fn merge_manifest(
147        &self,
148        parent_path: &FilePath,
149        parent: ManifestFrontEnd,
150        child_path: &FilePath,
151        child: ManifestFrontEnd,
152    ) -> Result<ManifestFrontEnd> {
153        self.check_can_merge_manifest(parent_path, &parent, child_path, &child)?;
154
155        // Child must not specify any features, objects or enums that the parent has.
156        let features = merge_map(
157            &parent.features,
158            &child.features,
159            "Features",
160            "features",
161            child_path,
162        )?;
163
164        let p_types = &parent.legacy_types.unwrap_or(parent.types);
165        let c_types = &child.legacy_types.unwrap_or(child.types);
166
167        let objects = merge_map(
168            &c_types.objects,
169            &p_types.objects,
170            "Objects",
171            "objects",
172            child_path,
173        )?;
174        let enums = merge_map(&c_types.enums, &p_types.enums, "Enums", "enums", child_path)?;
175
176        let imports = self.merge_import_block_list(&parent.imports, &child.imports)?;
177
178        let merged = ManifestFrontEnd {
179            features,
180            types: Types { enums, objects },
181            legacy_types: None,
182            imports,
183            ..parent
184        };
185
186        Ok(merged)
187    }
188
189    fn inline_manifest_resources(
190        &self,
191        path: &FilePath,
192        manifest: &mut ManifestFrontEnd,
193    ) -> Result<()> {
194        for feature in manifest.features.values_mut() {
195            let as_typed = &feature.examples;
196            let mut inlined = Vec::with_capacity(as_typed.len());
197            for example in as_typed {
198                inlined.push(example.inline(&self.files, path)?);
199            }
200            feature.examples = inlined;
201        }
202
203        for import in &mut manifest.imports {
204            let mut features: BTreeMap<String, FeatureAdditionChoices> = Default::default();
205            for (feature_id, additions) in &import.features {
206                let additions: FeatureAdditions = additions.clone().into();
207                features.insert(
208                    feature_id.clone(),
209                    additions.inline(&self.files, path)?.into(),
210                );
211            }
212            import.features = features;
213        }
214
215        Ok(())
216    }
217
218    /// Load a manifest and all its imports, recursively if necessary.
219    ///
220    /// We populate a map of `FileId` to `FeatureManifest`s, so to avoid unnecessary clones,
221    /// we return a `FileId` even when the file has already been imported.
222    fn load_imports(
223        &self,
224        current: &FilePath,
225        channel: Option<&str>,
226        imports: &mut BTreeMap<ModuleId, FeatureManifest>,
227        // includes: &mut HashSet<ModuleId>,
228    ) -> Result<ModuleId> {
229        let id = current.try_into()?;
230        if imports.contains_key(&id) {
231            return Ok(id);
232        }
233        // We put a terminus in here, to make sure we don't try and load more than once.
234        imports.insert(id.clone(), Default::default());
235
236        // This loads the manifest in its frontend format (i.e. direct from YAML via serde), including
237        // all the `includes` for this manifest.
238        let frontend = self.load_manifest(current, &mut HashSet::new())?;
239
240        // Aside: tiny quality of life improvement. In the case where only one channel is supported,
241        // we use it. This helps with globbing directories where the app wants to keep the feature definition
242        // away from the feature configuration.
243        let channel = if frontend.channels.len() == 1 {
244            frontend.channels.first().map(String::as_str)
245        } else {
246            channel
247        };
248
249        let mut manifest = frontend.get_intermediate_representation(&id, channel)?;
250
251        // We're now going to go through all the imports in the manifest YAML.
252        // Each of the import blocks will have a path, and a Map<FeatureId, List<DefaultBlock>>
253        // This loop does the work of merging the default blocks back into the imported manifests.
254        // We'll then attach all the manifests to the root (i.e. the one we're generating code for today), in `imports`.
255        // We associate only the feature ids with the manifest we're loading in this method.
256        let mut imported_feature_id_map = BTreeMap::new();
257
258        for block in &frontend.imports {
259            // 1. Load the imported manifests in to the hash map.
260            let path = self.files.join(current, &block.path)?;
261            // The channel comes from the importer, rather than the command or the imported file.
262            let child_id = self.load_imports(&path, Some(&block.channel), imports)?;
263            let child_manifest = imports.get_mut(&child_id).expect("just loaded this file");
264
265            // We detect that there are no name collisions after the loading has finished, with `check_can_import_manifest`.
266            // We can't do it greedily, because of transitive imports may cause collisions, but we'll check here for better error
267            // messages.
268            check_can_import_manifest(&manifest, child_manifest)?;
269
270            // We detect that the imported files have language specific files in `validate_manifest_for_lang()`.
271            // We can't do it now because we don't yet know what this run is going to generate.
272
273            // 2. We'll build a set of feature names that this manifest imports from the child manifest.
274            // This will be the only thing we add directly to the manifest we load in this method.
275            let mut feature_ids = BTreeSet::new();
276
277            // 3. For each of the features in each of the imported files, the user can specify new defaults that should
278            //    merge into/overwrite the defaults specified in the imported file. Let's do that now:
279            // a. Prepare a DefaultsMerger, with an object map.
280            let merger = DefaultsMerger::new(
281                &child_manifest.obj_defs,
282                frontend.channels.clone(),
283                channel.map(str::to_string),
284            );
285
286            // b. Prepare a feature map that we'll alter in place.
287            //    EXP- 2540 If we want to support re-exporting/encapsulating features then we will need to change
288            //    this to be a more recursive look up. e.g. change `FeatureManifest.feature_defs` to be a `BTreeMap`.
289            let feature_map = &mut child_manifest.feature_defs;
290
291            // c. Iterate over the features we want to add to the original feature:
292            //    - by adding to the list of examples.
293            //    - by overriding default values.
294            for (f, feature_additions) in &block.features {
295                let feature_def = feature_map.get_mut(f).ok_or_else(|| {
296                    FMLError::FMLModuleError(
297                        id.clone(),
298                        format!("Cannot override defaults for `{f}` feature from {child_id}"),
299                    )
300                })?;
301                // FeatureAdditions holds the extra examples and defaults for this feature.
302                let additions: FeatureAdditions = feature_additions.clone().into();
303
304                // d.i) Append the imported list of examples to the original feature examples and…
305                feature_def
306                    .examples
307                    .extend(additions.examples.iter().map(Into::into));
308
309                // d.ii) Merge the overrides in place into the FeatureDefs
310                merger
311                    .merge_feature_defaults(feature_def, &Some(additions.defaults))
312                    .map_err(|e| FMLError::FMLModuleError(child_id.clone(), e.to_string()))?;
313
314                feature_ids.insert(f.clone());
315            }
316
317            // 4. Associate the imports as children of this manifest.
318            imported_feature_id_map.insert(child_id.clone(), feature_ids);
319        }
320
321        manifest.imported_features = imported_feature_id_map;
322        imports.insert(id.clone(), manifest);
323
324        Ok(id)
325    }
326
327    pub fn get_intermediate_representation(
328        &self,
329        channel: Option<&str>,
330    ) -> Result<FeatureManifest, FMLError> {
331        let mut manifests = BTreeMap::new();
332        let id = self.load_imports(&self.source, channel, &mut manifests)?;
333        let mut fm = manifests
334            .remove(&id)
335            .expect("Top level manifest should always be present");
336
337        for child in manifests.values() {
338            check_can_import_manifest(&fm, child)?;
339        }
340
341        fm.all_imports = manifests;
342
343        Ok(fm)
344    }
345}
346
347impl Parser {
348    fn check_can_merge_manifest(
349        &self,
350        parent_path: &FilePath,
351        parent: &ManifestFrontEnd,
352        child_path: &FilePath,
353        child: &ManifestFrontEnd,
354    ) -> Result<()> {
355        if !child.channels.is_empty() {
356            let child = &child.channels;
357            let child = child.iter().collect::<HashSet<&String>>();
358            let parent = &parent.channels;
359            let parent = parent.iter().collect::<HashSet<&String>>();
360            if !child.is_subset(&parent) {
361                return Err(FMLError::ValidationError(
362                    "channels".to_string(),
363                    format!(
364                        "Included manifest should not define its own channels: {}",
365                        child_path
366                    ),
367                ));
368            }
369        }
370
371        if let Some(about) = &child.about {
372            if !about.is_includable() {
373                return Err(FMLError::ValidationError(
374                "about".to_string(),
375                format!("Only files that don't already correspond to generated files may be included: file has a `class` and `package`/`module` name: {}", child_path),
376            ));
377            }
378        }
379
380        let mut map = Default::default();
381        self.check_can_merge_imports(parent_path, &parent.imports, &mut map)?;
382        self.check_can_merge_imports(child_path, &child.imports, &mut map)?;
383
384        Ok(())
385    }
386
387    fn canonicalize_import_paths(
388        &self,
389        path: &FilePath,
390        blocks: &mut Vec<ImportBlock>,
391    ) -> Result<()> {
392        for ib in blocks {
393            let p = &self.files.join(path, &ib.path)?;
394            ib.path = p.canonicalize()?.to_string();
395        }
396        Ok(())
397    }
398
399    fn check_can_merge_imports(
400        &self,
401        path: &FilePath,
402        blocks: &Vec<ImportBlock>,
403        map: &mut HashMap<String, String>,
404    ) -> Result<()> {
405        for b in blocks {
406            let id = &b.path;
407            let channel = &b.channel;
408            let existing = map.insert(id.clone(), channel.clone());
409            if let Some(v) = existing {
410                if &v != channel {
411                    return Err(FMLError::FMLModuleError(
412                        path.try_into()?,
413                        format!(
414                            "File {} is imported with two different channels: {} and {}",
415                            id, v, &channel
416                        ),
417                    ));
418                }
419            }
420        }
421        Ok(())
422    }
423
424    fn merge_import_block_list(
425        &self,
426        parent: &[ImportBlock],
427        child: &[ImportBlock],
428    ) -> Result<Vec<ImportBlock>> {
429        let mut map = parent
430            .iter()
431            .map(|im| (im.path.clone(), im.clone()))
432            .collect::<HashMap<_, _>>();
433
434        for cib in child {
435            let path = &cib.path;
436            if let Some(pib) = map.get(path) {
437                // We'll define an ordering here: the parent will come after the child
438                // so the top-level one will override the lower level ones.
439                // In practice, this shouldn't make a difference.
440                let merged = merge_import_block(cib, pib)?;
441                map.insert(path.clone(), merged);
442            } else {
443                map.insert(path.clone(), cib.clone());
444            }
445        }
446
447        Ok(map.values().map(|b| b.to_owned()).collect::<Vec<_>>())
448    }
449}
450
451fn merge_map<T: Clone>(
452    a: &BTreeMap<String, T>,
453    b: &BTreeMap<String, T>,
454    display_key: &str,
455    key: &str,
456    child_path: &FilePath,
457) -> Result<BTreeMap<String, T>> {
458    let mut set = HashSet::new();
459
460    let (a, b) = if a.len() < b.len() { (a, b) } else { (b, a) };
461
462    let mut map = b.clone();
463
464    for (k, v) in a {
465        if map.contains_key(k) {
466            set.insert(k.clone());
467        } else {
468            map.insert(k.clone(), v.clone());
469        }
470    }
471
472    if set.is_empty() {
473        Ok(map)
474    } else {
475        Err(FMLError::ValidationError(
476            format!("{}/{:?}", key, set),
477            format!(
478                "{} cannot be defined twice, overloaded definition detected at {}",
479                display_key, child_path,
480            ),
481        ))
482    }
483}
484
485fn merge_import_block(a: &ImportBlock, b: &ImportBlock) -> Result<ImportBlock> {
486    let mut block = a.clone();
487
488    for (id, additions) in &b.features {
489        block
490            .features
491            .entry(id.clone())
492            .and_modify(|existing| existing.merge(additions))
493            .or_insert(additions.clone());
494    }
495    Ok(block)
496}
497
498/// Check if this parent can import this child.
499fn check_can_import_manifest(parent: &FeatureManifest, child: &FeatureManifest) -> Result<()> {
500    check_can_import_list(parent, child, "enum", |fm: &FeatureManifest| {
501        fm.enum_defs.keys().collect()
502    })?;
503    check_can_import_list(parent, child, "objects", |fm: &FeatureManifest| {
504        fm.obj_defs.keys().collect()
505    })?;
506    check_can_import_list(parent, child, "features", |fm: &FeatureManifest| {
507        fm.feature_defs.keys().collect()
508    })?;
509
510    Ok(())
511}
512
513fn check_can_import_list(
514    parent: &FeatureManifest,
515    child: &FeatureManifest,
516    key: &str,
517    f: fn(&FeatureManifest) -> HashSet<&String>,
518) -> Result<()> {
519    let p = f(parent);
520    let c = f(child);
521    let intersection = p.intersection(&c).collect::<HashSet<_>>();
522    if !intersection.is_empty() {
523        Err(FMLError::ValidationError(
524            key.to_string(),
525            format!(
526                "`{}` types {:?} conflict when {} imports {}",
527                key, &intersection, &parent.id, &child.id
528            ),
529        ))
530    } else {
531        Ok(())
532    }
533}
534
535impl ExampleBlock {
536    fn inline(&self, files: &FileLoader, root: &FilePath) -> Result<Self> {
537        Ok(match self {
538            Self::Inline(_) => self.clone(),
539            Self::Partial(PartialExampleBlock { metadata, path }) => {
540                let file = files.join(root, path)?;
541                let value: Value = files.read(&file)?;
542                Self::Inline(InlineExampleBlock {
543                    metadata: metadata.to_owned(),
544                    value,
545                })
546            }
547            Self::BarePath(path) | Self::Path(PathOnly { path }) => {
548                let file = files.join(root, path)?;
549                let value: InlineExampleBlock = files.read(&file)?;
550                Self::Inline(value)
551            }
552        })
553    }
554}
555
556impl FeatureAdditionChoices {
557    fn merge(&mut self, other: &Self) {
558        match (self, other) {
559            (Self::FeatureAdditions(a), Self::FeatureAdditions(b)) => a.merge(b),
560            _ => unreachable!("FeatureAdditionChoices should have been rationalized already. This is a bug in nimbus-fml"),
561        };
562    }
563}
564
565impl FeatureAdditions {
566    fn inline(&self, files: &FileLoader, root: &FilePath) -> Result<Self> {
567        let examples = self
568            .examples
569            .iter()
570            .map(|ex| ex.inline(files, root))
571            .collect::<Result<_>>()?;
572        Ok(Self {
573            examples,
574            defaults: self.defaults.clone(),
575        })
576    }
577
578    fn merge(&mut self, other: &Self) {
579        self.examples.extend(other.examples.clone());
580        self.defaults.extend(other.defaults.clone());
581    }
582}
583
584#[cfg(test)]
585mod unit_tests {
586
587    use std::{
588        path::{Path, PathBuf},
589        vec,
590    };
591
592    use serde_json::json;
593
594    use super::*;
595    use crate::{
596        error::Result,
597        frontend::ImportBlock,
598        intermediate_representation::{PropDef, VariantDef},
599        util::{join, pkg_dir},
600    };
601
602    #[test]
603    fn test_parse_from_front_end_representation() -> Result<()> {
604        let path = join(pkg_dir(), "fixtures/fe/nimbus_features.yaml");
605        let path = Path::new(&path);
606        let files = FileLoader::default()?;
607        let parser = Parser::new(files, path.into())?;
608        let ir = parser.get_intermediate_representation(Some("release"))?;
609
610        // Validate parsed enums
611        assert!(ir.enum_defs.len() == 1);
612        let enum_def = &ir.enum_defs["PlayerProfile"];
613        assert!(enum_def.name == *"PlayerProfile");
614        assert!(enum_def.doc == *"This is an enum type");
615        assert!(enum_def.variants.contains(&VariantDef {
616            name: "adult".to_string(),
617            doc: "This represents an adult player profile".to_string()
618        }));
619        assert!(enum_def.variants.contains(&VariantDef {
620            name: "child".to_string(),
621            doc: "This represents a child player profile".to_string()
622        }));
623
624        // Validate parsed objects
625        assert!(ir.obj_defs.len() == 1);
626        let obj_def = &ir.obj_defs["Button"];
627        assert!(obj_def.name == *"Button");
628        assert!(obj_def.doc == *"This is a button object");
629        assert!(obj_def.props.contains(&PropDef::with_doc(
630            "label",
631            "This is the label for the button",
632            &TypeRef::String,
633            &serde_json::json!("REQUIRED FIELD")
634        )));
635        assert!(obj_def.props.contains(&PropDef::with_doc(
636            "color",
637            "This is the color of the button",
638            &TypeRef::Option(Box::new(TypeRef::String)),
639            &serde_json::Value::Null
640        )));
641
642        // Validate parsed features
643        assert!(ir.feature_defs.len() == 1);
644        let feature_def = ir.get_feature("dialog-appearance").unwrap();
645        assert!(feature_def.name == *"dialog-appearance");
646        assert!(feature_def.doc() == *"This is the appearance of the dialog");
647        let positive_button = feature_def
648            .props
649            .iter()
650            .find(|x| x.name == "positive")
651            .unwrap();
652        assert!(positive_button.name == *"positive");
653        assert!(positive_button.doc == *"This is a positive button");
654        assert!(positive_button.typ == TypeRef::Object("Button".to_string()));
655        // We verify that the label, which came from the field default is "Ok then"
656        // and the color default, which came from the feature default is "green"
657        assert!(positive_button.default.get("label").unwrap().as_str() == Some("Ok then"));
658        assert!(positive_button.default.get("color").unwrap().as_str() == Some("green"));
659        let negative_button = feature_def
660            .props
661            .iter()
662            .find(|x| x.name == "negative")
663            .unwrap();
664        assert!(negative_button.name == *"negative");
665        assert!(negative_button.doc == *"This is a negative button");
666        assert!(negative_button.typ == TypeRef::Object("Button".to_string()));
667        assert!(negative_button.default.get("label").unwrap().as_str() == Some("Not this time"));
668        assert!(negative_button.default.get("color").unwrap().as_str() == Some("red"));
669        let background_color = feature_def
670            .props
671            .iter()
672            .find(|x| x.name == "background-color")
673            .unwrap();
674        assert!(background_color.name == *"background-color");
675        assert!(background_color.doc == *"This is the background color");
676        assert!(background_color.typ == TypeRef::String);
677        assert!(background_color.default.as_str() == Some("white"));
678        let player_mapping = feature_def
679            .props
680            .iter()
681            .find(|x| x.name == "player-mapping")
682            .unwrap();
683        assert!(player_mapping.name == *"player-mapping");
684        assert!(player_mapping.doc == *"This is the map of the player type to a button");
685        assert!(
686            player_mapping.typ
687                == TypeRef::EnumMap(
688                    Box::new(TypeRef::Enum("PlayerProfile".to_string())),
689                    Box::new(TypeRef::Object("Button".to_string()))
690                )
691        );
692        assert!(
693            player_mapping.default
694                == json!({
695                    "child": {
696                        "label": "Play game!",
697                        "color": "green"
698                    },
699                    "adult": {
700                        "label": "Play game!",
701                        "color": "blue",
702                    }
703                })
704        );
705
706        Ok(())
707    }
708
709    #[test]
710    fn test_merging_defaults() -> Result<()> {
711        let path = join(pkg_dir(), "fixtures/fe/default_merging.yaml");
712        let path = Path::new(&path);
713        let files = FileLoader::default()?;
714        let parser = Parser::new(files, path.into())?;
715        let ir = parser.get_intermediate_representation(Some("release"))?;
716        let feature_def = ir.get_feature("dialog-appearance").unwrap();
717        let positive_button = feature_def
718            .props
719            .iter()
720            .find(|x| x.name == "positive")
721            .unwrap();
722        // We validate that the no-channel feature level default got merged back
723        assert_eq!(
724            positive_button
725                .default
726                .get("alt-text")
727                .unwrap()
728                .as_str()
729                .unwrap(),
730            "Go Ahead!"
731        );
732        // We validate that the original field level default don't get lost if no
733        // feature level default with the same name exists
734        assert_eq!(
735            positive_button
736                .default
737                .get("label")
738                .unwrap()
739                .as_str()
740                .unwrap(),
741            "Ok then"
742        );
743        // We validate that feature level default overwrite field level defaults if one exists
744        // in the field level, it's blue, but on the feature level it's green
745        assert_eq!(
746            positive_button
747                .default
748                .get("color")
749                .unwrap()
750                .as_str()
751                .unwrap(),
752            "green"
753        );
754        // We now re-run this, but merge back the nightly channel instead
755        let files = FileLoader::default()?;
756        let parser = Parser::new(files, path.into())?;
757        let ir = parser.get_intermediate_representation(Some("nightly"))?;
758        let feature_def = ir.get_feature("dialog-appearance").unwrap();
759        let positive_button = feature_def
760            .props
761            .iter()
762            .find(|x| x.name == "positive")
763            .unwrap();
764        // We validate that feature level default overwrite field level defaults if one exists
765        // in the field level, it's blue, but on the feature level it's bright-red
766        // note that it's bright-red because we merged back the `nightly`
767        // channel, instead of the `release` channel that merges back
768        // by default
769        assert_eq!(
770            positive_button
771                .default
772                .get("color")
773                .unwrap()
774                .as_str()
775                .unwrap(),
776            "bright-red"
777        );
778        // We against validate that regardless
779        // of the channel, the no-channel feature level default got merged back
780        assert_eq!(
781            positive_button
782                .default
783                .get("alt-text")
784                .unwrap()
785                .as_str()
786                .unwrap(),
787            "Go Ahead!"
788        );
789        Ok(())
790    }
791
792    #[test]
793    fn test_convert_to_typeref_string() -> Result<()> {
794        // Testing converting to TypeRef::String
795        let types = Default::default();
796        assert_eq!(
797            get_typeref_from_string("String".to_string(), &types).unwrap(),
798            TypeRef::String
799        );
800        get_typeref_from_string("string".to_string(), &types).unwrap_err();
801        get_typeref_from_string("str".to_string(), &types).unwrap_err();
802
803        Ok(())
804    }
805
806    #[test]
807    fn test_convert_to_typeref_int() -> Result<()> {
808        // Testing converting to TypeRef::Int
809        let types = Default::default();
810        assert_eq!(
811            get_typeref_from_string("Int".to_string(), &types).unwrap(),
812            TypeRef::Int
813        );
814        get_typeref_from_string("integer".to_string(), &types).unwrap_err();
815        get_typeref_from_string("int".to_string(), &types).unwrap_err();
816
817        Ok(())
818    }
819
820    #[test]
821    fn test_convert_to_typeref_boolean() -> Result<()> {
822        // Testing converting to TypeRef::Boolean
823        let types = Default::default();
824        assert_eq!(
825            get_typeref_from_string("Boolean".to_string(), &types).unwrap(),
826            TypeRef::Boolean
827        );
828        get_typeref_from_string("boolean".to_string(), &types).unwrap_err();
829        get_typeref_from_string("bool".to_string(), &types).unwrap_err();
830
831        Ok(())
832    }
833
834    #[test]
835    fn test_convert_to_typeref_bundletext() -> Result<()> {
836        // Testing converting to TypeRef::BundleText
837        let types = Default::default();
838        get_typeref_from_string("bundletext(something)".to_string(), &types).unwrap_err();
839        get_typeref_from_string("BundleText()".to_string(), &types).unwrap_err();
840
841        // The commented out lines below represent areas we need better
842        // type checking on, but are ignored for now
843
844        // get_typeref_from_string("BundleText".to_string()).unwrap_err();
845        // get_typeref_from_string("BundleText<>".to_string()).unwrap_err();
846        // get_typeref_from_string("BundleText<21>".to_string()).unwrap_err();
847
848        Ok(())
849    }
850
851    #[test]
852    fn test_convert_to_typeref_bundleimage() -> Result<()> {
853        // Testing converting to TypeRef::BundleImage
854        let types = Default::default();
855        assert_eq!(
856            get_typeref_from_string("BundleImage<test_name>".to_string(), &types).unwrap(),
857            TypeRef::BundleImage
858        );
859        get_typeref_from_string("bundleimage(something)".to_string(), &types).unwrap_err();
860        get_typeref_from_string("BundleImage()".to_string(), &types).unwrap_err();
861
862        // The commented out lines below represent areas we need better
863        // type checking on, but are ignored for now
864
865        // get_typeref_from_string("BundleImage".to_string()).unwrap_err();
866        // get_typeref_from_string("BundleImage<>".to_string()).unwrap_err();
867        // get_typeref_from_string("BundleImage<21>".to_string()).unwrap_err();
868
869        Ok(())
870    }
871
872    #[test]
873    fn test_convert_to_typeref_enum() -> Result<()> {
874        // Testing converting to TypeRef::Enum
875        let types = Default::default();
876        assert_eq!(
877            get_typeref_from_string("Enum<test_name>".to_string(), &types).unwrap(),
878            TypeRef::Enum("test_name".to_string())
879        );
880        get_typeref_from_string("enum(something)".to_string(), &types).unwrap_err();
881        get_typeref_from_string("Enum()".to_string(), &types).unwrap_err();
882
883        // The commented out lines below represent areas we need better
884        // type checking on, but are ignored for now
885
886        // get_typeref_from_string("Enum".to_string()).unwrap_err();
887        // get_typeref_from_string("Enum<>".to_string()).unwrap_err();
888        // get_typeref_from_string("Enum<21>".to_string()).unwrap_err();
889
890        Ok(())
891    }
892
893    #[test]
894    fn test_convert_to_typeref_object() -> Result<()> {
895        // Testing converting to TypeRef::Object
896        let types = Default::default();
897        assert_eq!(
898            get_typeref_from_string("Object<test_name>".to_string(), &types).unwrap(),
899            TypeRef::Object("test_name".to_string())
900        );
901        get_typeref_from_string("object(something)".to_string(), &types).unwrap_err();
902        get_typeref_from_string("Object()".to_string(), &types).unwrap_err();
903
904        // The commented out lines below represent areas we need better
905        // type checking on, but are ignored for now
906
907        // get_typeref_from_string("Object".to_string()).unwrap_err();
908        // get_typeref_from_string("Object<>".to_string()).unwrap_err();
909        // get_typeref_from_string("Object<21>".to_string()).unwrap_err();
910
911        Ok(())
912    }
913
914    #[test]
915    fn test_convert_to_typeref_list() -> Result<()> {
916        // Testing converting to TypeRef::List
917        let types = Default::default();
918        assert_eq!(
919            get_typeref_from_string("List<String>".to_string(), &types).unwrap(),
920            TypeRef::List(Box::new(TypeRef::String))
921        );
922        assert_eq!(
923            get_typeref_from_string("List<Int>".to_string(), &types).unwrap(),
924            TypeRef::List(Box::new(TypeRef::Int))
925        );
926        assert_eq!(
927            get_typeref_from_string("List<Boolean>".to_string(), &types).unwrap(),
928            TypeRef::List(Box::new(TypeRef::Boolean))
929        );
930
931        // Generate a list of user types to validate use of them in a list
932        let mut types: HashMap<_, _> = Default::default();
933        types.insert(
934            "TestEnum".to_string(),
935            TypeRef::Enum("TestEnum".to_string()),
936        );
937        types.insert(
938            "TestObject".to_string(),
939            TypeRef::Object("TestObject".to_string()),
940        );
941
942        assert_eq!(
943            get_typeref_from_string("List<TestEnum>".to_string(), &types).unwrap(),
944            TypeRef::List(Box::new(TypeRef::Enum("TestEnum".to_string())))
945        );
946        assert_eq!(
947            get_typeref_from_string("List<TestObject>".to_string(), &types).unwrap(),
948            TypeRef::List(Box::new(TypeRef::Object("TestObject".to_string())))
949        );
950
951        get_typeref_from_string("list(something)".to_string(), &types).unwrap_err();
952        get_typeref_from_string("List()".to_string(), &types).unwrap_err();
953
954        // The commented out lines below represent areas we need better
955        // type checking on, but are ignored for now
956
957        // get_typeref_from_string("List".to_string()).unwrap_err();
958        // get_typeref_from_string("List<>".to_string()).unwrap_err();
959        // get_typeref_from_string("List<21>".to_string()).unwrap_err();
960
961        Ok(())
962    }
963
964    #[test]
965    fn test_convert_to_typeref_option() -> Result<()> {
966        // Testing converting to TypeRef::Option
967        let types = Default::default();
968        assert_eq!(
969            get_typeref_from_string("Option<String>".to_string(), &types).unwrap(),
970            TypeRef::Option(Box::new(TypeRef::String))
971        );
972        assert_eq!(
973            get_typeref_from_string("Option<Int>".to_string(), &types).unwrap(),
974            TypeRef::Option(Box::new(TypeRef::Int))
975        );
976        assert_eq!(
977            get_typeref_from_string("Option<Boolean>".to_string(), &types).unwrap(),
978            TypeRef::Option(Box::new(TypeRef::Boolean))
979        );
980
981        // Generate a list of user types to validate use of them as Options
982        let mut types = HashMap::new();
983        types.insert(
984            "TestEnum".to_string(),
985            TypeRef::Enum("TestEnum".to_string()),
986        );
987        types.insert(
988            "TestObject".to_string(),
989            TypeRef::Object("TestObject".to_string()),
990        );
991        assert_eq!(
992            get_typeref_from_string("Option<TestEnum>".to_string(), &types).unwrap(),
993            TypeRef::Option(Box::new(TypeRef::Enum("TestEnum".to_string())))
994        );
995        assert_eq!(
996            get_typeref_from_string("Option<TestObject>".to_string(), &types).unwrap(),
997            TypeRef::Option(Box::new(TypeRef::Object("TestObject".to_string())))
998        );
999
1000        get_typeref_from_string("option(something)".to_string(), &types).unwrap_err();
1001        get_typeref_from_string("Option(Something)".to_string(), &types).unwrap_err();
1002
1003        // The commented out lines below represent areas we need better
1004        // type checking on, but are ignored for now
1005
1006        // get_typeref_from_string("Option".to_string()).unwrap_err();
1007        // get_typeref_from_string("Option<>".to_string()).unwrap_err();
1008        // get_typeref_from_string("Option<21>".to_string()).unwrap_err();
1009
1010        Ok(())
1011    }
1012
1013    #[test]
1014    fn test_convert_to_typeref_map() -> Result<()> {
1015        // Testing converting to TypeRef::Map
1016        let types = Default::default();
1017        assert_eq!(
1018            get_typeref_from_string("Map<String, String>".to_string(), &types).unwrap(),
1019            TypeRef::StringMap(Box::new(TypeRef::String))
1020        );
1021        assert_eq!(
1022            get_typeref_from_string("Map<String, Int>".to_string(), &types).unwrap(),
1023            TypeRef::StringMap(Box::new(TypeRef::Int))
1024        );
1025        assert_eq!(
1026            get_typeref_from_string("Map<String, Boolean>".to_string(), &types).unwrap(),
1027            TypeRef::StringMap(Box::new(TypeRef::Boolean))
1028        );
1029
1030        // Generate a list of user types to validate use of them in a list
1031        let mut types = HashMap::new();
1032        types.insert(
1033            "TestEnum".to_string(),
1034            TypeRef::Enum("TestEnum".to_string()),
1035        );
1036        types.insert(
1037            "TestObject".to_string(),
1038            TypeRef::Object("TestObject".to_string()),
1039        );
1040        assert_eq!(
1041            get_typeref_from_string("Map<String, TestEnum>".to_string(), &types).unwrap(),
1042            TypeRef::StringMap(Box::new(TypeRef::Enum("TestEnum".to_string())))
1043        );
1044        assert_eq!(
1045            get_typeref_from_string("Map<String, TestObject>".to_string(), &types).unwrap(),
1046            TypeRef::StringMap(Box::new(TypeRef::Object("TestObject".to_string())))
1047        );
1048        assert_eq!(
1049            get_typeref_from_string("Map<TestEnum, String>".to_string(), &types).unwrap(),
1050            TypeRef::EnumMap(
1051                Box::new(TypeRef::Enum("TestEnum".to_string())),
1052                Box::new(TypeRef::String)
1053            )
1054        );
1055        assert_eq!(
1056            get_typeref_from_string("Map<TestEnum, TestObject>".to_string(), &types).unwrap(),
1057            TypeRef::EnumMap(
1058                Box::new(TypeRef::Enum("TestEnum".to_string())),
1059                Box::new(TypeRef::Object("TestObject".to_string()))
1060            )
1061        );
1062
1063        get_typeref_from_string("map(something)".to_string(), &Default::default()).unwrap_err();
1064        get_typeref_from_string("Map(Something)".to_string(), &Default::default()).unwrap_err();
1065
1066        // The commented out lines below represent areas we need better
1067        // type checking on, but are ignored for now
1068
1069        // get_typeref_from_string("Map".to_string()).unwrap_err();
1070        // get_typeref_from_string("Map<>".to_string()).unwrap_err();
1071        // get_typeref_from_string("Map<21>".to_string()).unwrap_err();
1072
1073        Ok(())
1074    }
1075
1076    #[test]
1077    fn test_include_check_can_merge_manifest() -> Result<()> {
1078        let files = FileLoader::default()?;
1079        let parser = Parser::new(files, std::env::temp_dir().as_path().into())?;
1080        let parent_path: FilePath = std::env::temp_dir().as_path().into();
1081        let child_path = parent_path.join("http://not-needed.com")?;
1082        let parent = ManifestFrontEnd {
1083            channels: vec!["alice".to_string(), "bob".to_string()],
1084            ..Default::default()
1085        };
1086        let child = ManifestFrontEnd {
1087            channels: vec!["alice".to_string(), "bob".to_string()],
1088            ..Default::default()
1089        };
1090
1091        assert!(parser
1092            .check_can_merge_manifest(&parent_path, &parent, &child_path, &child)
1093            .is_ok());
1094
1095        let child = ManifestFrontEnd {
1096            channels: vec!["eve".to_string()],
1097            ..Default::default()
1098        };
1099
1100        assert!(parser
1101            .check_can_merge_manifest(&parent_path, &parent, &child_path, &child)
1102            .is_err());
1103
1104        Ok(())
1105    }
1106
1107    #[test]
1108    fn test_include_check_can_merge_manifest_with_imports() -> Result<()> {
1109        let files = FileLoader::default()?;
1110        let parser = Parser::new(files, std::env::temp_dir().as_path().into())?;
1111        let parent_path: FilePath = std::env::temp_dir().as_path().into();
1112        let child_path = parent_path.join("http://child")?;
1113        let parent = ManifestFrontEnd {
1114            channels: vec!["alice".to_string(), "bob".to_string()],
1115            imports: vec![ImportBlock {
1116                path: "absolute_path".to_string(),
1117                channel: "one_channel".to_string(),
1118                features: Default::default(),
1119            }],
1120            ..Default::default()
1121        };
1122        let child = ManifestFrontEnd {
1123            channels: vec!["alice".to_string(), "bob".to_string()],
1124            imports: vec![ImportBlock {
1125                path: "absolute_path".to_string(),
1126                channel: "another_channel".to_string(),
1127                features: Default::default(),
1128            }],
1129            ..Default::default()
1130        };
1131
1132        let mut map = Default::default();
1133        let res = parser.check_can_merge_imports(&parent_path, &parent.imports, &mut map);
1134        assert!(res.is_ok());
1135        assert_eq!(map.get("absolute_path").unwrap(), "one_channel");
1136
1137        let err_msg = "Problem with http://child/: File absolute_path is imported with two different channels: one_channel and another_channel";
1138        let res = parser.check_can_merge_imports(&child_path, &child.imports, &mut map);
1139        assert!(res.is_err());
1140        assert_eq!(res.unwrap_err().to_string(), err_msg.to_string());
1141
1142        let res = parser.check_can_merge_manifest(&parent_path, &parent, &child_path, &child);
1143        assert!(res.is_err());
1144        assert_eq!(res.unwrap_err().to_string(), err_msg.to_string());
1145
1146        Ok(())
1147    }
1148
1149    #[test]
1150    fn test_include_circular_includes() -> Result<()> {
1151        use crate::util::pkg_dir;
1152        // snake.yaml includes tail.yaml, which includes snake.yaml
1153        let path = PathBuf::from(pkg_dir()).join("fixtures/fe/including/circular/snake.yaml");
1154
1155        let files = FileLoader::default()?;
1156        let parser = Parser::new(files, path.as_path().into())?;
1157        let ir = parser.get_intermediate_representation(Some("release"));
1158        assert!(ir.is_ok());
1159
1160        Ok(())
1161    }
1162
1163    #[test]
1164    fn test_include_deeply_nested_includes() -> Result<()> {
1165        use crate::util::pkg_dir;
1166        // Deeply nested includes, which start at 00-head.yaml, and then recursively includes all the
1167        // way down to 06-toe.yaml
1168        let path_buf = PathBuf::from(pkg_dir()).join("fixtures/fe/including/deep/00-head.yaml");
1169
1170        let files = FileLoader::default()?;
1171        let parser = Parser::new(files, path_buf.as_path().into())?;
1172
1173        let ir = parser.get_intermediate_representation(Some("release"))?;
1174        assert_eq!(ir.feature_defs.len(), 1);
1175
1176        Ok(())
1177    }
1178}