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