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