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