esp_config/generate/
mod.rs

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