Skip to main content

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