esp_config/generate/
mod.rs

1use core::fmt::Display;
2use std::{collections::HashMap, env, fmt, fs, io::Write, path::PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6use crate::generate::{validator::Validator, value::Value};
7
8mod markdown;
9pub(crate) mod validator;
10pub(crate) mod value;
11
12/// Configuration errors.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Error {
15    /// Parse errors.
16    Parse(String),
17    /// Validation errors.
18    Validation(String),
19}
20
21impl Error {
22    /// Convenience function for creating parse errors.
23    pub fn parse<S>(message: S) -> Self
24    where
25        S: Into<String>,
26    {
27        Self::Parse(message.into())
28    }
29
30    /// Convenience function for creating validation errors.
31    pub fn validation<S>(message: S) -> Self
32    where
33        S: Into<String>,
34    {
35        Self::Validation(message.into())
36    }
37}
38
39impl fmt::Display for Error {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Error::Parse(message) => write!(f, "{message}"),
43            Error::Validation(message) => write!(f, "{message}"),
44        }
45    }
46}
47
48/// Generate and parse config from a prefix, and an array tuples containing the
49/// name, description, default value, and an optional validator.
50///
51/// This function will parse any `SCREAMING_SNAKE_CASE` environment variables
52/// that match the given prefix. It will then attempt to parse the [`Value`] and
53/// run any validators which have been specified.
54///
55/// [`Stability::Unstable`] features will only be enabled if the `unstable`
56/// feature is enabled in the dependant crate. If the `unstable` feature is not
57/// enabled, setting these options will result in a build error.
58///
59/// Once the config has been parsed, this function will emit `snake_case` cfg's
60/// _without_ the prefix which can be used in the dependant crate. After that,
61/// it will create a markdown table in the `OUT_DIR` under the name
62/// `{prefix}_config_table.md` where prefix has also been converted to
63/// `snake_case`. This can be included in crate documentation to outline the
64/// available configuration options for the crate.
65///
66/// Passing a value of true for the `emit_md_tables` argument will create and
67/// write markdown files of the available configuration and selected
68/// configuration which can be included in documentation.
69///
70/// Unknown keys with the supplied prefix will cause this function to panic.
71pub fn generate_config(
72    crate_name: &str,
73    config: &[ConfigOption],
74    enable_unstable: bool,
75    emit_md_tables: bool,
76) -> HashMap<String, Value> {
77    let configs = generate_config_internal(std::io::stdout(), crate_name, config, enable_unstable);
78
79    if emit_md_tables {
80        let file_name = snake_case(crate_name);
81
82        let mut doc_table = markdown::DOC_TABLE_HEADER.replace(
83            "{prefix}",
84            format!("{}_CONFIG_*", screaming_snake_case(crate_name)).as_str(),
85        );
86        let mut selected_config = String::from(markdown::SELECTED_TABLE_HEADER);
87
88        for (name, option, value) in configs.iter() {
89            if !option.active {
90                continue;
91            }
92            markdown::write_doc_table_line(&mut doc_table, name, option);
93            markdown::write_summary_table_line(&mut selected_config, name, value);
94        }
95
96        write_out_file(format!("{file_name}_config_table.md"), doc_table);
97        write_out_file(format!("{file_name}_selected_config.md"), selected_config);
98    }
99
100    // Remove the ConfigOptions from the output
101    configs.into_iter().map(|(k, _, v)| (k, v)).collect()
102}
103
104pub fn generate_config_internal<'a>(
105    mut stdout: impl Write,
106    crate_name: &str,
107    config: &'a [ConfigOption],
108    enable_unstable: bool,
109) -> Vec<(String, &'a ConfigOption, Value)> {
110    // Only rebuild if `build.rs` changed. Otherwise, Cargo will rebuild if any
111    // other file changed.
112    writeln!(stdout, "cargo:rerun-if-changed=build.rs").ok();
113
114    // Ensure that the prefix is `SCREAMING_SNAKE_CASE`:
115    let prefix = format!("{}_CONFIG_", screaming_snake_case(crate_name));
116
117    let mut configs = create_config(&prefix, config);
118    capture_from_env(crate_name, &prefix, &mut configs, enable_unstable);
119
120    for (_, option, value) in configs.iter() {
121        if let Some(ref validator) = option.constraint {
122            validator.validate(value).unwrap();
123        }
124    }
125
126    emit_configuration(&mut stdout, &configs);
127
128    #[cfg(not(test))]
129    {
130        let config_json = config_json(&configs, false);
131        write_out_file(format!("{crate_name}_config_data.json"), config_json);
132    }
133
134    configs
135}
136
137fn config_json(config: &[(String, &ConfigOption, Value)], pretty: bool) -> String {
138    #[derive(Serialize)]
139    struct Item<'a> {
140        option: &'a ConfigOption,
141        actual_value: Value,
142    }
143
144    let mut to_write = Vec::new();
145    for (_, option, value) in config.iter() {
146        to_write.push(Item {
147            actual_value: value.clone(),
148            option,
149        })
150    }
151
152    if pretty {
153        serde_json::to_string_pretty(&to_write).unwrap()
154    } else {
155        serde_json::to_string(&to_write).unwrap()
156    }
157}
158
159/// The stability of the configuration option.
160#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
161pub enum Stability {
162    /// Unstable options need to be activated with the `unstable` feature
163    /// of the package that defines them.
164    Unstable,
165    /// Stable options contain the first version in which they were
166    /// stabilized.
167    Stable(String),
168}
169
170impl Display for Stability {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self {
173            Stability::Unstable => write!(f, "⚠️ Unstable"),
174            Stability::Stable(version) => write!(f, "Stable since {version}"),
175        }
176    }
177}
178
179/// A display hint (for tooling only)
180#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
181pub enum DisplayHint {
182    /// No display hint
183    None,
184
185    /// Use a binary representation
186    Binary,
187
188    /// Use a hexadecimal representation
189    Hex,
190}
191
192/// A configuration option.
193#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
194pub struct ConfigOption {
195    /// The name of the configuration option.
196    ///
197    /// The associated environment variable has the format of
198    /// `<PREFIX>_CONFIG_<NAME>`.
199    pub name: String,
200
201    /// The description of the configuration option.
202    ///
203    /// The description will be included in the generated markdown
204    /// documentation.
205    pub description: String,
206
207    /// The default value of the configuration option.
208    pub default_value: Value,
209
210    /// An optional validator for the configuration option.
211    pub constraint: Option<Validator>,
212
213    /// The stability of the configuration option.
214    pub stability: Stability,
215
216    /// Whether the config option should be offered to the user.
217    ///
218    /// Inactive options are not included in the documentation, and accessing
219    /// them provides the default value.
220    pub active: bool,
221
222    /// A display hint (for tooling)
223    pub display_hint: DisplayHint,
224}
225
226impl ConfigOption {
227    /// Create a new config option.
228    ///
229    /// Unstable, active, no display-hint and not constrained by default.
230    pub fn new(name: &str, description: &str, default_value: impl Into<Value>) -> Self {
231        Self {
232            name: name.to_string(),
233            description: description.to_string(),
234            default_value: default_value.into(),
235            constraint: None,
236            stability: Stability::Unstable,
237            active: true,
238            display_hint: DisplayHint::None,
239        }
240    }
241
242    /// Constrain the config option
243    pub fn constraint(mut self, validator: Validator) -> Self {
244        self.constraint = Some(validator);
245        self
246    }
247
248    /// Constrain the config option
249    pub fn constraint_by(mut self, validator: Option<Validator>) -> Self {
250        self.constraint = validator;
251        self
252    }
253
254    /// Mark this config option as stable
255    pub fn stable(mut self, version: &str) -> Self {
256        self.stability = Stability::Stable(version.to_string());
257        self
258    }
259
260    /// Sets the active flag of this config option
261    pub fn active(mut self, active: bool) -> Self {
262        self.active = active;
263        self
264    }
265
266    /// Sets the display hint
267    pub fn display_hint(mut self, display_hint: DisplayHint) -> Self {
268        self.display_hint = display_hint;
269        self
270    }
271
272    fn env_var(&self, prefix: &str) -> String {
273        format!("{}{}", prefix, screaming_snake_case(&self.name))
274    }
275
276    fn cfg_name(&self) -> String {
277        snake_case(&self.name)
278    }
279
280    fn is_stable(&self) -> bool {
281        matches!(self.stability, Stability::Stable(_))
282    }
283}
284
285fn create_config<'a>(
286    prefix: &str,
287    config: &'a [ConfigOption],
288) -> Vec<(String, &'a ConfigOption, Value)> {
289    let mut configs = Vec::with_capacity(config.len());
290
291    for option in config {
292        configs.push((option.env_var(prefix), option, option.default_value.clone()));
293    }
294
295    configs
296}
297
298fn capture_from_env(
299    crate_name: &str,
300    prefix: &str,
301    configs: &mut Vec<(String, &ConfigOption, Value)>,
302    enable_unstable: bool,
303) {
304    let mut unknown = Vec::new();
305    let mut failed = Vec::new();
306    let mut unstable = Vec::new();
307
308    // Try and capture input from the environment:
309    for (var, value) in env::vars() {
310        if var.starts_with(prefix) {
311            let Some((_, option, cfg)) = configs.iter_mut().find(|(k, _, _)| k == &var) else {
312                unknown.push(var);
313                continue;
314            };
315
316            if !option.active {
317                unknown.push(var);
318                continue;
319            }
320
321            if !enable_unstable && !option.is_stable() {
322                unstable.push(var);
323                continue;
324            }
325
326            if let Err(e) = cfg.parse_in_place(&value) {
327                failed.push(format!("{var}: {e}"));
328            }
329        }
330    }
331
332    if !failed.is_empty() {
333        panic!("Invalid configuration options detected: {failed:?}");
334    }
335
336    if !unstable.is_empty() {
337        panic!(
338            "The following configuration options are unstable: {unstable:?}. You can enable it by \
339            activating the 'unstable' feature in {crate_name}."
340        );
341    }
342
343    if !unknown.is_empty() {
344        panic!("Unknown configuration options detected: {unknown:?}");
345    }
346}
347
348fn emit_configuration(mut stdout: impl Write, configs: &[(String, &ConfigOption, Value)]) {
349    for (env_var_name, option, value) in configs.iter() {
350        let cfg_name = option.cfg_name();
351
352        // Output the raw configuration as an env var. Values that haven't been seen
353        // will be output here with the default value. Also trigger a rebuild if config
354        // environment variable changed.
355        writeln!(stdout, "cargo:rustc-env={env_var_name}={value}").ok();
356        writeln!(stdout, "cargo:rerun-if-env-changed={env_var_name}").ok();
357
358        // Emit known config symbol:
359        writeln!(stdout, "cargo:rustc-check-cfg=cfg({cfg_name})").ok();
360
361        // Emit specially-handled values:
362        if let Value::Bool(true) = value {
363            writeln!(stdout, "cargo:rustc-cfg={cfg_name}").ok();
364        }
365
366        // Emit extra symbols based on the validator (e.g. enumerated values):
367        if let Some(validator) = option.constraint.as_ref() {
368            validator.emit_cargo_extras(&mut stdout, &cfg_name, value);
369        }
370    }
371}
372
373fn write_out_file(file_name: String, json: String) {
374    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
375    let out_file = out_dir.join(file_name);
376    fs::write(out_file, json).unwrap();
377}
378
379fn snake_case(name: &str) -> String {
380    let mut name = name.replace("-", "_");
381    name.make_ascii_lowercase();
382
383    name
384}
385
386fn screaming_snake_case(name: &str) -> String {
387    let mut name = name.replace("-", "_");
388    name.make_ascii_uppercase();
389
390    name
391}
392
393#[cfg(test)]
394mod test {
395    use super::*;
396    use crate::generate::{validator::Validator, value::Value};
397
398    #[test]
399    fn value_number_formats() {
400        const INPUTS: &[&str] = &["0xAA", "0o252", "0b0000000010101010", "170"];
401        let mut v = Value::Integer(0);
402
403        for input in INPUTS {
404            v.parse_in_place(input).unwrap();
405            // no matter the input format, the output format should be decimal
406            assert_eq!(v.to_string(), "170");
407        }
408    }
409
410    #[test]
411    fn value_bool_inputs() {
412        let mut v = Value::Bool(false);
413
414        v.parse_in_place("true").unwrap();
415        assert_eq!(v.to_string(), "true");
416
417        v.parse_in_place("false").unwrap();
418        assert_eq!(v.to_string(), "false");
419
420        v.parse_in_place("else")
421            .expect_err("Only true or false are valid");
422    }
423
424    #[test]
425    fn env_override() {
426        temp_env::with_vars(
427            [
428                ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
429                ("ESP_TEST_CONFIG_NUMBER_SIGNED", Some("-999")),
430                ("ESP_TEST_CONFIG_STRING", Some("Hello world!")),
431                ("ESP_TEST_CONFIG_BOOL", Some("true")),
432            ],
433            || {
434                let configs = generate_config(
435                    "esp-test",
436                    &[
437                        ConfigOption {
438                            name: String::from("number"),
439                            description: String::from("NA"),
440                            default_value: Value::Integer(999),
441                            constraint: None,
442                            stability: Stability::Stable(String::from("testing")),
443                            active: true,
444                            display_hint: DisplayHint::None,
445                        },
446                        ConfigOption {
447                            name: String::from("number_signed"),
448                            description: String::from("NA"),
449                            default_value: Value::Integer(-777),
450                            constraint: None,
451                            stability: Stability::Stable(String::from("testing")),
452                            active: true,
453                            display_hint: DisplayHint::None,
454                        },
455                        ConfigOption {
456                            name: String::from("string"),
457                            description: String::from("NA"),
458                            default_value: Value::String("Demo".to_string()),
459                            constraint: None,
460                            stability: Stability::Stable(String::from("testing")),
461                            active: true,
462                            display_hint: DisplayHint::None,
463                        },
464                        ConfigOption {
465                            name: String::from("bool"),
466                            description: String::from("NA"),
467                            default_value: Value::Bool(false),
468                            constraint: None,
469                            stability: Stability::Stable(String::from("testing")),
470                            active: true,
471                            display_hint: DisplayHint::None,
472                        },
473                        ConfigOption {
474                            name: String::from("number_default"),
475                            description: String::from("NA"),
476                            default_value: Value::Integer(999),
477                            constraint: None,
478                            stability: Stability::Stable(String::from("testing")),
479                            active: true,
480                            display_hint: DisplayHint::None,
481                        },
482                        ConfigOption {
483                            name: String::from("string_default"),
484                            description: String::from("NA"),
485                            default_value: Value::String("Demo".to_string()),
486                            constraint: None,
487                            stability: Stability::Stable(String::from("testing")),
488                            active: true,
489                            display_hint: DisplayHint::None,
490                        },
491                        ConfigOption {
492                            name: String::from("bool_default"),
493                            description: String::from("NA"),
494                            default_value: Value::Bool(false),
495                            constraint: None,
496                            stability: Stability::Stable(String::from("testing")),
497                            active: true,
498                            display_hint: DisplayHint::None,
499                        },
500                    ],
501                    false,
502                    false,
503                );
504
505                // some values have changed
506                assert_eq!(configs["ESP_TEST_CONFIG_NUMBER"], Value::Integer(0xaa));
507                assert_eq!(
508                    configs["ESP_TEST_CONFIG_NUMBER_SIGNED"],
509                    Value::Integer(-999)
510                );
511                assert_eq!(
512                    configs["ESP_TEST_CONFIG_STRING"],
513                    Value::String("Hello world!".to_string())
514                );
515                assert_eq!(configs["ESP_TEST_CONFIG_BOOL"], Value::Bool(true));
516
517                // the rest are the defaults
518                assert_eq!(
519                    configs["ESP_TEST_CONFIG_NUMBER_DEFAULT"],
520                    Value::Integer(999)
521                );
522                assert_eq!(
523                    configs["ESP_TEST_CONFIG_STRING_DEFAULT"],
524                    Value::String("Demo".to_string())
525                );
526                assert_eq!(configs["ESP_TEST_CONFIG_BOOL_DEFAULT"], Value::Bool(false));
527            },
528        )
529    }
530
531    #[test]
532    fn builtin_validation_passes() {
533        temp_env::with_vars(
534            [
535                ("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("7")),
536                ("ESP_TEST_CONFIG_NEGATIVE_NUMBER", Some("-1")),
537                ("ESP_TEST_CONFIG_NON_NEGATIVE_NUMBER", Some("0")),
538                ("ESP_TEST_CONFIG_RANGE", Some("9")),
539            ],
540            || {
541                generate_config(
542                    "esp-test",
543                    &[
544                        ConfigOption {
545                            name: String::from("positive_number"),
546                            description: String::from("NA"),
547                            default_value: Value::Integer(-1),
548                            constraint: Some(Validator::PositiveInteger),
549                            stability: Stability::Stable(String::from("testing")),
550                            active: true,
551                            display_hint: DisplayHint::None,
552                        },
553                        ConfigOption {
554                            name: String::from("negative_number"),
555                            description: String::from("NA"),
556                            default_value: Value::Integer(1),
557                            constraint: Some(Validator::NegativeInteger),
558                            stability: Stability::Stable(String::from("testing")),
559                            active: true,
560                            display_hint: DisplayHint::None,
561                        },
562                        ConfigOption {
563                            name: String::from("non_negative_number"),
564                            description: String::from("NA"),
565                            default_value: Value::Integer(-1),
566                            constraint: Some(Validator::NonNegativeInteger),
567                            stability: Stability::Stable(String::from("testing")),
568                            active: true,
569                            display_hint: DisplayHint::None,
570                        },
571                        ConfigOption {
572                            name: String::from("range"),
573                            description: String::from("NA"),
574                            default_value: Value::Integer(0),
575                            constraint: Some(Validator::IntegerInRange(5..10)),
576                            stability: Stability::Stable(String::from("testing")),
577                            active: true,
578                            display_hint: DisplayHint::None,
579                        },
580                    ],
581                    false,
582                    false,
583                )
584            },
585        );
586    }
587
588    #[test]
589    #[should_panic]
590    fn builtin_validation_bails() {
591        temp_env::with_vars([("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("-99"))], || {
592            generate_config(
593                "esp-test",
594                &[ConfigOption {
595                    name: String::from("positive_number"),
596                    description: String::from("NA"),
597                    default_value: Value::Integer(-1),
598                    constraint: Some(Validator::PositiveInteger),
599                    stability: Stability::Stable(String::from("testing")),
600                    active: true,
601                    display_hint: DisplayHint::None,
602                }],
603                false,
604                false,
605            )
606        });
607    }
608
609    #[test]
610    #[should_panic]
611    fn env_unknown_bails() {
612        temp_env::with_vars(
613            [
614                ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
615                ("ESP_TEST_CONFIG_RANDOM_VARIABLE", Some("")),
616            ],
617            || {
618                generate_config(
619                    "esp-test",
620                    &[ConfigOption {
621                        name: String::from("number"),
622                        description: String::from("NA"),
623                        default_value: Value::Integer(999),
624                        constraint: None,
625                        stability: Stability::Stable(String::from("testing")),
626                        active: true,
627                        display_hint: DisplayHint::None,
628                    }],
629                    false,
630                    false,
631                );
632            },
633        );
634    }
635
636    #[test]
637    #[should_panic]
638    fn env_invalid_values_bails() {
639        temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("Hello world"))], || {
640            generate_config(
641                "esp-test",
642                &[ConfigOption {
643                    name: String::from("number"),
644                    description: String::from("NA"),
645                    default_value: Value::Integer(999),
646                    constraint: None,
647                    stability: Stability::Stable(String::from("testing")),
648                    active: true,
649                    display_hint: DisplayHint::None,
650                }],
651                false,
652                false,
653            );
654        });
655    }
656
657    #[test]
658    fn env_unknown_prefix_is_ignored() {
659        temp_env::with_vars(
660            [("ESP_TEST_OTHER_CONFIG_NUMBER", Some("Hello world"))],
661            || {
662                generate_config(
663                    "esp-test",
664                    &[ConfigOption {
665                        name: String::from("number"),
666                        description: String::from("NA"),
667                        default_value: Value::Integer(999),
668                        constraint: None,
669                        stability: Stability::Stable(String::from("testing")),
670                        active: true,
671                        display_hint: DisplayHint::None,
672                    }],
673                    false,
674                    false,
675                );
676            },
677        );
678    }
679
680    #[test]
681    fn enumeration_validator() {
682        let mut stdout = Vec::new();
683        temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
684            generate_config_internal(
685                &mut stdout,
686                "esp-test",
687                &[ConfigOption {
688                    name: String::from("some-key"),
689                    description: String::from("NA"),
690                    default_value: Value::String("variant-0".to_string()),
691                    constraint: Some(Validator::Enumeration(vec![
692                        "variant-0".to_string(),
693                        "variant-1".to_string(),
694                    ])),
695                    stability: Stability::Stable(String::from("testing")),
696                    active: true,
697                    display_hint: DisplayHint::None,
698                }],
699                false,
700            );
701        });
702
703        let cargo_lines: Vec<&str> = std::str::from_utf8(&stdout).unwrap().lines().collect();
704        assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key)"));
705        assert!(cargo_lines.contains(&"cargo:rustc-env=ESP_TEST_CONFIG_SOME_KEY=variant-0"));
706        assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_0)"));
707        assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_1)"));
708        assert!(cargo_lines.contains(&"cargo:rustc-cfg=some_key_variant_0"));
709    }
710
711    #[test]
712    fn json_output() {
713        let mut stdout = Vec::new();
714        let config = [
715            ConfigOption {
716                name: String::from("some-key"),
717                description: String::from("NA"),
718                default_value: Value::String("variant-0".to_string()),
719                constraint: Some(Validator::Enumeration(vec![
720                    "variant-0".to_string(),
721                    "variant-1".to_string(),
722                ])),
723                stability: Stability::Stable(String::from("testing")),
724                active: true,
725                display_hint: DisplayHint::None,
726            },
727            ConfigOption {
728                name: String::from("some-key2"),
729                description: String::from("NA"),
730                default_value: Value::Bool(true),
731                constraint: None,
732                stability: Stability::Unstable,
733                active: true,
734                display_hint: DisplayHint::None,
735            },
736        ];
737        let configs =
738            temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
739                generate_config_internal(&mut stdout, "esp-test", &config, false)
740            });
741
742        let json_output = config_json(&configs, true);
743        println!("{json_output}");
744        pretty_assertions::assert_eq!(
745            r#"[
746  {
747    "option": {
748      "name": "some-key",
749      "description": "NA",
750      "default_value": {
751        "String": "variant-0"
752      },
753      "constraint": {
754        "Enumeration": [
755          "variant-0",
756          "variant-1"
757        ]
758      },
759      "stability": {
760        "Stable": "testing"
761      },
762      "active": true,
763      "display_hint": "None"
764    },
765    "actual_value": {
766      "String": "variant-0"
767    }
768  },
769  {
770    "option": {
771      "name": "some-key2",
772      "description": "NA",
773      "default_value": {
774        "Bool": true
775      },
776      "constraint": null,
777      "stability": "Unstable",
778      "active": true,
779      "display_hint": "None"
780    },
781    "actual_value": {
782      "Bool": true
783    }
784  }
785]"#,
786            json_output
787        );
788    }
789
790    #[test]
791    #[should_panic]
792    fn unstable_option_panics_unless_enabled() {
793        let mut stdout = Vec::new();
794        temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
795            generate_config_internal(
796                &mut stdout,
797                "esp-test",
798                &[ConfigOption {
799                    name: String::from("some-key"),
800                    description: String::from("NA"),
801                    default_value: Value::String("variant-0".to_string()),
802                    constraint: Some(Validator::Enumeration(vec![
803                        "variant-0".to_string(),
804                        "variant-1".to_string(),
805                    ])),
806                    stability: Stability::Unstable,
807                    active: true,
808                    display_hint: DisplayHint::None,
809                }],
810                false,
811            );
812        });
813    }
814
815    #[test]
816    #[should_panic]
817    fn inactive_option_panics() {
818        let mut stdout = Vec::new();
819        temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
820            generate_config_internal(
821                &mut stdout,
822                "esp-test",
823                &[ConfigOption {
824                    name: String::from("some-key"),
825                    description: String::from("NA"),
826                    default_value: Value::String("variant-0".to_string()),
827                    constraint: Some(Validator::Enumeration(vec![
828                        "variant-0".to_string(),
829                        "variant-1".to_string(),
830                    ])),
831                    stability: Stability::Stable(String::from("testing")),
832                    active: false,
833                    display_hint: DisplayHint::None,
834                }],
835                false,
836            );
837        });
838    }
839
840    #[test]
841    fn convenience_constructors() {
842        assert_eq!(
843            ConfigOption {
844                name: String::from("number"),
845                description: String::from("NA"),
846                default_value: Value::Integer(999),
847                constraint: None,
848                stability: Stability::Unstable,
849                active: true,
850                display_hint: DisplayHint::None,
851            },
852            ConfigOption::new("number", "NA", 999)
853        );
854
855        assert_eq!(
856            ConfigOption {
857                name: String::from("string"),
858                description: String::from("descr"),
859                default_value: Value::String("some string".to_string()),
860                constraint: None,
861                stability: Stability::Stable("1.0.0".to_string()),
862                active: true,
863                display_hint: DisplayHint::None,
864            },
865            ConfigOption::new("string", "descr", "some string").stable("1.0.0")
866        );
867
868        assert_eq!(
869            ConfigOption {
870                name: String::from("number"),
871                description: String::from("NA"),
872                default_value: Value::Integer(999),
873                constraint: Some(Validator::PositiveInteger),
874                stability: Stability::Unstable,
875                active: false,
876                display_hint: DisplayHint::None,
877            },
878            ConfigOption::new("number", "NA", 999)
879                .active(false)
880                .constraint(Validator::PositiveInteger)
881        );
882
883        assert_eq!(
884            ConfigOption {
885                name: String::from("number"),
886                description: String::from("NA"),
887                default_value: Value::Integer(999),
888                constraint: Some(Validator::PositiveInteger),
889                stability: Stability::Unstable,
890                active: true,
891                display_hint: DisplayHint::Hex,
892            },
893            ConfigOption::new("number", "NA", 999)
894                .constraint_by(Some(Validator::PositiveInteger))
895                .display_hint(DisplayHint::Hex)
896        );
897    }
898}