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};
5use somni_expr::TypeSet128;
6
7use crate::generate::{validator::Validator, value::Value};
8
9mod markdown;
10pub(crate) mod validator;
11pub(crate) mod value;
12
13/// Configuration errors.
14#[derive(Clone, PartialEq, Eq)]
15pub enum Error {
16    /// Parse errors.
17    Parse(String),
18    /// Validation errors.
19    Validation(String),
20}
21
22impl Error {
23    /// Convenience function for creating parse errors.
24    pub fn parse<S>(message: S) -> Self
25    where
26        S: Into<String>,
27    {
28        Self::Parse(message.into())
29    }
30
31    /// Convenience function for creating validation errors.
32    pub fn validation<S>(message: S) -> Self
33    where
34        S: Into<String>,
35    {
36        Self::Validation(message.into())
37    }
38}
39
40impl fmt::Debug for Error {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{self}")
43    }
44}
45
46impl fmt::Display for Error {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Error::Parse(message) => write!(f, "{message}"),
50            Error::Validation(message) => write!(f, "{message}"),
51        }
52    }
53}
54
55impl std::error::Error for Error {
56    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
57        None
58    }
59
60    fn description(&self) -> &str {
61        "description() is deprecated; use Display"
62    }
63
64    fn cause(&self) -> Option<&dyn core::error::Error> {
65        self.source()
66    }
67}
68
69/// The root node of a configuration.
70#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
71#[serde(deny_unknown_fields)]
72pub struct Config {
73    /// The crate name.
74    #[serde(rename = "crate")]
75    pub krate: String,
76    /// The config options for this crate.
77    pub options: Vec<CfgOption>,
78    /// Optionally additional checks.
79    pub checks: Option<Vec<String>>,
80}
81
82fn true_default() -> String {
83    "true".to_string()
84}
85
86fn unstable_default() -> Stability {
87    Stability::Unstable
88}
89
90/// A default value for a configuration option.
91#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
92#[serde(deny_unknown_fields)]
93pub struct CfgDefaultValue {
94    /// Condition which makes this default value used.
95    /// You can and have to have exactly one active default value.
96    #[serde(rename = "if")]
97    #[serde(default = "true_default")]
98    pub if_: String,
99    /// The default value.
100    pub value: Value,
101}
102
103/// A configuration option.
104#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
105#[serde(deny_unknown_fields)]
106pub struct CfgOption {
107    /// Name of the configuration option
108    pub name: String,
109    /// Description of the configuration option.
110    /// This will be visible in the documentation and in the tooling.
111    pub description: String,
112    /// A condition which specified when this option is active.
113    #[serde(default = "true_default")]
114    pub active: String,
115    /// The default value.
116    /// Exactly one of the items needs to be active at any time.
117    pub default: Vec<CfgDefaultValue>,
118    /// Constraints (Validators) to use.
119    /// If given at most one item is allowed to be active at any time.
120    pub constraints: Option<Vec<CfgConstraint>>,
121    /// A display hint for the value.
122    /// This is meant for tooling and/or documentation.
123    pub display_hint: Option<DisplayHint>,
124    /// The stability guarantees of this option.
125    #[serde(default = "unstable_default")]
126    pub stability: Stability,
127}
128
129/// A conditional constraint / validator for a config option.
130#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
131#[serde(deny_unknown_fields)]
132pub struct CfgConstraint {
133    /// Condition which makes this validator used.
134    #[serde(rename = "if")]
135    #[serde(default = "true_default")]
136    if_: String,
137    /// The validator to be used.
138    #[serde(rename = "type")]
139    type_: Validator,
140}
141
142/// Generate the config from a YAML definition.
143///
144/// After deserializing the config and normalizing it, this will call
145/// [generate_config] to finally get the currently active configuration.
146pub fn generate_config_from_yaml_definition(
147    yaml: &str,
148    enable_unstable: bool,
149    emit_md_tables: bool,
150    chip: Option<esp_metadata_generated::Chip>,
151) -> Result<HashMap<String, Value>, Error> {
152    let features: Vec<String> = env::vars()
153        .filter(|(k, _)| k.starts_with("CARGO_FEATURE_"))
154        .map(|(k, _)| k)
155        .map(|v| {
156            v.strip_prefix("CARGO_FEATURE_")
157                .unwrap_or_default()
158                .to_string()
159        })
160        .collect();
161
162    let (config, options) = evaluate_yaml_config(yaml, chip, features, false)?;
163
164    let cfg = generate_config(&config.krate, &options, enable_unstable, emit_md_tables);
165
166    do_checks(config.checks.as_ref(), &cfg)?;
167
168    Ok(cfg)
169}
170
171/// Check the given actual values by applying checking the given checks
172pub fn do_checks(checks: Option<&Vec<String>>, cfg: &HashMap<String, Value>) -> Result<(), Error> {
173    if let Some(checks) = checks {
174        let mut eval_ctx = somni_expr::Context::<TypeSet128>::new_with_types();
175        for (k, v) in cfg.iter() {
176            match v {
177                Value::Bool(v) => eval_ctx.add_variable(k, *v),
178                Value::Integer(v) => eval_ctx.add_variable(k, *v),
179                Value::String(v) => eval_ctx.add_variable::<&str>(k, v),
180            }
181        }
182        for check in checks {
183            if !eval_ctx
184                .evaluate::<bool>(check)
185                .map_err(|err| Error::Parse(format!("Validation error: {err:?}")))?
186            {
187                return Err(Error::Validation(format!("Validation error: '{check}'")));
188            }
189        }
190    };
191    Ok(())
192}
193
194/// Evaluate the given YAML representation of a config definition.
195pub fn evaluate_yaml_config(
196    yaml: &str,
197    chip: Option<esp_metadata_generated::Chip>,
198    features: Vec<String>,
199    ignore_feature_gates: bool,
200) -> Result<(Config, Vec<ConfigOption>), Error> {
201    let config: Config = serde_yaml::from_str(yaml).map_err(|err| Error::Parse(err.to_string()))?;
202    let mut options = Vec::new();
203    let mut eval_ctx = somni_expr::Context::new();
204    if let Some(chip) = chip {
205        eval_ctx.add_variable("chip", chip.name());
206        eval_ctx.add_variable("ignore_feature_gates", ignore_feature_gates);
207        eval_ctx.add_function("feature", move |feature: &str| chip.contains(feature));
208        eval_ctx.add_function("cargo_feature", |feature: &str| {
209            features.contains(&feature.to_uppercase().replace("-", "_"))
210        });
211    }
212    for option in &config.options {
213        let active = eval_ctx
214            .evaluate::<bool>(&option.active)
215            .map_err(|err| Error::Parse(format!("{err:?}")))?;
216
217        let constraint = {
218            let mut active_constraint = None;
219            if let Some(constraints) = &option.constraints {
220                for constraint in constraints {
221                    if eval_ctx
222                        .evaluate::<bool>(&constraint.if_)
223                        .map_err(|err| Error::Parse(format!("{err:?}")))?
224                    {
225                        active_constraint = Some(constraint.type_.clone());
226                        break;
227                    }
228                }
229            };
230
231            if option.constraints.is_some() && active_constraint.is_none() {
232                panic!(
233                    "No constraint active for crate {}, option {}",
234                    config.krate, option.name
235                );
236            }
237
238            active_constraint
239        };
240
241        let default_value = {
242            let mut default_value = None;
243            for value in &option.default {
244                if eval_ctx
245                    .evaluate::<bool>(&value.if_)
246                    .map_err(|err| Error::Parse(format!("{err:?}")))?
247                {
248                    default_value = Some(value.value.clone());
249                    break;
250                }
251            }
252
253            if default_value.is_none() {
254                panic!(
255                    "No default value active for crate {}, option {}",
256                    config.krate, option.name
257                );
258            }
259
260            default_value
261        };
262
263        let option = ConfigOption {
264            name: option.name.clone(),
265            description: option.description.clone(),
266            default_value: default_value.ok_or_else(|| {
267                Error::Parse(format!("No default value found for {}", option.name))
268            })?,
269            constraint,
270            stability: option.stability.clone(),
271            active,
272            display_hint: option.display_hint.unwrap_or(DisplayHint::None),
273        };
274        options.push(option);
275    }
276    Ok((config, options))
277}
278
279/// Generate and parse config from a prefix, and an array of [ConfigOption].
280///
281/// This function will parse any `SCREAMING_SNAKE_CASE` environment variables
282/// that match the given prefix. It will then attempt to parse the [`Value`] and
283/// run any validators which have been specified.
284///
285/// [`Stability::Unstable`] features will only be enabled if the `unstable`
286/// feature is enabled in the dependant crate. If the `unstable` feature is not
287/// enabled, setting these options will result in a build error.
288///
289/// Once the config has been parsed, this function will emit `snake_case` cfg's
290/// _without_ the prefix which can be used in the dependant crate. After that,
291/// it will create a markdown table in the `OUT_DIR` under the name
292/// `{prefix}_config_table.md` where prefix has also been converted to
293/// `snake_case`. This can be included in crate documentation to outline the
294/// available configuration options for the crate.
295///
296/// Passing a value of true for the `emit_md_tables` argument will create and
297/// write markdown files of the available configuration and selected
298/// configuration which can be included in documentation.
299///
300/// Unknown keys with the supplied prefix will cause this function to panic.
301pub fn generate_config(
302    crate_name: &str,
303    config: &[ConfigOption],
304    enable_unstable: bool,
305    emit_md_tables: bool,
306) -> HashMap<String, Value> {
307    let configs = generate_config_internal(std::io::stdout(), crate_name, config, enable_unstable);
308
309    if emit_md_tables {
310        let file_name = snake_case(crate_name);
311
312        let mut doc_table = markdown::DOC_TABLE_HEADER.replace(
313            "{prefix}",
314            format!("{}_CONFIG_*", screaming_snake_case(crate_name)).as_str(),
315        );
316        let mut selected_config = String::from(markdown::SELECTED_TABLE_HEADER);
317
318        for (name, option, value) in configs.iter() {
319            if !option.active {
320                continue;
321            }
322            markdown::write_doc_table_line(&mut doc_table, name, option);
323            markdown::write_summary_table_line(&mut selected_config, name, value);
324        }
325
326        write_out_file(format!("{file_name}_config_table.md"), doc_table);
327        write_out_file(format!("{file_name}_selected_config.md"), selected_config);
328    }
329
330    // Remove the ConfigOptions from the output
331    configs.into_iter().map(|(k, _, v)| (k, v)).collect()
332}
333
334pub fn generate_config_internal<'a>(
335    mut stdout: impl Write,
336    crate_name: &str,
337    config: &'a [ConfigOption],
338    enable_unstable: bool,
339) -> Vec<(String, &'a ConfigOption, Value)> {
340    // Only rebuild if `build.rs` changed. Otherwise, Cargo will rebuild if any
341    // other file changed.
342    writeln!(stdout, "cargo:rerun-if-changed=build.rs").ok();
343
344    // Ensure that the prefix is `SCREAMING_SNAKE_CASE`:
345    let prefix = format!("{}_CONFIG_", screaming_snake_case(crate_name));
346
347    let mut configs = create_config(&prefix, config);
348    capture_from_env(crate_name, &prefix, &mut configs, enable_unstable);
349
350    for (_, option, value) in configs.iter() {
351        if let Some(ref validator) = option.constraint {
352            validator.validate(value).unwrap();
353        }
354    }
355
356    emit_configuration(&mut stdout, &configs);
357
358    configs
359}
360
361/// The stability of the configuration option.
362#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
363pub enum Stability {
364    /// Unstable options need to be activated with the `unstable` feature
365    /// of the package that defines them.
366    Unstable,
367    /// Stable options contain the first version in which they were
368    /// stabilized.
369    Stable(String),
370}
371
372impl Display for Stability {
373    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374        match self {
375            Stability::Unstable => write!(f, "⚠️ Unstable"),
376            Stability::Stable(version) => write!(f, "Stable since {version}"),
377        }
378    }
379}
380
381/// A display hint (for tooling only)
382#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
383pub enum DisplayHint {
384    /// No display hint
385    None,
386
387    /// Use a binary representation
388    Binary,
389
390    /// Use a hexadecimal representation
391    Hex,
392
393    /// Use a octal representation
394    Octal,
395}
396
397impl DisplayHint {
398    /// Converts a [Value] to String applying the correct display hint.
399    pub fn format_value(self, value: &Value) -> String {
400        match value {
401            Value::Bool(b) => b.to_string(),
402            Value::Integer(i) => match self {
403                DisplayHint::None => format!("{i}"),
404                DisplayHint::Binary => format!("0b{i:0b}"),
405                DisplayHint::Hex => format!("0x{i:X}"),
406                DisplayHint::Octal => format!("0o{i:o}"),
407            },
408            Value::String(s) => s.clone(),
409        }
410    }
411}
412
413/// A configuration option.
414#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
415pub struct ConfigOption {
416    /// The name of the configuration option.
417    ///
418    /// The associated environment variable has the format of
419    /// `<PREFIX>_CONFIG_<NAME>`.
420    pub name: String,
421
422    /// The description of the configuration option.
423    ///
424    /// The description will be included in the generated markdown
425    /// documentation.
426    pub description: String,
427
428    /// The default value of the configuration option.
429    pub default_value: Value,
430
431    /// An optional validator for the configuration option.
432    pub constraint: Option<Validator>,
433
434    /// The stability of the configuration option.
435    pub stability: Stability,
436
437    /// Whether the config option should be offered to the user.
438    ///
439    /// Inactive options are not included in the documentation, and accessing
440    /// them provides the default value.
441    pub active: bool,
442
443    /// A display hint (for tooling)
444    pub display_hint: DisplayHint,
445}
446
447impl ConfigOption {
448    /// Get the corresponding ENV_VAR name given the crate-name
449    pub fn full_env_var(&self, crate_name: &str) -> String {
450        self.env_var(&format!("{}_CONFIG_", screaming_snake_case(crate_name)))
451    }
452
453    fn env_var(&self, prefix: &str) -> String {
454        format!("{}{}", prefix, screaming_snake_case(&self.name))
455    }
456
457    fn cfg_name(&self) -> String {
458        snake_case(&self.name)
459    }
460
461    fn is_stable(&self) -> bool {
462        matches!(self.stability, Stability::Stable(_))
463    }
464}
465
466fn create_config<'a>(
467    prefix: &str,
468    config: &'a [ConfigOption],
469) -> Vec<(String, &'a ConfigOption, Value)> {
470    let mut configs = Vec::with_capacity(config.len());
471
472    for option in config {
473        configs.push((option.env_var(prefix), option, option.default_value.clone()));
474    }
475
476    configs
477}
478
479fn capture_from_env(
480    crate_name: &str,
481    prefix: &str,
482    configs: &mut Vec<(String, &ConfigOption, Value)>,
483    enable_unstable: bool,
484) {
485    let mut unknown = Vec::new();
486    let mut failed = Vec::new();
487    let mut unstable = Vec::new();
488
489    // Try and capture input from the environment:
490    for (var, value) in env::vars() {
491        if var.starts_with(prefix) {
492            let Some((_, option, cfg)) = configs.iter_mut().find(|(k, _, _)| k == &var) else {
493                unknown.push(var);
494                continue;
495            };
496
497            if !option.active {
498                unknown.push(var);
499                continue;
500            }
501
502            if !enable_unstable && !option.is_stable() {
503                unstable.push(var);
504                continue;
505            }
506
507            if let Err(e) = cfg.parse_in_place(&value) {
508                failed.push(format!("{var}: {e}"));
509            }
510        }
511    }
512
513    if !failed.is_empty() {
514        panic!("Invalid configuration options detected: {failed:?}");
515    }
516
517    if !unstable.is_empty() {
518        panic!(
519            "The following configuration options are unstable: {unstable:?}. You can enable it by \
520            activating the 'unstable' feature in {crate_name}."
521        );
522    }
523
524    if !unknown.is_empty() {
525        panic!("Unknown configuration options detected: {unknown:?}");
526    }
527}
528
529fn emit_configuration(mut stdout: impl Write, configs: &[(String, &ConfigOption, Value)]) {
530    for (env_var_name, option, value) in configs.iter() {
531        let cfg_name = option.cfg_name();
532
533        // Output the raw configuration as an env var. Values that haven't been seen
534        // will be output here with the default value. Also trigger a rebuild if config
535        // environment variable changed.
536        writeln!(stdout, "cargo:rustc-env={env_var_name}={value}").ok();
537        writeln!(stdout, "cargo:rerun-if-env-changed={env_var_name}").ok();
538
539        // Emit known config symbol:
540        writeln!(stdout, "cargo:rustc-check-cfg=cfg({cfg_name})").ok();
541
542        // Emit specially-handled values:
543        if let Value::Bool(true) = value {
544            writeln!(stdout, "cargo:rustc-cfg={cfg_name}").ok();
545        }
546
547        // Emit extra symbols based on the validator (e.g. enumerated values):
548        if let Some(validator) = option.constraint.as_ref() {
549            validator.emit_cargo_extras(&mut stdout, &cfg_name, value);
550        }
551    }
552}
553
554fn write_out_file(file_name: String, json: String) {
555    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
556    let out_file = out_dir.join(file_name);
557    fs::write(out_file, json).unwrap();
558}
559
560fn snake_case(name: &str) -> String {
561    let mut name = name.replace("-", "_");
562    name.make_ascii_lowercase();
563
564    name
565}
566
567fn screaming_snake_case(name: &str) -> String {
568    let mut name = name.replace("-", "_");
569    name.make_ascii_uppercase();
570
571    name
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577    use crate::generate::{validator::Validator, value::Value};
578
579    #[test]
580    fn value_number_formats() {
581        const INPUTS: &[&str] = &["0xAA", "0o252", "0b0000000010101010", "170"];
582        let mut v = Value::Integer(0);
583
584        for input in INPUTS {
585            v.parse_in_place(input).unwrap();
586            // no matter the input format, the output format should be decimal
587            assert_eq!(v.to_string(), "170");
588        }
589    }
590
591    #[test]
592    fn value_bool_inputs() {
593        let mut v = Value::Bool(false);
594
595        v.parse_in_place("true").unwrap();
596        assert_eq!(v.to_string(), "true");
597
598        v.parse_in_place("false").unwrap();
599        assert_eq!(v.to_string(), "false");
600
601        v.parse_in_place("else")
602            .expect_err("Only true or false are valid");
603    }
604
605    #[test]
606    fn env_override() {
607        temp_env::with_vars(
608            [
609                ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
610                ("ESP_TEST_CONFIG_NUMBER_SIGNED", Some("-999")),
611                ("ESP_TEST_CONFIG_STRING", Some("Hello world!")),
612                ("ESP_TEST_CONFIG_BOOL", Some("true")),
613            ],
614            || {
615                let configs = generate_config(
616                    "esp-test",
617                    &[
618                        ConfigOption {
619                            name: String::from("number"),
620                            description: String::from("NA"),
621                            default_value: Value::Integer(999),
622                            constraint: None,
623                            stability: Stability::Stable(String::from("testing")),
624                            active: true,
625                            display_hint: DisplayHint::None,
626                        },
627                        ConfigOption {
628                            name: String::from("number_signed"),
629                            description: String::from("NA"),
630                            default_value: Value::Integer(-777),
631                            constraint: None,
632                            stability: Stability::Stable(String::from("testing")),
633                            active: true,
634                            display_hint: DisplayHint::None,
635                        },
636                        ConfigOption {
637                            name: String::from("string"),
638                            description: String::from("NA"),
639                            default_value: Value::String("Demo".to_string()),
640                            constraint: None,
641                            stability: Stability::Stable(String::from("testing")),
642                            active: true,
643                            display_hint: DisplayHint::None,
644                        },
645                        ConfigOption {
646                            name: String::from("bool"),
647                            description: String::from("NA"),
648                            default_value: Value::Bool(false),
649                            constraint: None,
650                            stability: Stability::Stable(String::from("testing")),
651                            active: true,
652                            display_hint: DisplayHint::None,
653                        },
654                        ConfigOption {
655                            name: String::from("number_default"),
656                            description: String::from("NA"),
657                            default_value: Value::Integer(999),
658                            constraint: None,
659                            stability: Stability::Stable(String::from("testing")),
660                            active: true,
661                            display_hint: DisplayHint::None,
662                        },
663                        ConfigOption {
664                            name: String::from("string_default"),
665                            description: String::from("NA"),
666                            default_value: Value::String("Demo".to_string()),
667                            constraint: None,
668                            stability: Stability::Stable(String::from("testing")),
669                            active: true,
670                            display_hint: DisplayHint::None,
671                        },
672                        ConfigOption {
673                            name: String::from("bool_default"),
674                            description: String::from("NA"),
675                            default_value: Value::Bool(false),
676                            constraint: None,
677                            stability: Stability::Stable(String::from("testing")),
678                            active: true,
679                            display_hint: DisplayHint::None,
680                        },
681                    ],
682                    false,
683                    false,
684                );
685
686                // some values have changed
687                assert_eq!(configs["ESP_TEST_CONFIG_NUMBER"], Value::Integer(0xaa));
688                assert_eq!(
689                    configs["ESP_TEST_CONFIG_NUMBER_SIGNED"],
690                    Value::Integer(-999)
691                );
692                assert_eq!(
693                    configs["ESP_TEST_CONFIG_STRING"],
694                    Value::String("Hello world!".to_string())
695                );
696                assert_eq!(configs["ESP_TEST_CONFIG_BOOL"], Value::Bool(true));
697
698                // the rest are the defaults
699                assert_eq!(
700                    configs["ESP_TEST_CONFIG_NUMBER_DEFAULT"],
701                    Value::Integer(999)
702                );
703                assert_eq!(
704                    configs["ESP_TEST_CONFIG_STRING_DEFAULT"],
705                    Value::String("Demo".to_string())
706                );
707                assert_eq!(configs["ESP_TEST_CONFIG_BOOL_DEFAULT"], Value::Bool(false));
708            },
709        )
710    }
711
712    #[test]
713    fn builtin_validation_passes() {
714        temp_env::with_vars(
715            [
716                ("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("7")),
717                ("ESP_TEST_CONFIG_NEGATIVE_NUMBER", Some("-1")),
718                ("ESP_TEST_CONFIG_NON_NEGATIVE_NUMBER", Some("0")),
719                ("ESP_TEST_CONFIG_RANGE", Some("9")),
720            ],
721            || {
722                generate_config(
723                    "esp-test",
724                    &[
725                        ConfigOption {
726                            name: String::from("positive_number"),
727                            description: String::from("NA"),
728                            default_value: Value::Integer(-1),
729                            constraint: Some(Validator::PositiveInteger),
730                            stability: Stability::Stable(String::from("testing")),
731                            active: true,
732                            display_hint: DisplayHint::None,
733                        },
734                        ConfigOption {
735                            name: String::from("negative_number"),
736                            description: String::from("NA"),
737                            default_value: Value::Integer(1),
738                            constraint: Some(Validator::NegativeInteger),
739                            stability: Stability::Stable(String::from("testing")),
740                            active: true,
741                            display_hint: DisplayHint::None,
742                        },
743                        ConfigOption {
744                            name: String::from("non_negative_number"),
745                            description: String::from("NA"),
746                            default_value: Value::Integer(-1),
747                            constraint: Some(Validator::NonNegativeInteger),
748                            stability: Stability::Stable(String::from("testing")),
749                            active: true,
750                            display_hint: DisplayHint::None,
751                        },
752                        ConfigOption {
753                            name: String::from("range"),
754                            description: String::from("NA"),
755                            default_value: Value::Integer(0),
756                            constraint: Some(Validator::IntegerInRange(5..10)),
757                            stability: Stability::Stable(String::from("testing")),
758                            active: true,
759                            display_hint: DisplayHint::None,
760                        },
761                    ],
762                    false,
763                    false,
764                )
765            },
766        );
767    }
768
769    #[test]
770    #[should_panic]
771    fn builtin_validation_bails() {
772        temp_env::with_vars([("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("-99"))], || {
773            generate_config(
774                "esp-test",
775                &[ConfigOption {
776                    name: String::from("positive_number"),
777                    description: String::from("NA"),
778                    default_value: Value::Integer(-1),
779                    constraint: Some(Validator::PositiveInteger),
780                    stability: Stability::Stable(String::from("testing")),
781                    active: true,
782                    display_hint: DisplayHint::None,
783                }],
784                false,
785                false,
786            )
787        });
788    }
789
790    #[test]
791    #[should_panic]
792    fn env_unknown_bails() {
793        temp_env::with_vars(
794            [
795                ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
796                ("ESP_TEST_CONFIG_RANDOM_VARIABLE", Some("")),
797            ],
798            || {
799                generate_config(
800                    "esp-test",
801                    &[ConfigOption {
802                        name: String::from("number"),
803                        description: String::from("NA"),
804                        default_value: Value::Integer(999),
805                        constraint: None,
806                        stability: Stability::Stable(String::from("testing")),
807                        active: true,
808                        display_hint: DisplayHint::None,
809                    }],
810                    false,
811                    false,
812                );
813            },
814        );
815    }
816
817    #[test]
818    #[should_panic]
819    fn env_invalid_values_bails() {
820        temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("Hello world"))], || {
821            generate_config(
822                "esp-test",
823                &[ConfigOption {
824                    name: String::from("number"),
825                    description: String::from("NA"),
826                    default_value: Value::Integer(999),
827                    constraint: None,
828                    stability: Stability::Stable(String::from("testing")),
829                    active: true,
830                    display_hint: DisplayHint::None,
831                }],
832                false,
833                false,
834            );
835        });
836    }
837
838    #[test]
839    fn env_unknown_prefix_is_ignored() {
840        temp_env::with_vars(
841            [("ESP_TEST_OTHER_CONFIG_NUMBER", Some("Hello world"))],
842            || {
843                generate_config(
844                    "esp-test",
845                    &[ConfigOption {
846                        name: String::from("number"),
847                        description: String::from("NA"),
848                        default_value: Value::Integer(999),
849                        constraint: None,
850                        stability: Stability::Stable(String::from("testing")),
851                        active: true,
852                        display_hint: DisplayHint::None,
853                    }],
854                    false,
855                    false,
856                );
857            },
858        );
859    }
860
861    #[test]
862    fn enumeration_validator() {
863        let mut stdout = Vec::new();
864        temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
865            generate_config_internal(
866                &mut stdout,
867                "esp-test",
868                &[ConfigOption {
869                    name: String::from("some-key"),
870                    description: String::from("NA"),
871                    default_value: Value::String("variant-0".to_string()),
872                    constraint: Some(Validator::Enumeration(vec![
873                        "variant-0".to_string(),
874                        "variant-1".to_string(),
875                    ])),
876                    stability: Stability::Stable(String::from("testing")),
877                    active: true,
878                    display_hint: DisplayHint::None,
879                }],
880                false,
881            );
882        });
883
884        let cargo_lines: Vec<&str> = std::str::from_utf8(&stdout).unwrap().lines().collect();
885        assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key)"));
886        assert!(cargo_lines.contains(&"cargo:rustc-env=ESP_TEST_CONFIG_SOME_KEY=variant-0"));
887        assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_0)"));
888        assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_1)"));
889        assert!(cargo_lines.contains(&"cargo:rustc-cfg=some_key_variant_0"));
890    }
891
892    #[test]
893    #[should_panic]
894    fn unstable_option_panics_unless_enabled() {
895        let mut stdout = Vec::new();
896        temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
897            generate_config_internal(
898                &mut stdout,
899                "esp-test",
900                &[ConfigOption {
901                    name: String::from("some-key"),
902                    description: String::from("NA"),
903                    default_value: Value::String("variant-0".to_string()),
904                    constraint: Some(Validator::Enumeration(vec![
905                        "variant-0".to_string(),
906                        "variant-1".to_string(),
907                    ])),
908                    stability: Stability::Unstable,
909                    active: true,
910                    display_hint: DisplayHint::None,
911                }],
912                false,
913            );
914        });
915    }
916
917    #[test]
918    #[should_panic]
919    fn inactive_option_panics() {
920        let mut stdout = Vec::new();
921        temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
922            generate_config_internal(
923                &mut stdout,
924                "esp-test",
925                &[ConfigOption {
926                    name: String::from("some-key"),
927                    description: String::from("NA"),
928                    default_value: Value::String("variant-0".to_string()),
929                    constraint: Some(Validator::Enumeration(vec![
930                        "variant-0".to_string(),
931                        "variant-1".to_string(),
932                    ])),
933                    stability: Stability::Stable(String::from("testing")),
934                    active: false,
935                    display_hint: DisplayHint::None,
936                }],
937                false,
938            );
939        });
940    }
941
942    #[test]
943    fn deserialization() {
944        let yml = r#"
945crate: esp-bootloader-esp-idf
946
947options:
948- name: mmu_page_size
949  description: ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.
950  default:
951    - value: '"64k"'
952  stability: !Stable xxxx
953  constraints:
954  - if: true
955    type:
956      validator: enumeration
957      value:
958      - 8k
959      - 16k
960      - 32k
961      - 64k
962
963- name: esp_idf_version
964  description: ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.
965  default:
966    - if: 'chip == "esp32c6"'
967      value: '"esp32c6"'
968    - if: 'chip == "esp32"'
969      value: '"other"'
970  active: true
971
972- name: partition-table-offset
973  description: "The address of partition table (by default 0x8000). Allows you to \
974    move the partition table, it gives more space for the bootloader. Note that the \
975    bootloader and app will both need to be compiled with the same \
976    PARTITION_TABLE_OFFSET value."
977  default:
978    - if: true
979      value: 32768
980  stability: Unstable
981  active: 'chip == "esp32c6"'
982"#;
983
984        let (cfg, options) = evaluate_yaml_config(
985            yml,
986            Some(esp_metadata_generated::Chip::Esp32c6),
987            vec![],
988            false,
989        )
990        .unwrap();
991
992        assert_eq!("esp-bootloader-esp-idf", cfg.krate);
993
994        assert_eq!(
995            vec![
996                    ConfigOption {
997                        name: "mmu_page_size".to_string(),
998                        description: "ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.".to_string(),
999                        default_value: Value::String("64k".to_string()),
1000                        constraint: Some(
1001                            Validator::Enumeration(
1002                                vec![
1003                                    "8k".to_string(),
1004                                    "16k".to_string(),
1005                                    "32k".to_string(),
1006                                    "64k".to_string(),
1007                                ],
1008                            ),
1009                        ),
1010                        stability: Stability::Stable("xxxx".to_string()),
1011                        active: true,
1012                        display_hint: DisplayHint::None,
1013                    },
1014                    ConfigOption {
1015                        name: "esp_idf_version".to_string(),
1016                        description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.".to_string(),
1017                        default_value: Value::String("esp32c6".to_string()),
1018                        constraint: None,
1019                        stability: Stability::Unstable,
1020                        active: true,
1021                        display_hint: DisplayHint::None,
1022                    },
1023                    ConfigOption {
1024                        name: "partition-table-offset".to_string(),
1025                        description: "The address of partition table (by default 0x8000). Allows you to move the partition table, it gives more space for the bootloader. Note that the bootloader and app will both need to be compiled with the same PARTITION_TABLE_OFFSET value.".to_string(),
1026                        default_value: Value::Integer(32768),
1027                        constraint: None,
1028                        stability: Stability::Unstable,
1029                        active: true,
1030                        display_hint: DisplayHint::None,
1031                    },
1032            ],
1033            options
1034        );
1035    }
1036
1037    #[test]
1038    fn deserialization_fallback_default() {
1039        let yml = r#"
1040crate: esp-bootloader-esp-idf
1041
1042options:
1043- name: esp_idf_version
1044  description: ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.
1045  default:
1046    - if: 'chip == "esp32c6"'
1047      value: '"esp32c6"'
1048    - if: 'chip == "esp32"'
1049      value: '"other"'
1050    - value: '"default"'
1051  active: true
1052"#;
1053
1054        let (cfg, options) = evaluate_yaml_config(
1055            yml,
1056            Some(esp_metadata_generated::Chip::Esp32c3),
1057            vec![],
1058            false,
1059        )
1060        .unwrap();
1061
1062        assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1063
1064        assert_eq!(
1065            vec![
1066                    ConfigOption {
1067                        name: "esp_idf_version".to_string(),
1068                        description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.".to_string(),
1069                        default_value: Value::String("default".to_string()),
1070                        constraint: None,
1071                        stability: Stability::Unstable,
1072                        active: true,
1073                        display_hint: DisplayHint::None,
1074                    },
1075            ],
1076            options
1077        );
1078    }
1079
1080    #[test]
1081    fn deserialization_fallback_contraint() {
1082        let yml = r#"
1083crate: esp-bootloader-esp-idf
1084
1085options:
1086- name: option
1087  description: Desc
1088  default:
1089    - value: 100
1090  constraints:
1091    - if: 'chip == "esp32c6"'
1092      type:
1093        validator: integer_in_range
1094        value:
1095          start: 0
1096          end: 100
1097    - if: true
1098      type:
1099        validator: integer_in_range
1100        value:
1101          start: 0
1102          end: 50
1103  active: true
1104"#;
1105
1106        let (cfg, options) = evaluate_yaml_config(
1107            yml,
1108            Some(esp_metadata_generated::Chip::Esp32),
1109            vec![],
1110            false,
1111        )
1112        .unwrap();
1113
1114        assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1115
1116        assert_eq!(
1117            vec![ConfigOption {
1118                name: "option".to_string(),
1119                description: "Desc".to_string(),
1120                default_value: Value::Integer(100),
1121                constraint: Some(Validator::IntegerInRange(0..50)),
1122                stability: Stability::Unstable,
1123                active: true,
1124                display_hint: DisplayHint::None,
1125            },],
1126            options
1127        );
1128    }
1129}