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