1use 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#[non_exhaustive]
70#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)]
71pub enum TypeRef {
72 String,
74 Int,
75 Boolean,
76
77 StringAlias(String),
79
80 BundleText,
84 BundleImage,
85
86 Enum(String),
87 Object(String),
89
90 StringMap(Box<TypeRef>),
93 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 _ => 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#[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 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 self.validate_schema()?;
309 self.validate_defaults()?;
310
311 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 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}