nimbus_fml/client/
inspector.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
5pub use crate::editing::FmlEditorError;
6use crate::{
7    editing::{CursorPosition, ErrorConverter},
8    error::{ClientError, FMLError, Result},
9    intermediate_representation::{FeatureDef, FeatureExample, FeatureManifest},
10    FmlClient, JsonObject,
11};
12use serde_json::Value;
13use std::sync::Arc;
14use url::Url;
15
16impl FmlClient {
17    pub fn get_feature_inspector(&self, id: String) -> Option<Arc<FmlFeatureInspector>> {
18        _ = self.manifest.find_feature(&id)?;
19        Some(Arc::new(FmlFeatureInspector::new(
20            self.manifest.clone(),
21            id,
22        )))
23    }
24}
25
26pub struct FmlFeatureInspector {
27    manifest: Arc<FeatureManifest>,
28    feature_id: String,
29}
30
31impl FmlFeatureInspector {
32    pub(crate) fn new(manifest: Arc<FeatureManifest>, feature_id: String) -> Self {
33        Self {
34            manifest,
35            feature_id,
36        }
37    }
38
39    pub fn get_default_json(&self) -> Result<JsonObject> {
40        let f = self.get_feature();
41
42        match f.default_json() {
43            Value::Object(map) => Ok(map),
44            _ => Err(FMLError::ClientError(ClientError::InvalidFeatureValue(
45                "A non-JSON object is returned as default. This is likely a Nimbus FML bug."
46                    .to_string(),
47            ))),
48        }
49    }
50
51    pub fn get_examples(&self) -> Result<Vec<FmlFeatureExample>> {
52        let feature_examples = &self.get_feature().examples;
53        let mut examples: Vec<FmlFeatureExample> = Vec::with_capacity(feature_examples.len() + 1);
54        // Make an FmlFeatureExample out of the FeatureExample, for exposure to foreign languages.
55        examples.extend(feature_examples.clone().into_iter().map(Into::into).rev());
56
57        // Add the full defaults for every feature.
58        // This will help kick-start adoption.
59        examples.push(FmlFeatureExample {
60            name: String::from("Default configuration (in full)"),
61            value: self.get_default_json()?,
62            ..Default::default()
63        });
64
65        Ok(examples)
66    }
67
68    pub fn get_errors(&self, string: String) -> Option<Vec<FmlEditorError>> {
69        match self.parse_json_string(&string) {
70            Err(e) => Some(vec![e]),
71            Ok(json) => {
72                let errors = self.get_semantic_errors(&string, json);
73                if errors.is_empty() {
74                    None
75                } else {
76                    Some(errors)
77                }
78            }
79        }
80    }
81
82    pub fn get_schema_hash(&self) -> String {
83        let (fm, f) = self.get_manifest_and_feature();
84        fm.feature_schema_hash(f)
85    }
86
87    pub fn get_defaults_hash(&self) -> String {
88        let (fm, f) = self.get_manifest_and_feature();
89        fm.feature_defaults_hash(f)
90    }
91}
92
93impl FmlFeatureInspector {
94    fn get_feature(&self) -> &FeatureDef {
95        self.get_manifest_and_feature().1
96    }
97
98    fn _get_manifest(&self) -> &FeatureManifest {
99        self.get_manifest_and_feature().0
100    }
101
102    fn get_manifest_and_feature(&self) -> (&FeatureManifest, &FeatureDef) {
103        self.manifest
104            .find_feature(&self.feature_id)
105            .expect("We construct an inspector with a feature_id, so this should be impossible")
106    }
107
108    fn parse_json_string(&self, string: &str) -> Result<Value, FmlEditorError> {
109        Ok(match serde_json::from_str::<Value>(string) {
110            Ok(json) if json.is_object() => json,
111            Ok(_) => syntax_error("Need valid JSON object", 0, 0, string)?,
112            Err(e) => {
113                let col = e.column();
114                syntax_error(
115                    "Need valid JSON object",
116                    e.line() - 1,
117                    if col == 0 { 0 } else { col - 1 },
118                    "",
119                )?
120            }
121        })
122    }
123
124    fn get_semantic_errors(&self, src: &str, value: Value) -> Vec<FmlEditorError> {
125        let (manifest, feature_def) = self.get_manifest_and_feature();
126        let (merged_value, errors) = manifest.merge_and_errors(feature_def, &value);
127        if !errors.is_empty() {
128            let converter = ErrorConverter::new(&manifest.enum_defs, &manifest.obj_defs);
129            converter.convert_into_editor_errors(feature_def, &merged_value, src, &errors)
130        } else {
131            Default::default()
132        }
133    }
134}
135
136fn syntax_error(
137    message: &str,
138    line: usize,
139    col: usize,
140    highlight: &str,
141) -> Result<Value, FmlEditorError> {
142    let error_span = CursorPosition::new(line, col) + highlight;
143    Err(FmlEditorError {
144        message: String::from(message),
145        error_span,
146        line: line as u32,
147        col: col as u32,
148        ..Default::default()
149    })
150}
151
152#[derive(Default)]
153pub struct FmlFeatureExample {
154    pub name: String,
155    pub description: Option<String>,
156    pub url: Option<Url>,
157    pub value: JsonObject,
158}
159
160impl From<FeatureExample> for FmlFeatureExample {
161    fn from(example: FeatureExample) -> Self {
162        let metadata = example.metadata;
163        Self {
164            name: metadata.name,
165            description: metadata.description,
166            url: metadata.url,
167            value: match example.value {
168                Value::Object(v) => v,
169                _ => Default::default(),
170            },
171        }
172    }
173}
174
175#[cfg(test)]
176mod unit_tests {
177    use crate::{client::test_helper::client, editing::FmlEditorError};
178
179    use super::*;
180
181    impl FmlFeatureInspector {
182        pub(crate) fn get_first_error(&self, string: String) -> Option<FmlEditorError> {
183            let mut errors = self.get_errors(string)?;
184            errors.pop()
185        }
186    }
187
188    #[test]
189    fn test_construction() -> Result<()> {
190        let client = client("./nimbus_features.yaml", "release")?;
191        assert_eq!(
192            client.get_feature_ids(),
193            vec!["dialog-appearance".to_string()]
194        );
195        let f = client.get_feature_inspector("dialog-appearance".to_string());
196        assert!(f.is_some());
197
198        let f = client.get_feature_inspector("not-there".to_string());
199        assert!(f.is_none());
200
201        Ok(())
202    }
203
204    #[test]
205    fn test_get_first_error_invalid_json() -> Result<()> {
206        let client = client("./nimbus_features.yaml", "release")?;
207        let f = client
208            .get_feature_inspector("dialog-appearance".to_string())
209            .unwrap();
210
211        fn test_syntax_error(
212            inspector: &FmlFeatureInspector,
213            input: &str,
214            col: usize,
215            highlight: bool,
216        ) {
217            let error = inspector
218                .get_first_error(input.to_string())
219                .unwrap_or_else(|| unreachable!("No error for '{input}'"));
220            let highlight = if highlight { input } else { "" };
221            assert_eq!(
222                error,
223                syntax_error("Need valid JSON object", 0, col, highlight).unwrap_err()
224            );
225        }
226
227        test_syntax_error(&f, "", 0, false);
228        test_syntax_error(&f, "x", 0, false);
229        test_syntax_error(&f, "{ \"\" }, ", 5, false);
230        test_syntax_error(&f, "{ \"foo\":", 7, false);
231
232        test_syntax_error(&f, "[]", 0, true);
233        test_syntax_error(&f, "1", 0, true);
234        test_syntax_error(&f, "true", 0, true);
235        test_syntax_error(&f, "\"string\"", 0, true);
236
237        assert!(f.get_first_error("{}".to_string()).is_none());
238        Ok(())
239    }
240
241    #[test]
242    fn test_get_first_error_type_invalid() -> Result<()> {
243        let client = client("./nimbus_features.yaml", "release")?;
244        let f = client
245            .get_feature_inspector("dialog-appearance".to_string())
246            .unwrap();
247
248        let s = r#"{}"#;
249        assert!(f.get_first_error(s.to_string()).is_none());
250        let s = r#"{
251            "positive": {}
252        }"#;
253        assert!(f.get_first_error(s.to_string()).is_none());
254
255        let s = r#"{
256            "positive": 1
257        }"#;
258        if let Some(_err) = f.get_first_error(s.to_string()) {
259        } else {
260            unreachable!("No error for \"{s}\"");
261        }
262
263        let s = r#"{
264            "positive1": {}
265        }"#;
266        if let Some(_err) = f.get_first_error(s.to_string()) {
267        } else {
268            unreachable!("No error for \"{s}\"");
269        }
270
271        Ok(())
272    }
273
274    #[test]
275    fn test_deterministic_errors() -> Result<()> {
276        let client = client("./nimbus_features.yaml", "release")?;
277        let inspector = client
278            .get_feature_inspector("dialog-appearance".to_string())
279            .unwrap();
280
281        let s = r#"{
282            "positive": { "yes" : { "trait": 1 }  }
283        }"#;
284        let err1 = inspector
285            .get_first_error(s.to_string())
286            .unwrap_or_else(|| unreachable!("No error for \"{s}\""));
287
288        let err2 = inspector
289            .get_first_error(s.to_string())
290            .unwrap_or_else(|| unreachable!("No error for \"{s}\""));
291
292        assert_eq!(err1, err2);
293
294        Ok(())
295    }
296
297    #[test]
298    fn test_semantic_errors() -> Result<()> {
299        let client = client("./browser.yaml", "release")?;
300        let inspector = client
301            .get_feature_inspector("nimbus-validation".to_string())
302            .unwrap();
303
304        let do_test = |lines: &[&str], token: &str, expected: (u32, u32)| {
305            let input = lines.join("\n");
306            let err = inspector
307                .get_first_error(input.clone())
308                .unwrap_or_else(|| unreachable!("No error for \"{input}\""));
309
310            assert_eq!(
311                err.highlight,
312                Some(token.to_string()),
313                "Token {token} not detected in error in {input}"
314            );
315
316            let observed = (err.error_span.from.line, err.error_span.from.col);
317            assert_eq!(
318                expected, observed,
319                "Error at {token} in the wrong place in {input}"
320            );
321        };
322
323        // invalid property name.
324        do_test(
325            &[
326                // 012345678901234567890
327                r#"{"#,              // 0
328                r#"  "invalid": 1"#, // 1
329                r#"}"#,              // 2
330            ],
331            "\"invalid\"",
332            (1, 2),
333        );
334
335        // simple type mismatch
336        do_test(
337            &[
338                // 012345678901234567890
339                r#"{"#,                // 0
340                r#"  "icon-type": 1"#, // 1
341                r#"}"#,                // 2
342            ],
343            "1",
344            (1, 15),
345        );
346
347        // enum mismatch
348        do_test(
349            &[
350                // 012345678901234567890
351                r#"{"#,                        // 0
352                r#"  "icon-type": "invalid""#, // 1
353                r#"}"#,                        // 2
354            ],
355            "\"invalid\"",
356            (1, 15),
357        );
358
359        // invalid field within object
360        do_test(
361            &[
362                // 012345678901234567890
363                r#"{"#,                   // 0
364                r#"  "nested": {"#,       // 1
365                r#"    "invalid": true"#, // 2
366                r#"  }"#,                 // 3
367                r#"}"#,                   // 4
368            ],
369            "\"invalid\"",
370            (2, 4),
371        );
372
373        // nested in an object type mismatch
374        do_test(
375            &[
376                // 012345678901234567890
377                r#"{"#,                    // 0
378                r#"  "nested": {"#,        // 1
379                r#"    "is-useful": 256"#, // 2
380                r#"  }"#,                  // 3
381                r#"}"#,                    // 4
382            ],
383            "256",
384            (2, 17),
385        );
386
387        // nested in a map type mismatch
388        do_test(
389            &[
390                // 012345678901234567890
391                r#"{"#,                      // 0
392                r#"  "string-int-map": {"#,  // 1
393                r#"    "valid": "invalid""#, // 2
394                r#"  }"#,                    // 3
395                r#"}"#,                      // 4
396            ],
397            "\"invalid\"",
398            (2, 13),
399        );
400
401        // invalid key in enum map
402        do_test(
403            &[
404                // 012345678901234567890
405                r#"{"#,                 // 0
406                r#"  "enum-map": {"#,   // 1
407                r#"    "invalid": 42"#, // 2
408                r#"  }"#,               // 3
409                r#"}"#,                 // 4
410            ],
411            "\"invalid\"",
412            (2, 4),
413        );
414
415        // type mismatch in list
416        do_test(
417            &[
418                // 012345678901234567890
419                r#"{"#,                         // 0
420                r#"  "nested-list": ["#,        // 1
421                r#"     {"#,                    // 2
422                r#"        "is-useful": true"#, // 3
423                r#"     },"#,                   // 4
424                r#"     false"#,                // 5
425                r#"  ]"#,                       // 6
426                r#"}"#,                         // 7
427            ],
428            "false",
429            (5, 5),
430        );
431
432        // Difficult!
433        do_test(
434            &[
435                // 012345678901234567890
436                r#"{"#,                          // 0
437                r#"  "string-int-map": {"#,      // 1
438                r#"    "nested": 1,"#,           // 2
439                r#"    "is-useful": 2,"#,        // 3
440                r#"    "invalid": 3"#,           // 4 error is not here!
441                r#"  },"#,                       // 5
442                r#"  "nested": {"#,              // 6
443                r#"    "is-useful": "invalid""#, // 7 error is here!
444                r#"  }"#,                        // 8
445                r#"}"#,                          // 9
446            ],
447            "\"invalid\"",
448            (7, 17),
449        );
450
451        Ok(())
452    }
453}
454
455#[cfg(test)]
456mod correction_candidates {
457    use crate::{
458        client::test_helper::client,
459        editing::{CorrectionCandidate, CursorSpan},
460    };
461
462    use super::*;
463
464    // Makes a correction; this is a simulation of what the editor will do.
465    fn perform_correction(
466        lines: &[&str],
467        position: &CursorSpan,
468        correction: &CorrectionCandidate,
469    ) -> String {
470        let position = correction.insertion_span.as_ref().unwrap_or(position);
471        position.insert_str(lines, &correction.insert)
472    }
473
474    /// Takes an editor input and an inspector.
475    /// The editor input (lines) should have exactly one thing wrong with it.
476    ///
477    /// The correction candidates are tried one by one, and then the lines are
478    /// inspected again.
479    ///
480    /// The function fails if:
481    /// a) there are no errors in the initial text
482    /// b) there are no completions in the first error.
483    /// c) after applying each correction, then there is still an error.
484    ///
485    /// For obvious reasons, this does not handle arbitrary text. Some text will have too
486    /// many errors, some will not have any corrections, and some errors will not be corrected
487    /// by every correction (e.g. the key in a feature or object).
488    fn try_correcting_single_error(inspector: &FmlFeatureInspector, lines: &[&str]) {
489        let input = lines.join("\n");
490        let err = inspector.get_first_error(input.clone());
491        assert_ne!(None, err, "No error found in input: {input}");
492        let err = err.unwrap();
493        assert_ne!(
494            0,
495            err.corrections.len(),
496            "No corrections for {input}: {err:?}"
497        );
498
499        for correction in &err.corrections {
500            let input = perform_correction(lines, &err.error_span, correction);
501            let err = inspector.get_first_error(input.clone());
502            assert_eq!(None, err, "Error found in {input}");
503        }
504    }
505
506    #[test]
507    fn test_correction_candidates_placeholders_scalar() -> Result<()> {
508        let fm = client("./browser.yaml", "release")?;
509
510        let inspector = fm
511            .get_feature_inspector("search-term-groups".to_string())
512            .unwrap();
513        // Correcting a Boolean, should correct 1 to true or false
514        try_correcting_single_error(
515            &inspector,
516            &[
517                // 012345678901234567890
518                r#"{"#,              // 0
519                r#"  "enabled": 1"#, // 1
520                r#"}"#,              // 2
521            ],
522        );
523
524        let inspector = fm
525            .get_feature_inspector("nimbus-validation".to_string())
526            .unwrap();
527
528        // Correcting an Text, should correct 1 to ""
529        try_correcting_single_error(
530            &inspector,
531            &[
532                // 012345678901234567890
533                r#"{"#,                           // 0
534                r#"  "settings-punctuation": 1"#, // 1
535                r#"}"#,                           // 2
536            ],
537        );
538
539        // Correcting an Image, should correct 1 to ""
540        try_correcting_single_error(
541            &inspector,
542            &[
543                // 012345678901234567890
544                r#"{"#,                    // 0
545                r#"  "settings-icon": 1"#, // 1
546                r#"}"#,                    // 2
547            ],
548        );
549
550        // Correcting an Int, should correct "not-valid" to 0
551        try_correcting_single_error(
552            &inspector,
553            &[
554                // 012345678901234567890
555                r#"{"#,                          // 0
556                r#"  "string-int-map": { "#,     // 1
557                r#"     "valid": "not-valid" "#, // 2
558                r#"   }"#,                       // 3
559                r#"}"#,                          // 4
560            ],
561        );
562        Ok(())
563    }
564
565    #[test]
566    fn test_correction_candidates_replacing_structural() -> Result<()> {
567        let fm = client("./browser.yaml", "release")?;
568        let inspector = fm
569            .get_feature_inspector("nimbus-validation".to_string())
570            .unwrap();
571
572        // Correcting an Text, should correct {} to ""
573        try_correcting_single_error(
574            &inspector,
575            &[
576                // 012345678901234567890
577                r#"{"#,                            // 0
578                r#"  "settings-punctuation": {}"#, // 1
579                r#"}"#,                            // 2
580            ],
581        );
582
583        // Correcting an Text, should correct [] to ""
584        try_correcting_single_error(
585            &inspector,
586            &[
587                // 012345678901234567890
588                r#"{"#,                            // 0
589                r#"  "settings-punctuation": []"#, // 1
590                r#"}"#,                            // 2
591            ],
592        );
593
594        // Correcting an Text, should correct ["foo"] to ""
595        try_correcting_single_error(
596            &inspector,
597            &[
598                // 012345678901234567890
599                r#"{"#,                                 // 0
600                r#"  "settings-punctuation": ["foo"]"#, // 1
601                r#"}"#,                                 // 2
602            ],
603        );
604
605        Ok(())
606    }
607
608    // All of theses corrections fail because error_path is currently only able
609    // to encode the last token as the one in error. If the value in error is a `{ }`, it's encoded
610    // as `{}`, which is not found in the source code.
611    // The solution is to make error_path keep track of the start token and end token, and calculate
612    // an `error_range(src: &src) -> (from: CursorPosition, to: CursorPosition)`.
613    // Until that happens, we'll ignore this test.
614    #[test]
615    fn test_correction_candidates_replacing_structural_plus_whitespace() -> Result<()> {
616        let fm = client("./browser.yaml", "release")?;
617        let inspector = fm
618            .get_feature_inspector("nimbus-validation".to_string())
619            .unwrap();
620
621        // Correcting an Text, should correct { } to ""
622        try_correcting_single_error(
623            &inspector,
624            &[
625                // 012345678901234567890
626                r#"{"#,                             // 0
627                r#"  "settings-punctuation": { }"#, // 1
628                r#"}"#,                             // 2
629            ],
630        );
631
632        // Correcting an Text, should correct [ ] to ""
633        try_correcting_single_error(
634            &inspector,
635            &[
636                // 012345678901234567890
637                r#"{"#,                             // 0
638                r#"  "settings-punctuation": [ ]"#, // 1
639                r#"}"#,                             // 2
640            ],
641        );
642
643        // Correcting an Text, should correct [ "foo"] to ""
644        try_correcting_single_error(
645            &inspector,
646            &[
647                // 012345678901234567890
648                r#"{"#,                                  // 0
649                r#"  "settings-punctuation": [ "foo"]"#, // 1
650                r#"}"#,                                  // 2
651            ],
652        );
653
654        Ok(())
655    }
656
657    #[test]
658    fn test_correction_candidates_placeholders_structural() -> Result<()> {
659        let fm = client("./browser.yaml", "release")?;
660        let inspector = fm
661            .get_feature_inspector("nimbus-validation".to_string())
662            .unwrap();
663
664        // Correcting an Option<Text>, should correct true to ""
665        try_correcting_single_error(
666            &inspector,
667            &[
668                // 012345678901234567890
669                r#"{"#,                        // 0
670                r#"  "settings-title": true"#, // 1
671                r#"}"#,                        // 2
672            ],
673        );
674
675        // Correcting an Map<String, String>, should correct 1 to {}
676        try_correcting_single_error(
677            &inspector,
678            &[
679                // 012345678901234567890
680                r#"{"#,                 // 0
681                r#"  "string-map": 1"#, // 1
682                r#"}"#,                 // 2
683            ],
684        );
685
686        // Correcting a nested ValidationObject, should correct 1 to {}
687        try_correcting_single_error(
688            &inspector,
689            &[
690                // 012345678901234567890
691                r#"{"#,             // 0
692                r#"  "nested": 1"#, // 1
693                r#"}"#,             // 2
694            ],
695        );
696
697        // Correcting a Option<ValidationObject>, should correct 1 to {}
698        try_correcting_single_error(
699            &inspector,
700            &[
701                // 012345678901234567890
702                r#"{"#,                      // 0
703                r#"  "nested-optional": 1"#, // 1
704                r#"}"#,                      // 2
705            ],
706        );
707
708        // Correcting a List<ValidationObject>, should correct 1 to []
709        try_correcting_single_error(
710            &inspector,
711            &[
712                // 012345678901234567890
713                r#"{"#,                  // 0
714                r#"  "nested-list": 1"#, // 1
715                r#"}"#,                  // 2
716            ],
717        );
718
719        // Correcting a List<ValidationObject>, should correct 1 to {}
720        try_correcting_single_error(
721            &inspector,
722            &[
723                // 012345678901234567890
724                r#"{"#,                    // 0
725                r#"  "nested-list": [1]"#, // 1
726                r#"}"#,                    // 2
727            ],
728        );
729
730        Ok(())
731    }
732
733    #[test]
734    fn test_correction_candidates_property_keys() -> Result<()> {
735        let fm = client("./browser.yaml", "release")?;
736        let inspector = fm.get_feature_inspector("homescreen".to_string()).unwrap();
737
738        try_correcting_single_error(
739            &inspector,
740            &[
741                // 012345678901234567890
742                r#"{"#,               // 0
743                r#"  "invalid": {}"#, // 1
744                r#"}"#,               // 2
745            ],
746        );
747        Ok(())
748    }
749
750    #[test]
751    fn test_correction_candidates_enum_strings() -> Result<()> {
752        let fm = client("./enums.fml.yaml", "release")?;
753        let inspector = fm
754            .get_feature_inspector("my-coverall-feature".to_string())
755            .unwrap();
756
757        try_correcting_single_error(
758            &inspector,
759            &[
760                // 012345678901234567890123
761                r#"{"#,                // 0
762                r#"  "scalar": true"#, // 1
763                r#"}"#,                // 2
764            ],
765        );
766
767        try_correcting_single_error(
768            &inspector,
769            &[
770                // 012345678901234567890123
771                r#"{"#,              // 0
772                r#"  "scalar": 13"#, // 1
773                r#"}"#,              // 2
774            ],
775        );
776
777        try_correcting_single_error(
778            &inspector,
779            &[
780                // 012345678901234567890123
781                r#"{"#,              // 0
782                r#"  "list": [13]"#, // 1
783                r#"}"#,              // 2
784            ],
785        );
786
787        try_correcting_single_error(
788            &inspector,
789            &[
790                // 012345678901234567890123
791                r#"{"#,                      // 0
792                r#"  "list": ["top", 13 ]"#, // 1
793                r#"}"#,                      // 2
794            ],
795        );
796
797        try_correcting_single_error(
798            &inspector,
799            &[
800                // 012345678901234567890123
801                r#"{"#,                   // 0
802                r#"  "list": [ false ]"#, // 1
803                r#"}"#,                   // 2
804            ],
805        );
806
807        try_correcting_single_error(
808            &inspector,
809            &[
810                // 012345678901234567890123
811                r#"{"#,                         // 0
812                r#"  "list": ["top", false ]"#, // 1
813                r#"}"#,                         // 2
814            ],
815        );
816
817        try_correcting_single_error(
818            &inspector,
819            &[
820                // 012345678901234567890123
821                r#"{"#,                             // 0
822                r#"  "map": { "invalid": false }"#, // 1
823                r#"}"#,                             // 2
824            ],
825        );
826
827        try_correcting_single_error(
828            &inspector,
829            &[
830                // 012345678901234567890123
831                r#"{"#,                       // 0
832                r#"  "map": { "#,             // 1
833                r#"      "top": false, "#,    // 2
834                r#"      "invalid": false "#, // 3
835                r#"   } "#,                   // 4
836                r#"}"#,                       // 5
837            ],
838        );
839
840        Ok(())
841    }
842
843    #[test]
844    fn test_correction_candidates_string_aliases() -> Result<()> {
845        let fm = client("string-aliases.fml.yaml", "storms")?;
846        let inspector = fm
847            .get_feature_inspector("my-coverall-team".to_string())
848            .unwrap();
849
850        try_correcting_single_error(
851            &inspector,
852            &[
853                // 012345678901234567890123
854                r#"{                    "#, // 0
855                r#"  "players": [       "#, // 1
856                r#"       "Shrek",      "#, // 2
857                r#"       "Fiona"       "#, // 3
858                r#"  ],                 "#, // 4
859                r#"  "top-player": true "#, // 5
860                r#"}"#,                     // 6
861            ],
862        );
863
864        try_correcting_single_error(
865            &inspector,
866            &[
867                // 012345678901234567890123
868                r#"{                       "#, // 0
869                r#"  "players": [          "#, // 1
870                r#"       "Shrek",         "#, // 2
871                r#"       "Fiona"          "#, // 3
872                r#"  ],                    "#, // 4
873                r#"  "top-player": "Donkey""#, // 5
874                r#"}"#,                        // 6
875            ],
876        );
877
878        try_correcting_single_error(
879            &inspector,
880            &[
881                // 012345678901234567890123
882                r#"{                    "#, // 0
883                r#"  "players": [       "#, // 1
884                r#"       "Shrek",      "#, // 2
885                r#"       "Fiona"       "#, // 3
886                r#"  ],                 "#, // 4
887                r#"  "availability": {  "#, // 5
888                r#"     "Donkey": true  "#, // 6
889                r#"  }"#,                   // 7
890                r#"}"#,                     // 8
891            ],
892        );
893
894        try_correcting_single_error(
895            &inspector,
896            &[
897                // 012345678901234567890123
898                r#"{                    "#, // 0
899                r#"  "players": [       "#, // 1
900                r#"       "Shrek",      "#, // 2
901                r#"       "Fiona"       "#, // 3
902                r#"  ],                 "#, // 4
903                r#"  "availability": {  "#, // 5
904                r#"     "Shrek":   true,"#, // 6
905                r#"     "Donkey":  true "#, // 7
906                r#"  }"#,                   // 8
907                r#"}"#,                     // 9
908            ],
909        );
910
911        try_correcting_single_error(
912            &inspector,
913            &[
914                // 012345678901234567890123
915                r#"{                    "#, // 0
916                r#"  "players": [       "#, // 1
917                r#"       "Shrek",      "#, // 2
918                r#"       "Fiona"       "#, // 3
919                r#"  ],                 "#, // 4
920                r#"  "availability": {  "#, // 5
921                r#"     "Fiona":  true, "#, // 6
922                r#"     "invalid": true "#, // 7
923                r#"  }"#,                   // 8
924                r#"}"#,                     // 9
925            ],
926        );
927
928        Ok(())
929    }
930}
931
932#[cfg(test)]
933mod config_examples {
934    use super::*;
935    use crate::client::test_helper::client;
936
937    #[test]
938    fn smoke_test() -> Result<()> {
939        let fm = client("./config-examples/app.fml.yaml", "release")?;
940        let inspector = fm
941            .get_feature_inspector(String::from("my-component-feature"))
942            .unwrap();
943
944        let examples = inspector.get_examples()?;
945
946        assert_eq!(examples.len(), 5);
947        let names: Vec<_> = examples.iter().map(|ex| ex.name.as_str()).collect();
948        assert_eq!(
949            &[
950                "4. Partial example with JSON for imported feature",
951                "3. Inlined example for imported feature",
952                "2. An example from a file adjacent to the component",
953                "1. Inlined example for feature",
954                "Default configuration (in full)",
955            ],
956            names.as_slice()
957        );
958
959        Ok(())
960    }
961
962    #[test]
963    fn validating_test() -> Result<()> {
964        let res = client(
965            "./config-examples/app-with-broken-example.fml.yaml",
966            "release",
967        );
968        assert!(res.is_err());
969
970        let is_validation_err = matches!(
971                res.err().unwrap(),
972                FMLError::ValidationError(path, message) if
973                       path.as_str() ==
974                        "features/my-component-feature#examples[\"Broken example with invalid-property\"]"
975                    && message.starts_with(
976                        "Invalid property \"invalid-property\""));
977        assert!(is_validation_err);
978
979        Ok(())
980    }
981}