mirror of
https://github.com/esp-rs/esp-hal.git
synced 2025-09-27 20:30:35 +00:00
Refactor esp-config (#3362)
* Extract value and validator modules * Clean up * Replace tuples with ConfigOption * Move generate.rs * Further refactor write_doc_table_line * Carry around the config option, too * Move markdown table processing out of random places * Compare prettified json, use pretty_assertions to diff it * Work with Vec * Emit enumerated cfgs in one place
This commit is contained in:
parent
353950dbdf
commit
2aac175f77
@ -1,5 +1,5 @@
|
||||
use esp_build::assert_unique_used_features;
|
||||
use esp_config::{generate_config, Value};
|
||||
use esp_config::{generate_config, ConfigOption, Value};
|
||||
|
||||
fn main() {
|
||||
// Ensure that only a single chip is specified:
|
||||
@ -17,12 +17,12 @@ fn main() {
|
||||
// emit config
|
||||
generate_config(
|
||||
"esp_backtrace",
|
||||
&[(
|
||||
"backtrace-frames",
|
||||
"The maximum number of frames that will be printed in a backtrace",
|
||||
Value::Integer(10),
|
||||
None,
|
||||
)],
|
||||
&[ConfigOption {
|
||||
name: "backtrace-frames",
|
||||
description: "The maximum number of frames that will be printed in a backtrace",
|
||||
default_value: Value::Integer(10),
|
||||
constraint: None,
|
||||
}],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::env;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use esp_config::{generate_config, Validator, Value};
|
||||
use esp_config::{generate_config, ConfigOption, Validator, Value};
|
||||
|
||||
fn main() {
|
||||
let build_time = match env::var("SOURCE_DATE_EPOCH") {
|
||||
@ -17,25 +17,25 @@ fn main() {
|
||||
|
||||
// emit config
|
||||
generate_config("esp-bootloader-esp-idf", &[
|
||||
(
|
||||
"mmu_page_size",
|
||||
"ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.",
|
||||
Value::String(String::from("64k")),
|
||||
Some(Validator::Enumeration(
|
||||
ConfigOption {
|
||||
name: "mmu_page_size",
|
||||
description: "ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.",
|
||||
default_value: Value::String(String::from("64k")),
|
||||
constraint: Some(Validator::Enumeration(
|
||||
vec![String::from("8k"), String::from("16k"),String::from("32k"),String::from("64k"),]
|
||||
))
|
||||
),
|
||||
(
|
||||
"esp_idf_version",
|
||||
"ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.",
|
||||
Value::String(String::from("0.0.0")),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"partition-table-offset",
|
||||
"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.",
|
||||
Value::Integer(0x8000),
|
||||
Some(Validator::PositiveInteger),
|
||||
)
|
||||
},
|
||||
ConfigOption {
|
||||
name: "esp_idf_version",
|
||||
description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.",
|
||||
default_value: Value::String(String::from("0.0.0")),
|
||||
constraint: None,
|
||||
},
|
||||
ConfigOption {
|
||||
name: "partition-table-offset",
|
||||
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.",
|
||||
default_value: Value::Integer(0x8000),
|
||||
constraint: Some(Validator::PositiveInteger),
|
||||
}
|
||||
], true);
|
||||
}
|
||||
|
@ -9,8 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- `ConfigOption` struct (#3362)
|
||||
|
||||
### Changed
|
||||
|
||||
- `generate_config` now takes a slice of `ConfigOption`s instead of tuples. (#3362)
|
||||
|
||||
### Fixed
|
||||
|
||||
### Removed
|
||||
|
@ -15,6 +15,7 @@ serde_json = { version = "1.0.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
temp-env = "0.3.6"
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
[features]
|
||||
## Enable the generation and parsing of a config
|
||||
|
@ -1,892 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
fmt::{self, Write as _},
|
||||
fs,
|
||||
io::Write,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
const DOC_TABLE_HEADER: &str = r#"
|
||||
| Name | Description | Default value | Allowed value |
|
||||
|------|-------------|---------------|---------------|
|
||||
"#;
|
||||
|
||||
const SELECTED_TABLE_HEADER: &str = r#"
|
||||
| Name | Selected value |
|
||||
|------|----------------|
|
||||
"#;
|
||||
|
||||
/// Configuration errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// Parse errors.
|
||||
Parse(String),
|
||||
/// Validation errors.
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Convenience function for creating parse errors.
|
||||
pub fn parse<S>(message: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Self::Parse(message.into())
|
||||
}
|
||||
|
||||
/// Convenience function for creating validation errors.
|
||||
pub fn validation<S>(message: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Self::Validation(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Parse(message) => write!(f, "{message}"),
|
||||
Error::Validation(message) => write!(f, "{message}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported configuration value types.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub enum Value {
|
||||
/// Booleans.
|
||||
Bool(bool),
|
||||
/// Integers.
|
||||
Integer(i128),
|
||||
/// Strings.
|
||||
String(String),
|
||||
}
|
||||
|
||||
// TODO: Do we want to handle negative values for non-decimal values?
|
||||
impl Value {
|
||||
fn parse_in_place(&mut self, s: &str) -> Result<(), Error> {
|
||||
*self = match self {
|
||||
Value::Bool(_) => match s {
|
||||
"true" => Value::Bool(true),
|
||||
"false" => Value::Bool(false),
|
||||
_ => {
|
||||
return Err(Error::parse(format!(
|
||||
"Expected 'true' or 'false', found: '{s}'"
|
||||
)))
|
||||
}
|
||||
},
|
||||
Value::Integer(_) => {
|
||||
let inner = match s.as_bytes() {
|
||||
[b'0', b'x', ..] => i128::from_str_radix(&s[2..], 16),
|
||||
[b'0', b'o', ..] => i128::from_str_radix(&s[2..], 8),
|
||||
[b'0', b'b', ..] => i128::from_str_radix(&s[2..], 2),
|
||||
_ => i128::from_str_radix(&s, 10),
|
||||
}
|
||||
.map_err(|_| Error::parse(format!("Expected valid intger value, found: '{s}'")))?;
|
||||
|
||||
Value::Integer(inner)
|
||||
}
|
||||
Value::String(_) => Value::String(s.into()),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert the value to a [bool].
|
||||
pub fn as_bool(&self) -> bool {
|
||||
match self {
|
||||
Value::Bool(value) => *value,
|
||||
_ => panic!("attempted to convert non-bool value to a bool"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the value to an [i128].
|
||||
pub fn as_integer(&self) -> i128 {
|
||||
match self {
|
||||
Value::Integer(value) => *value,
|
||||
_ => panic!("attempted to convert non-integer value to an integer"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the value to a [String].
|
||||
pub fn as_string(&self) -> String {
|
||||
match self {
|
||||
Value::String(value) => value.to_owned(),
|
||||
_ => panic!("attempted to convert non-string value to a string"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the value a bool?
|
||||
pub fn is_bool(&self) -> bool {
|
||||
matches!(self, Value::Bool(_))
|
||||
}
|
||||
|
||||
/// Is the value an integer?
|
||||
pub fn is_integer(&self) -> bool {
|
||||
matches!(self, Value::Integer(_))
|
||||
}
|
||||
|
||||
/// Is the value a string?
|
||||
pub fn is_string(&self) -> bool {
|
||||
matches!(self, Value::String(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Value::Bool(b) => write!(f, "{b}"),
|
||||
Value::Integer(i) => write!(f, "{i}"),
|
||||
Value::String(s) => write!(f, "{s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration value validation functions.
|
||||
#[derive(Serialize)]
|
||||
pub enum Validator {
|
||||
/// Only allow negative integers, i.e. any values less than 0.
|
||||
NegativeInteger,
|
||||
/// Only allow non-negative integers, i.e. any values greater than or equal
|
||||
/// to 0.
|
||||
NonNegativeInteger,
|
||||
/// Only allow positive integers, i.e. any values greater than to 0.
|
||||
PositiveInteger,
|
||||
/// Ensure that an integer value falls within the specified range.
|
||||
IntegerInRange(Range<i128>),
|
||||
/// String-Enumeration. Only allows one of the given Strings.
|
||||
Enumeration(Vec<String>),
|
||||
/// A custom validation function to run against any supported value type.
|
||||
#[serde(serialize_with = "serialize_custom")]
|
||||
#[serde(untagged)]
|
||||
Custom(Box<dyn Fn(&Value) -> Result<(), Error>>),
|
||||
}
|
||||
|
||||
fn serialize_custom<S>(
|
||||
_: &Box<dyn Fn(&Value) -> Result<(), Error>>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str("Custom")
|
||||
}
|
||||
|
||||
impl Validator {
|
||||
fn validate(&self, value: &Value) -> Result<(), Error> {
|
||||
match self {
|
||||
Validator::NegativeInteger => negative_integer(value)?,
|
||||
Validator::NonNegativeInteger => non_negative_integer(value)?,
|
||||
Validator::PositiveInteger => positive_integer(value)?,
|
||||
Validator::IntegerInRange(range) => integer_in_range(range, value)?,
|
||||
Validator::Enumeration(values) => enumeration(values, value)?,
|
||||
Validator::Custom(validator_fn) => validator_fn(value)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn description(&self) -> Option<String> {
|
||||
match self {
|
||||
Validator::NegativeInteger => Some(String::from("Negative integer")),
|
||||
Validator::NonNegativeInteger => Some(String::from("Positive integer or 0")),
|
||||
Validator::PositiveInteger => Some(String::from("Positive integer")),
|
||||
Validator::IntegerInRange(range) => {
|
||||
Some(format!("Integer in range {}..{}", range.start, range.end))
|
||||
}
|
||||
Validator::Enumeration(values) => Some(format!("Any of {:?}", values)),
|
||||
Validator::Custom(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_cargo_extras<W: Write>(&self, stdout: &mut W, config_key: &str) {
|
||||
match self {
|
||||
Validator::Enumeration(values) => {
|
||||
let config_key = snake_case(config_key);
|
||||
for value in values {
|
||||
writeln!(
|
||||
stdout,
|
||||
"cargo:rustc-check-cfg=cfg({config_key}_{})",
|
||||
snake_case(value)
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enumeration(values: &Vec<String>, value: &Value) -> Result<(), Error> {
|
||||
if let Value::String(value) = value {
|
||||
if !values.contains(value) {
|
||||
return Err(Error::validation(format!(
|
||||
"Expected one of {:?}, found '{}'",
|
||||
values, value
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
return Err(Error::parse(
|
||||
"Validator::Enumeration can only be used with string values",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn negative_integer(value: &Value) -> Result<(), Error> {
|
||||
if !value.is_integer() {
|
||||
return Err(Error::validation(
|
||||
"Validator::NegativeInteger can only be used with integer values",
|
||||
));
|
||||
} else if value.as_integer() >= 0 {
|
||||
return Err(Error::validation(format!(
|
||||
"Expected negative integer, found '{}'",
|
||||
value.as_integer()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn non_negative_integer(value: &Value) -> Result<(), Error> {
|
||||
if !value.is_integer() {
|
||||
return Err(Error::validation(
|
||||
"Validator::NonNegativeInteger can only be used with integer values",
|
||||
));
|
||||
} else if value.as_integer() < 0 {
|
||||
return Err(Error::validation(format!(
|
||||
"Expected non-negative integer, found '{}'",
|
||||
value.as_integer()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn positive_integer(value: &Value) -> Result<(), Error> {
|
||||
if !value.is_integer() {
|
||||
return Err(Error::validation(
|
||||
"Validator::PositiveInteger can only be used with integer values",
|
||||
));
|
||||
} else if value.as_integer() <= 0 {
|
||||
return Err(Error::validation(format!(
|
||||
"Expected positive integer, found '{}'",
|
||||
value.as_integer()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn integer_in_range(range: &Range<i128>, value: &Value) -> Result<(), Error> {
|
||||
if !value.is_integer() || !range.contains(&value.as_integer()) {
|
||||
Err(Error::validation(format!(
|
||||
"Value '{}' does not fall within range '{:?}'",
|
||||
value, range
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate and parse config from a prefix, and an array tuples containing the
|
||||
/// name, description, default value, and an optional validator.
|
||||
///
|
||||
/// This function will parse any `SCREAMING_SNAKE_CASE` environment variables
|
||||
/// that match the given prefix. It will then attempt to parse the [`Value`] and
|
||||
/// run any validators which have been specified.
|
||||
///
|
||||
/// Once the config has been parsed, this function will emit `snake_case` cfg's
|
||||
/// _without_ the prefix which can be used in the dependant crate. After that,
|
||||
/// it will create a markdown table in the `OUT_DIR` under the name
|
||||
/// `{prefix}_config_table.md` where prefix has also been converted to
|
||||
/// `snake_case`. This can be included in crate documentation to outline the
|
||||
/// available configuration options for the crate.
|
||||
///
|
||||
/// Passing a value of true for the `emit_md_tables` argument will create and
|
||||
/// write markdown files of the available configuration and selected
|
||||
/// configuration which can be included in documentation.
|
||||
///
|
||||
/// Unknown keys with the supplied prefix will cause this function to panic.
|
||||
pub fn generate_config(
|
||||
crate_name: &str,
|
||||
config: &[(&str, &str, Value, Option<Validator>)],
|
||||
emit_md_tables: bool,
|
||||
) -> HashMap<String, Value> {
|
||||
generate_config_internal(&mut std::io::stdout(), crate_name, config, emit_md_tables)
|
||||
}
|
||||
|
||||
pub fn generate_config_internal<W: Write>(
|
||||
stdout: &mut W,
|
||||
crate_name: &str,
|
||||
config: &[(&str, &str, Value, Option<Validator>)],
|
||||
emit_md_tables: bool,
|
||||
) -> HashMap<String, Value> {
|
||||
// Only rebuild if `build.rs` changed. Otherwise, Cargo will rebuild if any
|
||||
// other file changed.
|
||||
writeln!(stdout, "cargo:rerun-if-changed=build.rs").ok();
|
||||
|
||||
#[cfg(not(test))]
|
||||
env_change_work_around(stdout);
|
||||
|
||||
let mut doc_table = String::from(DOC_TABLE_HEADER);
|
||||
let mut selected_config = String::from(SELECTED_TABLE_HEADER);
|
||||
|
||||
// Ensure that the prefix is `SCREAMING_SNAKE_CASE`:
|
||||
let prefix = format!("{}_CONFIG_", screaming_snake_case(crate_name));
|
||||
|
||||
// Build a lookup table for any provided validators; we must prefix the
|
||||
// name of the config and transform it to SCREAMING_SNAKE_CASE so that
|
||||
// it matches the keys in the hash table produced by `create_config`.
|
||||
let config_validators = config
|
||||
.iter()
|
||||
.flat_map(|(name, _description, _default, validator)| {
|
||||
if let Some(validator) = validator {
|
||||
let name = format!("{prefix}{}", screaming_snake_case(name));
|
||||
Some((name, validator))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut configs = create_config(stdout, &prefix, config, &mut doc_table);
|
||||
capture_from_env(&prefix, &mut configs);
|
||||
|
||||
for (name, value) in configs.iter() {
|
||||
if let Some(validator) = config_validators.get(name) {
|
||||
validator.validate(value).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let validators: HashMap<String, &Validator> = config
|
||||
.iter()
|
||||
.filter_map(|(name, _, _, validator)| {
|
||||
if validator.is_some() {
|
||||
Some((snake_case(name), validator.as_ref().unwrap()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
emit_configuration(stdout, &prefix, &configs, &validators, &mut selected_config);
|
||||
|
||||
write_config_json(snake_case(crate_name), config, &configs);
|
||||
|
||||
if emit_md_tables {
|
||||
let file_name = snake_case(crate_name);
|
||||
write_config_tables(&file_name, doc_table, selected_config);
|
||||
}
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
fn write_config_json(
|
||||
crate_name: String,
|
||||
config: &[(&str, &str, Value, Option<Validator>)],
|
||||
selected_config: &HashMap<String, Value>,
|
||||
) {
|
||||
#[derive(Serialize)]
|
||||
struct Item<'a> {
|
||||
name: String,
|
||||
description: String,
|
||||
default_value: Value,
|
||||
actual_value: Value,
|
||||
constraint: &'a Option<Validator>,
|
||||
}
|
||||
|
||||
let mut to_write: Vec<Item<'_>> = Vec::new();
|
||||
for item in config.iter() {
|
||||
let option_name = format!(
|
||||
"{}_CONFIG_{}",
|
||||
screaming_snake_case(&crate_name),
|
||||
screaming_snake_case(&item.0)
|
||||
);
|
||||
let val = match selected_config.get(&option_name) {
|
||||
Some(val) => val.clone(),
|
||||
None => item.2.clone(),
|
||||
};
|
||||
|
||||
to_write.push(Item {
|
||||
name: item.0.to_string(),
|
||||
description: item.1.to_string(),
|
||||
default_value: item.2.clone(),
|
||||
actual_value: val,
|
||||
constraint: &item.3,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let json = serde_json::to_string(&to_write).unwrap();
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
let out_file = out_dir
|
||||
.join(format!("{crate_name}_config_data.json"))
|
||||
.display()
|
||||
.to_string();
|
||||
fs::write(out_file, json).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// A work-around for https://github.com/rust-lang/cargo/issues/10358
|
||||
// This can be removed when https://github.com/rust-lang/cargo/pull/14058 is merged.
|
||||
// Unlikely to work on projects in workspaces
|
||||
#[cfg(not(test))]
|
||||
fn env_change_work_around<W: Write>(stdout: &mut W) {
|
||||
let mut out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
|
||||
// We clean out_dir by removing all trailing directories, until it ends with
|
||||
// target
|
||||
while !out_dir.ends_with("target") {
|
||||
if !out_dir.pop() {
|
||||
return; // We ran out of directories...
|
||||
}
|
||||
}
|
||||
out_dir.pop();
|
||||
|
||||
let dotcargo = out_dir.join(".cargo/");
|
||||
if dotcargo.exists() {
|
||||
if dotcargo.join("config.toml").exists() {
|
||||
writeln!(
|
||||
stdout,
|
||||
"cargo:rerun-if-changed={}",
|
||||
dotcargo.join("config.toml").display()
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
if dotcargo.join("config").exists() {
|
||||
writeln!(
|
||||
stdout,
|
||||
"cargo:rerun-if-changed={}",
|
||||
dotcargo.join("config").display()
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config<W: Write>(
|
||||
stdout: &mut W,
|
||||
prefix: &str,
|
||||
config: &[(&str, &str, Value, Option<Validator>)],
|
||||
doc_table: &mut String,
|
||||
) -> HashMap<String, Value> {
|
||||
let mut configs = HashMap::new();
|
||||
|
||||
for (name, description, default, validator) in config {
|
||||
let name = format!("{prefix}{}", screaming_snake_case(name));
|
||||
let allowed_values = if let Some(validator) = validator {
|
||||
validator.description()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.unwrap_or(String::from("-"));
|
||||
|
||||
configs.insert(name.clone(), default.clone());
|
||||
|
||||
// Write documentation table line:
|
||||
let default = default.to_string();
|
||||
writeln!(
|
||||
doc_table,
|
||||
"|**{name}**|{description}|{default}|{allowed_values}"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Rebuild if config environment variable changed:
|
||||
writeln!(stdout, "cargo:rerun-if-env-changed={name}").ok();
|
||||
}
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
fn capture_from_env(prefix: &str, configs: &mut HashMap<String, Value>) {
|
||||
let mut unknown = Vec::new();
|
||||
let mut failed = Vec::new();
|
||||
|
||||
// Try and capture input from the environment:
|
||||
for (var, value) in env::vars() {
|
||||
if var.starts_with(prefix) {
|
||||
let Some(cfg) = configs.get_mut(&var) else {
|
||||
unknown.push(var);
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Err(e) = cfg.parse_in_place(&value) {
|
||||
failed.push(format!("{var}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
panic!("Invalid configuration options detected: {:?}", failed);
|
||||
}
|
||||
|
||||
if !unknown.is_empty() {
|
||||
panic!("Unknown configuration options detected: {:?}", unknown);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_configuration<W: Write>(
|
||||
stdout: &mut W,
|
||||
prefix: &str,
|
||||
configs: &HashMap<String, Value>,
|
||||
validators: &HashMap<String, &Validator>,
|
||||
selected_config: &mut String,
|
||||
) {
|
||||
for (name, value) in configs.iter() {
|
||||
let cfg_name = snake_case(name.trim_start_matches(prefix));
|
||||
writeln!(stdout, "cargo:rustc-check-cfg=cfg({cfg_name})").ok();
|
||||
|
||||
if let Value::Bool(true) = value {
|
||||
writeln!(stdout, "cargo:rustc-cfg={cfg_name}").ok();
|
||||
}
|
||||
|
||||
if let Value::String(value) = value {
|
||||
if let Some(Validator::Enumeration(_)) = validators.get(&cfg_name) {
|
||||
let value = format!("{}_{}", cfg_name, snake_case(value));
|
||||
writeln!(stdout, "cargo:rustc-cfg={value}").ok();
|
||||
}
|
||||
}
|
||||
|
||||
let value = value.to_string();
|
||||
|
||||
// Values that haven't been seen will be output here with the default value:
|
||||
writeln!(stdout, "cargo:rustc-env={}={}", name, value).ok();
|
||||
writeln!(selected_config, "|**{name}**|{value}|").unwrap();
|
||||
}
|
||||
|
||||
for (name, validator) in validators {
|
||||
validator.emit_cargo_extras(stdout, &name);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_config_tables(prefix: &str, doc_table: String, selected_config: String) {
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
|
||||
let out_file = out_dir
|
||||
.join(format!("{prefix}_config_table.md"))
|
||||
.display()
|
||||
.to_string();
|
||||
fs::write(out_file, doc_table).unwrap();
|
||||
|
||||
let out_file = out_dir
|
||||
.join(format!("{prefix}_selected_config.md"))
|
||||
.display()
|
||||
.to_string();
|
||||
fs::write(out_file, selected_config).unwrap();
|
||||
}
|
||||
|
||||
fn snake_case(name: &str) -> String {
|
||||
let mut name = name.replace("-", "_");
|
||||
name.make_ascii_lowercase();
|
||||
|
||||
name
|
||||
}
|
||||
|
||||
fn screaming_snake_case(name: &str) -> String {
|
||||
let mut name = name.replace("-", "_");
|
||||
name.make_ascii_uppercase();
|
||||
|
||||
name
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn value_number_formats() {
|
||||
const INPUTS: &[&str] = &["0xAA", "0o252", "0b0000000010101010", "170"];
|
||||
let mut v = Value::Integer(0);
|
||||
|
||||
for input in INPUTS {
|
||||
v.parse_in_place(input).unwrap();
|
||||
// no matter the input format, the output format should be decimal
|
||||
assert_eq!(format!("{v}"), "170");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_bool_inputs() {
|
||||
let mut v = Value::Bool(false);
|
||||
|
||||
v.parse_in_place("true").unwrap();
|
||||
assert_eq!(format!("{v}"), "true");
|
||||
|
||||
v.parse_in_place("false").unwrap();
|
||||
assert_eq!(format!("{v}"), "false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
|
||||
("ESP_TEST_CONFIG_NUMBER_SIGNED", Some("-999")),
|
||||
("ESP_TEST_CONFIG_STRING", Some("Hello world!")),
|
||||
("ESP_TEST_CONFIG_BOOL", Some("true")),
|
||||
],
|
||||
|| {
|
||||
let configs = generate_config(
|
||||
"esp-test",
|
||||
&[
|
||||
("number", "NA", Value::Integer(999), None),
|
||||
("number_signed", "NA", Value::Integer(-777), None),
|
||||
("string", "NA", Value::String("Demo".to_owned()), None),
|
||||
("bool", "NA", Value::Bool(false), None),
|
||||
("number_default", "NA", Value::Integer(999), None),
|
||||
(
|
||||
"string_default",
|
||||
"NA",
|
||||
Value::String("Demo".to_owned()),
|
||||
None,
|
||||
),
|
||||
("bool_default", "NA", Value::Bool(false), None),
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// some values have changed
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_CONFIG_NUMBER").unwrap() {
|
||||
Value::Integer(num) => *num,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
0xaa
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_CONFIG_NUMBER_SIGNED").unwrap() {
|
||||
Value::Integer(num) => *num,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
-999
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_CONFIG_STRING").unwrap() {
|
||||
Value::String(val) => val,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"Hello world!"
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_CONFIG_BOOL").unwrap() {
|
||||
Value::Bool(val) => *val,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// the rest are the defaults
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_CONFIG_NUMBER_DEFAULT").unwrap() {
|
||||
Value::Integer(num) => *num,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
999
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_CONFIG_STRING_DEFAULT").unwrap() {
|
||||
Value::String(val) => val,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"Demo"
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_CONFIG_BOOL_DEFAULT").unwrap() {
|
||||
Value::Bool(val) => *val,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_validation_passes() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("7")),
|
||||
("ESP_TEST_CONFIG_NEGATIVE_NUMBER", Some("-1")),
|
||||
("ESP_TEST_CONFIG_NON_NEGATIVE_NUMBER", Some("0")),
|
||||
("ESP_TEST_CONFIG_RANGE", Some("9")),
|
||||
],
|
||||
|| {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[
|
||||
(
|
||||
"positive_number",
|
||||
"NA",
|
||||
Value::Integer(-1),
|
||||
Some(Validator::PositiveInteger),
|
||||
),
|
||||
(
|
||||
"negative_number",
|
||||
"NA",
|
||||
Value::Integer(1),
|
||||
Some(Validator::NegativeInteger),
|
||||
),
|
||||
(
|
||||
"non_negative_number",
|
||||
"NA",
|
||||
Value::Integer(-1),
|
||||
Some(Validator::NonNegativeInteger),
|
||||
),
|
||||
(
|
||||
"range",
|
||||
"NA",
|
||||
Value::Integer(0),
|
||||
Some(Validator::IntegerInRange(5..10)),
|
||||
),
|
||||
],
|
||||
false,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_validation_passes() {
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("13"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[(
|
||||
"number",
|
||||
"NA",
|
||||
Value::Integer(-1),
|
||||
Some(Validator::Custom(Box::new(|value| {
|
||||
let range = 10..20;
|
||||
if !value.is_integer() || !range.contains(&value.as_integer()) {
|
||||
Err(Error::validation("value does not fall within range"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}))),
|
||||
)],
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn builtin_validation_bails() {
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("-99"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[(
|
||||
"positive_number",
|
||||
"NA",
|
||||
Value::Integer(-1),
|
||||
Some(Validator::PositiveInteger),
|
||||
)],
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn custom_validation_bails() {
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("37"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[(
|
||||
"number",
|
||||
"NA",
|
||||
Value::Integer(-1),
|
||||
Some(Validator::Custom(Box::new(|value| {
|
||||
let range = 10..20;
|
||||
if !value.is_integer() || !range.contains(&value.as_integer()) {
|
||||
Err(Error::validation("value does not fall within range"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}))),
|
||||
)],
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn env_unknown_bails() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
|
||||
("ESP_TEST_CONFIG_RANDOM_VARIABLE", Some("")),
|
||||
],
|
||||
|| {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[("number", "NA", Value::Integer(999), None)],
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn env_invalid_values_bails() {
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("Hello world"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[("number", "NA", Value::Integer(999), None)],
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_unknown_prefix_is_ignored() {
|
||||
temp_env::with_vars(
|
||||
[("ESP_TEST_OTHER_CONFIG_NUMBER", Some("Hello world"))],
|
||||
|| {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[("number", "NA", Value::Integer(999), None)],
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enumeration_validator() {
|
||||
let mut stdout = Vec::new();
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
|
||||
generate_config_internal(
|
||||
&mut stdout,
|
||||
"esp-test",
|
||||
&[(
|
||||
"some-key",
|
||||
"NA",
|
||||
Value::String("variant-0".to_string()),
|
||||
Some(Validator::Enumeration(vec![
|
||||
"variant-0".to_string(),
|
||||
"variant-1".to_string(),
|
||||
])),
|
||||
)],
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
let cargo_lines: Vec<&str> = std::str::from_utf8(&stdout).unwrap().lines().collect();
|
||||
println!("{:#?}", cargo_lines);
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key)"));
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-env=ESP_TEST_CONFIG_SOME_KEY=variant-0"));
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_0)"));
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_1)"));
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-cfg=some_key_variant_0"));
|
||||
}
|
||||
}
|
32
esp-config/src/generate/markdown.rs
Normal file
32
esp-config/src/generate/markdown.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{ConfigOption, Value};
|
||||
|
||||
pub(crate) const DOC_TABLE_HEADER: &str = r#"
|
||||
| Name | Description | Default value | Allowed value |
|
||||
|------|-------------|---------------|---------------|
|
||||
"#;
|
||||
|
||||
pub(crate) const SELECTED_TABLE_HEADER: &str = r#"
|
||||
| Name | Selected value |
|
||||
|------|----------------|
|
||||
"#;
|
||||
|
||||
pub(crate) fn write_doc_table_line(mut table: impl Write, name: &str, option: &ConfigOption) {
|
||||
let allowed_values = option
|
||||
.constraint
|
||||
.as_ref()
|
||||
.and_then(|validator| validator.description())
|
||||
.unwrap_or(String::from("-"));
|
||||
|
||||
writeln!(
|
||||
table,
|
||||
"|**{}**|{}|{}|{}",
|
||||
name, option.description, option.default_value, allowed_values
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn write_summary_table_line(mut table: impl Write, name: &str, value: &Value) {
|
||||
writeln!(table, "|**{name}**|{value}|").unwrap();
|
||||
}
|
654
esp-config/src/generate/mod.rs
Normal file
654
esp-config/src/generate/mod.rs
Normal file
@ -0,0 +1,654 @@
|
||||
use std::{collections::HashMap, env, fmt, fs, io::Write, path::PathBuf};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::generate::{validator::Validator, value::Value};
|
||||
|
||||
mod markdown;
|
||||
pub(crate) mod validator;
|
||||
pub(crate) mod value;
|
||||
|
||||
/// Configuration errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// Parse errors.
|
||||
Parse(String),
|
||||
/// Validation errors.
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Convenience function for creating parse errors.
|
||||
pub fn parse<S>(message: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Self::Parse(message.into())
|
||||
}
|
||||
|
||||
/// Convenience function for creating validation errors.
|
||||
pub fn validation<S>(message: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Self::Validation(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Parse(message) => write!(f, "{message}"),
|
||||
Error::Validation(message) => write!(f, "{message}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate and parse config from a prefix, and an array tuples containing the
|
||||
/// name, description, default value, and an optional validator.
|
||||
///
|
||||
/// This function will parse any `SCREAMING_SNAKE_CASE` environment variables
|
||||
/// that match the given prefix. It will then attempt to parse the [`Value`] and
|
||||
/// run any validators which have been specified.
|
||||
///
|
||||
/// Once the config has been parsed, this function will emit `snake_case` cfg's
|
||||
/// _without_ the prefix which can be used in the dependant crate. After that,
|
||||
/// it will create a markdown table in the `OUT_DIR` under the name
|
||||
/// `{prefix}_config_table.md` where prefix has also been converted to
|
||||
/// `snake_case`. This can be included in crate documentation to outline the
|
||||
/// available configuration options for the crate.
|
||||
///
|
||||
/// Passing a value of true for the `emit_md_tables` argument will create and
|
||||
/// write markdown files of the available configuration and selected
|
||||
/// configuration which can be included in documentation.
|
||||
///
|
||||
/// Unknown keys with the supplied prefix will cause this function to panic.
|
||||
pub fn generate_config(
|
||||
crate_name: &str,
|
||||
config: &[ConfigOption],
|
||||
emit_md_tables: bool,
|
||||
) -> HashMap<String, Value> {
|
||||
let configs = generate_config_internal(std::io::stdout(), crate_name, config);
|
||||
|
||||
if emit_md_tables {
|
||||
let file_name = snake_case(crate_name);
|
||||
|
||||
let mut doc_table = String::from(markdown::DOC_TABLE_HEADER);
|
||||
let mut selected_config = String::from(markdown::SELECTED_TABLE_HEADER);
|
||||
|
||||
for (name, option, value) in configs.iter() {
|
||||
markdown::write_doc_table_line(&mut doc_table, &name, option);
|
||||
markdown::write_summary_table_line(&mut selected_config, &name, value);
|
||||
}
|
||||
|
||||
write_out_file(format!("{file_name}_config_table.md"), doc_table);
|
||||
write_out_file(format!("{file_name}_selected_config.md"), selected_config);
|
||||
}
|
||||
|
||||
// Remove the ConfigOptions from the output
|
||||
configs.into_iter().map(|(k, _, v)| (k, v)).collect()
|
||||
}
|
||||
|
||||
pub fn generate_config_internal<'a>(
|
||||
mut stdout: impl Write,
|
||||
crate_name: &str,
|
||||
config: &'a [ConfigOption],
|
||||
) -> Vec<(String, &'a ConfigOption, Value)> {
|
||||
// Only rebuild if `build.rs` changed. Otherwise, Cargo will rebuild if any
|
||||
// other file changed.
|
||||
writeln!(stdout, "cargo:rerun-if-changed=build.rs").ok();
|
||||
|
||||
#[cfg(not(test))]
|
||||
env_change_work_around(&mut stdout);
|
||||
|
||||
// Ensure that the prefix is `SCREAMING_SNAKE_CASE`:
|
||||
let prefix = format!("{}_CONFIG_", screaming_snake_case(crate_name));
|
||||
|
||||
let mut configs = create_config(&prefix, config);
|
||||
capture_from_env(&prefix, &mut configs);
|
||||
|
||||
for (_, option, value) in configs.iter() {
|
||||
if let Some(ref validator) = option.constraint {
|
||||
validator.validate(value).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
emit_configuration(&mut stdout, &configs);
|
||||
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let config_json = config_json(&configs, false);
|
||||
write_out_file(format!("{crate_name}_config_data.json"), config_json);
|
||||
}
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
fn config_json(config: &[(String, &ConfigOption, Value)], pretty: bool) -> String {
|
||||
#[derive(Serialize)]
|
||||
struct Item<'a> {
|
||||
#[serde(flatten)]
|
||||
option: &'a ConfigOption,
|
||||
actual_value: Value,
|
||||
}
|
||||
|
||||
let mut to_write = Vec::new();
|
||||
for (_, option, value) in config.iter() {
|
||||
to_write.push(Item {
|
||||
actual_value: value.clone(),
|
||||
option,
|
||||
})
|
||||
}
|
||||
|
||||
if pretty {
|
||||
serde_json::to_string_pretty(&to_write).unwrap()
|
||||
} else {
|
||||
serde_json::to_string(&to_write).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
// A work-around for https://github.com/rust-lang/cargo/issues/10358
|
||||
// This can be removed when https://github.com/rust-lang/cargo/pull/14058 is merged.
|
||||
// Unlikely to work on projects in workspaces
|
||||
#[cfg(not(test))]
|
||||
fn env_change_work_around(mut stdout: impl Write) {
|
||||
let mut out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
|
||||
// We clean out_dir by removing all trailing directories, until it ends with
|
||||
// target
|
||||
while !out_dir.ends_with("target") {
|
||||
if !out_dir.pop() {
|
||||
return; // We ran out of directories...
|
||||
}
|
||||
}
|
||||
out_dir.pop();
|
||||
|
||||
let dotcargo = out_dir.join(".cargo/");
|
||||
if dotcargo.exists() {
|
||||
if dotcargo.join("config.toml").exists() {
|
||||
writeln!(
|
||||
stdout,
|
||||
"cargo:rerun-if-changed={}",
|
||||
dotcargo.join("config.toml").display()
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
if dotcargo.join("config").exists() {
|
||||
writeln!(
|
||||
stdout,
|
||||
"cargo:rerun-if-changed={}",
|
||||
dotcargo.join("config").display()
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A configuration option.
|
||||
#[derive(Serialize)]
|
||||
pub struct ConfigOption {
|
||||
/// The name of the configuration option.
|
||||
///
|
||||
/// The associated environment variable has the format of
|
||||
/// `<PREFIX>_CONFIG_<NAME>`.
|
||||
pub name: &'static str,
|
||||
|
||||
/// The description of the configuration option.
|
||||
///
|
||||
/// The description will be included in the generated markdown
|
||||
/// documentation.
|
||||
pub description: &'static str,
|
||||
|
||||
/// The default value of the configuration option.
|
||||
pub default_value: Value,
|
||||
|
||||
/// An optional validator for the configuration option.
|
||||
pub constraint: Option<Validator>,
|
||||
}
|
||||
|
||||
impl ConfigOption {
|
||||
fn env_var(&self, prefix: &str) -> String {
|
||||
format!("{}{}", prefix, screaming_snake_case(self.name))
|
||||
}
|
||||
|
||||
fn cfg_name(&self) -> String {
|
||||
snake_case(self.name)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config<'a>(
|
||||
prefix: &str,
|
||||
config: &'a [ConfigOption],
|
||||
) -> Vec<(String, &'a ConfigOption, Value)> {
|
||||
let mut configs = Vec::with_capacity(config.len());
|
||||
|
||||
for option in config {
|
||||
configs.push((option.env_var(prefix), option, option.default_value.clone()));
|
||||
}
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
fn capture_from_env(prefix: &str, configs: &mut Vec<(String, &ConfigOption, Value)>) {
|
||||
let mut unknown = Vec::new();
|
||||
let mut failed = Vec::new();
|
||||
|
||||
// Try and capture input from the environment:
|
||||
for (var, value) in env::vars() {
|
||||
if var.starts_with(prefix) {
|
||||
let Some((_, _, cfg)) = configs.iter_mut().find(|(k, _, _)| k == &var) else {
|
||||
unknown.push(var);
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Err(e) = cfg.parse_in_place(&value) {
|
||||
failed.push(format!("{var}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
panic!("Invalid configuration options detected: {:?}", failed);
|
||||
}
|
||||
|
||||
if !unknown.is_empty() {
|
||||
panic!("Unknown configuration options detected: {:?}", unknown);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_configuration(mut stdout: impl Write, configs: &[(String, &ConfigOption, Value)]) {
|
||||
for (env_var_name, option, value) in configs.iter() {
|
||||
let cfg_name = option.cfg_name();
|
||||
|
||||
// Output the raw configuration as an env var. Values that haven't been seen
|
||||
// will be output here with the default value. Also trigger a rebuild if config
|
||||
// environment variable changed.
|
||||
writeln!(stdout, "cargo:rustc-env={}={}", env_var_name, value).ok();
|
||||
writeln!(stdout, "cargo:rerun-if-env-changed={}", env_var_name).ok();
|
||||
|
||||
// Emit known config symbol:
|
||||
writeln!(stdout, "cargo:rustc-check-cfg=cfg({cfg_name})").ok();
|
||||
|
||||
// Emit specially-handled values:
|
||||
if let Value::Bool(true) = value {
|
||||
writeln!(stdout, "cargo:rustc-cfg={cfg_name}").ok();
|
||||
}
|
||||
|
||||
// Emit extra symbols based on the validator (e.g. enumerated values):
|
||||
if let Some(validator) = option.constraint.as_ref() {
|
||||
validator.emit_cargo_extras(&mut stdout, &cfg_name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_out_file(file_name: String, json: String) {
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
let out_file = out_dir.join(file_name);
|
||||
fs::write(out_file, json).unwrap();
|
||||
}
|
||||
|
||||
fn snake_case(name: &str) -> String {
|
||||
let mut name = name.replace("-", "_");
|
||||
name.make_ascii_lowercase();
|
||||
|
||||
name
|
||||
}
|
||||
|
||||
fn screaming_snake_case(name: &str) -> String {
|
||||
let mut name = name.replace("-", "_");
|
||||
name.make_ascii_uppercase();
|
||||
|
||||
name
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::generate::{validator::Validator, value::Value};
|
||||
|
||||
#[test]
|
||||
fn value_number_formats() {
|
||||
const INPUTS: &[&str] = &["0xAA", "0o252", "0b0000000010101010", "170"];
|
||||
let mut v = Value::Integer(0);
|
||||
|
||||
for input in INPUTS {
|
||||
v.parse_in_place(input).unwrap();
|
||||
// no matter the input format, the output format should be decimal
|
||||
assert_eq!(v.to_string(), "170");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_bool_inputs() {
|
||||
let mut v = Value::Bool(false);
|
||||
|
||||
v.parse_in_place("true").unwrap();
|
||||
assert_eq!(v.to_string(), "true");
|
||||
|
||||
v.parse_in_place("false").unwrap();
|
||||
assert_eq!(v.to_string(), "false");
|
||||
|
||||
v.parse_in_place("else")
|
||||
.expect_err("Only true or false are valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
|
||||
("ESP_TEST_CONFIG_NUMBER_SIGNED", Some("-999")),
|
||||
("ESP_TEST_CONFIG_STRING", Some("Hello world!")),
|
||||
("ESP_TEST_CONFIG_BOOL", Some("true")),
|
||||
],
|
||||
|| {
|
||||
let configs = generate_config(
|
||||
"esp-test",
|
||||
&[
|
||||
ConfigOption {
|
||||
name: "number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(999),
|
||||
constraint: None,
|
||||
},
|
||||
ConfigOption {
|
||||
name: "number_signed",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(-777),
|
||||
constraint: None,
|
||||
},
|
||||
ConfigOption {
|
||||
name: "string",
|
||||
description: "NA",
|
||||
default_value: Value::String("Demo".to_string()),
|
||||
constraint: None,
|
||||
},
|
||||
ConfigOption {
|
||||
name: "bool",
|
||||
description: "NA",
|
||||
default_value: Value::Bool(false),
|
||||
constraint: None,
|
||||
},
|
||||
ConfigOption {
|
||||
name: "number_default",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(999),
|
||||
constraint: None,
|
||||
},
|
||||
ConfigOption {
|
||||
name: "string_default",
|
||||
description: "NA",
|
||||
default_value: Value::String("Demo".to_string()),
|
||||
constraint: None,
|
||||
},
|
||||
ConfigOption {
|
||||
name: "bool_default",
|
||||
description: "NA",
|
||||
default_value: Value::Bool(false),
|
||||
constraint: None,
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// some values have changed
|
||||
assert_eq!(configs["ESP_TEST_CONFIG_NUMBER"], Value::Integer(0xaa));
|
||||
assert_eq!(
|
||||
configs["ESP_TEST_CONFIG_NUMBER_SIGNED"],
|
||||
Value::Integer(-999)
|
||||
);
|
||||
assert_eq!(
|
||||
configs["ESP_TEST_CONFIG_STRING"],
|
||||
Value::String("Hello world!".to_string())
|
||||
);
|
||||
assert_eq!(configs["ESP_TEST_CONFIG_BOOL"], Value::Bool(true));
|
||||
|
||||
// the rest are the defaults
|
||||
assert_eq!(
|
||||
configs["ESP_TEST_CONFIG_NUMBER_DEFAULT"],
|
||||
Value::Integer(999)
|
||||
);
|
||||
assert_eq!(
|
||||
configs["ESP_TEST_CONFIG_STRING_DEFAULT"],
|
||||
Value::String("Demo".to_string())
|
||||
);
|
||||
assert_eq!(configs["ESP_TEST_CONFIG_BOOL_DEFAULT"], Value::Bool(false));
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_validation_passes() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("7")),
|
||||
("ESP_TEST_CONFIG_NEGATIVE_NUMBER", Some("-1")),
|
||||
("ESP_TEST_CONFIG_NON_NEGATIVE_NUMBER", Some("0")),
|
||||
("ESP_TEST_CONFIG_RANGE", Some("9")),
|
||||
],
|
||||
|| {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[
|
||||
ConfigOption {
|
||||
name: "positive_number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(-1),
|
||||
constraint: Some(Validator::PositiveInteger),
|
||||
},
|
||||
ConfigOption {
|
||||
name: "negative_number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(1),
|
||||
constraint: Some(Validator::NegativeInteger),
|
||||
},
|
||||
ConfigOption {
|
||||
name: "non_negative_number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(-1),
|
||||
constraint: Some(Validator::NonNegativeInteger),
|
||||
},
|
||||
ConfigOption {
|
||||
name: "range",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(0),
|
||||
constraint: Some(Validator::IntegerInRange(5..10)),
|
||||
},
|
||||
],
|
||||
false,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_validation_passes() {
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("13"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[ConfigOption {
|
||||
name: "number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(-1),
|
||||
constraint: Some(Validator::Custom(Box::new(|value| {
|
||||
let range = 10..20;
|
||||
if !value.is_integer() || !range.contains(&value.as_integer()) {
|
||||
Err(Error::validation("value does not fall within range"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}))),
|
||||
}],
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn builtin_validation_bails() {
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("-99"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[ConfigOption {
|
||||
name: "positive_number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(-1),
|
||||
constraint: Some(Validator::PositiveInteger),
|
||||
}],
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn custom_validation_bails() {
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("37"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[ConfigOption {
|
||||
name: "number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(-1),
|
||||
constraint: Some(Validator::Custom(Box::new(|value| {
|
||||
let range = 10..20;
|
||||
if !value.is_integer() || !range.contains(&value.as_integer()) {
|
||||
Err(Error::validation("value does not fall within range"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}))),
|
||||
}],
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn env_unknown_bails() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
|
||||
("ESP_TEST_CONFIG_RANDOM_VARIABLE", Some("")),
|
||||
],
|
||||
|| {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[ConfigOption {
|
||||
name: "number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(999),
|
||||
constraint: None,
|
||||
}],
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn env_invalid_values_bails() {
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("Hello world"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[ConfigOption {
|
||||
name: "number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(999),
|
||||
constraint: None,
|
||||
}],
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_unknown_prefix_is_ignored() {
|
||||
temp_env::with_vars(
|
||||
[("ESP_TEST_OTHER_CONFIG_NUMBER", Some("Hello world"))],
|
||||
|| {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[ConfigOption {
|
||||
name: "number",
|
||||
description: "NA",
|
||||
default_value: Value::Integer(999),
|
||||
constraint: None,
|
||||
}],
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enumeration_validator() {
|
||||
let mut stdout = Vec::new();
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
|
||||
generate_config_internal(
|
||||
&mut stdout,
|
||||
"esp-test",
|
||||
&[ConfigOption {
|
||||
name: "some-key",
|
||||
description: "NA",
|
||||
default_value: Value::String("variant-0".to_string()),
|
||||
constraint: Some(Validator::Enumeration(vec![
|
||||
"variant-0".to_string(),
|
||||
"variant-1".to_string(),
|
||||
])),
|
||||
}],
|
||||
);
|
||||
});
|
||||
|
||||
let cargo_lines: Vec<&str> = std::str::from_utf8(&stdout).unwrap().lines().collect();
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key)"));
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-env=ESP_TEST_CONFIG_SOME_KEY=variant-0"));
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_0)"));
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_1)"));
|
||||
assert!(cargo_lines.contains(&"cargo:rustc-cfg=some_key_variant_0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_output() {
|
||||
let mut stdout = Vec::new();
|
||||
let config = [ConfigOption {
|
||||
name: "some-key",
|
||||
description: "NA",
|
||||
default_value: Value::String("variant-0".to_string()),
|
||||
constraint: Some(Validator::Enumeration(vec![
|
||||
"variant-0".to_string(),
|
||||
"variant-1".to_string(),
|
||||
])),
|
||||
}];
|
||||
let configs =
|
||||
temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
|
||||
generate_config_internal(&mut stdout, "esp-test", &config)
|
||||
});
|
||||
|
||||
let json_output = config_json(&configs, true);
|
||||
pretty_assertions::assert_eq!(
|
||||
r#"[
|
||||
{
|
||||
"name": "some-key",
|
||||
"description": "NA",
|
||||
"default_value": {
|
||||
"String": "variant-0"
|
||||
},
|
||||
"constraint": {
|
||||
"Enumeration": [
|
||||
"variant-0",
|
||||
"variant-1"
|
||||
]
|
||||
},
|
||||
"actual_value": {
|
||||
"String": "variant-0"
|
||||
}
|
||||
}
|
||||
]"#,
|
||||
json_output
|
||||
);
|
||||
}
|
||||
}
|
165
esp-config/src/generate/validator.rs
Normal file
165
esp-config/src/generate/validator.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use std::{io::Write, ops::Range};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{snake_case, value::Value, Error};
|
||||
|
||||
/// Configuration value validation functions.
|
||||
#[derive(Serialize)]
|
||||
pub enum Validator {
|
||||
/// Only allow negative integers, i.e. any values less than 0.
|
||||
NegativeInteger,
|
||||
/// Only allow non-negative integers, i.e. any values greater than or
|
||||
/// equal to 0.
|
||||
NonNegativeInteger,
|
||||
/// Only allow positive integers, i.e. any values greater than to 0.
|
||||
PositiveInteger,
|
||||
/// Ensure that an integer value falls within the specified range.
|
||||
IntegerInRange(Range<i128>),
|
||||
/// String-Enumeration. Only allows one of the given Strings.
|
||||
Enumeration(Vec<String>),
|
||||
/// A custom validation function to run against any supported value
|
||||
/// type.
|
||||
#[serde(serialize_with = "serialize_custom")]
|
||||
#[serde(untagged)]
|
||||
Custom(Box<dyn Fn(&Value) -> Result<(), Error>>),
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_custom<S>(
|
||||
_: &Box<dyn Fn(&Value) -> Result<(), Error>>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str("Custom")
|
||||
}
|
||||
|
||||
impl Validator {
|
||||
pub(crate) fn validate(&self, value: &Value) -> Result<(), Error> {
|
||||
match self {
|
||||
Validator::NegativeInteger => negative_integer(value)?,
|
||||
Validator::NonNegativeInteger => non_negative_integer(value)?,
|
||||
Validator::PositiveInteger => positive_integer(value)?,
|
||||
Validator::IntegerInRange(range) => integer_in_range(range, value)?,
|
||||
Validator::Enumeration(values) => enumeration(values, value)?,
|
||||
Validator::Custom(validator_fn) => validator_fn(value)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn description(&self) -> Option<String> {
|
||||
match self {
|
||||
Validator::NegativeInteger => Some(String::from("Negative integer")),
|
||||
Validator::NonNegativeInteger => Some(String::from("Positive integer or 0")),
|
||||
Validator::PositiveInteger => Some(String::from("Positive integer")),
|
||||
Validator::IntegerInRange(range) => {
|
||||
Some(format!("Integer in range {}..{}", range.start, range.end))
|
||||
}
|
||||
Validator::Enumeration(values) => Some(format!("Any of {:?}", values)),
|
||||
Validator::Custom(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit_cargo_extras(
|
||||
&self,
|
||||
mut stdout: impl Write,
|
||||
config_key: &str,
|
||||
actual_value: &Value,
|
||||
) {
|
||||
match self {
|
||||
Validator::Enumeration(values) => {
|
||||
for possible_value in values {
|
||||
writeln!(
|
||||
stdout,
|
||||
"cargo:rustc-check-cfg=cfg({config_key}_{})",
|
||||
snake_case(possible_value)
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
stdout,
|
||||
"cargo:rustc-cfg={config_key}_{}",
|
||||
snake_case(&actual_value.to_string())
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn enumeration(values: &Vec<String>, value: &Value) -> Result<(), Error> {
|
||||
if let Value::String(value) = value {
|
||||
if !values.contains(value) {
|
||||
return Err(Error::validation(format!(
|
||||
"Expected one of {:?}, found '{}'",
|
||||
values, value
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
return Err(Error::parse(
|
||||
"Validator::Enumeration can only be used with string values",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn negative_integer(value: &Value) -> Result<(), Error> {
|
||||
if !value.is_integer() {
|
||||
return Err(Error::validation(
|
||||
"Validator::NegativeInteger can only be used with integer values",
|
||||
));
|
||||
} else if value.as_integer() >= 0 {
|
||||
return Err(Error::validation(format!(
|
||||
"Expected negative integer, found '{}'",
|
||||
value.as_integer()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn non_negative_integer(value: &Value) -> Result<(), Error> {
|
||||
if !value.is_integer() {
|
||||
return Err(Error::validation(
|
||||
"Validator::NonNegativeInteger can only be used with integer values",
|
||||
));
|
||||
} else if value.as_integer() < 0 {
|
||||
return Err(Error::validation(format!(
|
||||
"Expected non-negative integer, found '{}'",
|
||||
value.as_integer()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn positive_integer(value: &Value) -> Result<(), Error> {
|
||||
if !value.is_integer() {
|
||||
return Err(Error::validation(
|
||||
"Validator::PositiveInteger can only be used with integer values",
|
||||
));
|
||||
} else if value.as_integer() <= 0 {
|
||||
return Err(Error::validation(format!(
|
||||
"Expected positive integer, found '{}'",
|
||||
value.as_integer()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn integer_in_range(range: &Range<i128>, value: &Value) -> Result<(), Error> {
|
||||
if !value.is_integer() || !range.contains(&value.as_integer()) {
|
||||
Err(Error::validation(format!(
|
||||
"Value '{}' does not fall within range '{:?}'",
|
||||
value, range
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
96
esp-config/src/generate/value.rs
Normal file
96
esp-config/src/generate/value.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use std::fmt;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use super::Error;
|
||||
|
||||
/// Supported configuration value types.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub enum Value {
|
||||
/// Booleans.
|
||||
Bool(bool),
|
||||
/// Integers.
|
||||
Integer(i128),
|
||||
/// Strings.
|
||||
String(String),
|
||||
}
|
||||
|
||||
// TODO: Do we want to handle negative values for non-decimal values?
|
||||
impl Value {
|
||||
pub(crate) fn parse_in_place(&mut self, s: &str) -> Result<(), Error> {
|
||||
*self = match self {
|
||||
Value::Bool(_) => match s {
|
||||
"true" => Value::Bool(true),
|
||||
"false" => Value::Bool(false),
|
||||
_ => {
|
||||
return Err(Error::parse(format!(
|
||||
"Expected 'true' or 'false', found: '{s}'"
|
||||
)))
|
||||
}
|
||||
},
|
||||
Value::Integer(_) => {
|
||||
let inner = match s.as_bytes() {
|
||||
[b'0', b'x', ..] => i128::from_str_radix(&s[2..], 16),
|
||||
[b'0', b'o', ..] => i128::from_str_radix(&s[2..], 8),
|
||||
[b'0', b'b', ..] => i128::from_str_radix(&s[2..], 2),
|
||||
_ => i128::from_str_radix(&s, 10),
|
||||
}
|
||||
.map_err(|_| Error::parse(format!("Expected valid intger value, found: '{s}'")))?;
|
||||
|
||||
Value::Integer(inner)
|
||||
}
|
||||
Value::String(_) => Value::String(s.into()),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert the value to a [bool].
|
||||
pub fn as_bool(&self) -> bool {
|
||||
match self {
|
||||
Value::Bool(value) => *value,
|
||||
_ => panic!("attempted to convert non-bool value to a bool"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the value to an [i128].
|
||||
pub fn as_integer(&self) -> i128 {
|
||||
match self {
|
||||
Value::Integer(value) => *value,
|
||||
_ => panic!("attempted to convert non-integer value to an integer"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the value to a [String].
|
||||
pub fn as_string(&self) -> String {
|
||||
match self {
|
||||
Value::String(value) => value.to_owned(),
|
||||
_ => panic!("attempted to convert non-string value to a string"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the value a bool?
|
||||
pub fn is_bool(&self) -> bool {
|
||||
matches!(self, Value::Bool(_))
|
||||
}
|
||||
|
||||
/// Is the value an integer?
|
||||
pub fn is_integer(&self) -> bool {
|
||||
matches!(self, Value::Integer(_))
|
||||
}
|
||||
|
||||
/// Is the value a string?
|
||||
pub fn is_string(&self) -> bool {
|
||||
matches!(self, Value::String(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Value::Bool(b) => write!(f, "{b}"),
|
||||
Value::Integer(i) => write!(f, "{i}"),
|
||||
Value::String(s) => write!(f, "{s}"),
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
#[cfg(feature = "build")]
|
||||
mod generate;
|
||||
#[cfg(feature = "build")]
|
||||
pub use generate::{generate_config, Error, Validator, Value};
|
||||
pub use generate::{generate_config, validator::Validator, value::Value, ConfigOption, Error};
|
||||
|
||||
/// Parse the value of an environment variable as a [bool] at compile time.
|
||||
#[macro_export]
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::{error::Error as StdError, str::FromStr};
|
||||
|
||||
use esp_build::assert_unique_used_features;
|
||||
use esp_config::{generate_config, Error, Validator, Value};
|
||||
use esp_config::{generate_config, ConfigOption, Error, Validator, Value};
|
||||
use esp_metadata::{Chip, Config};
|
||||
|
||||
fn main() -> Result<(), Box<dyn StdError>> {
|
||||
@ -41,47 +41,48 @@ fn main() -> Result<(), Box<dyn StdError>> {
|
||||
// emit config
|
||||
let crate_config = generate_config(
|
||||
"esp_hal_embassy",
|
||||
&[(
|
||||
"low-power-wait",
|
||||
"Enables the lower-power wait if no tasks are ready to run on the thread-mode executor. This allows the MCU to use less power if the workload allows. Recommended for battery-powered systems. May impact analog performance.",
|
||||
Value::Bool(true),
|
||||
None
|
||||
),
|
||||
(
|
||||
"timer-queue",
|
||||
"<p>The flavour of the timer queue provided by this crate. Accepts one of `single-integrated`, `multiple-integrated` or `generic`. Integrated queues require the `executors` feature to be enabled.</p><p>If you use embassy-executor, the `single-integrated` queue is recommended for ease of use, while the `multiple-integrated` queue is recommended for performance. The `multiple-integrated` option needs one timer per executor.</p><p>The `generic` queue allows using embassy-time without the embassy executors.</p>",
|
||||
Value::String(if cfg!(feature = "executors") {
|
||||
String::from("single-integrated")
|
||||
} else {
|
||||
String::from("generic")
|
||||
}),
|
||||
Some(Validator::Custom(Box::new(|value| {
|
||||
let Value::String(string) = value else {
|
||||
return Err(Error::Validation(String::from("Expected a string")));
|
||||
};
|
||||
&[
|
||||
ConfigOption {
|
||||
name: "low-power-wait",
|
||||
description: "Enables the lower-power wait if no tasks are ready to run on the thread-mode executor. This allows the MCU to use less power if the workload allows. Recommended for battery-powered systems. May impact analog performance.",
|
||||
default_value: Value::Bool(true),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "timer-queue",
|
||||
description: "<p>The flavour of the timer queue provided by this crate. Accepts one of `single-integrated`, `multiple-integrated` or `generic`. Integrated queues require the `executors` feature to be enabled.</p><p>If you use embassy-executor, the `single-integrated` queue is recommended for ease of use, while the `multiple-integrated` queue is recommended for performance. The `multiple-integrated` option needs one timer per executor.</p><p>The `generic` queue allows using embassy-time without the embassy executors.</p>",
|
||||
default_value: Value::String(if cfg!(feature = "executors") {
|
||||
String::from("single-integrated")
|
||||
} else {
|
||||
String::from("generic")
|
||||
}),
|
||||
constraint: Some(Validator::Custom(Box::new(|value| {
|
||||
let Value::String(string) = value else {
|
||||
return Err(Error::Validation(String::from("Expected a string")));
|
||||
};
|
||||
|
||||
if !cfg!(feature = "executors") {
|
||||
if string.as_str() != "generic" {
|
||||
return Err(Error::Validation(format!("Expected 'generic' because the `executors` feature is not enabled. Found {string}")));
|
||||
if !cfg!(feature = "executors") {
|
||||
if string.as_str() != "generic" {
|
||||
return Err(Error::Validation(format!("Expected 'generic' because the `executors` feature is not enabled. Found {string}")));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match string.as_str() {
|
||||
"single-integrated" => Ok(()), // preferred for ease of use
|
||||
"multiple-integrated" => Ok(()), // preferred for performance
|
||||
"generic" => Ok(()), // allows using embassy-time without the embassy executors
|
||||
_ => Err(Error::Validation(format!("Expected 'single-integrated', 'multiple-integrated' or 'generic', found {string}")))
|
||||
}
|
||||
})))
|
||||
),
|
||||
(
|
||||
"generic-queue-size",
|
||||
"The capacity of the queue when the `generic` timer queue flavour is selected.",
|
||||
Value::Integer(64),
|
||||
Some(Validator::PositiveInteger),
|
||||
),
|
||||
],
|
||||
match string.as_str() {
|
||||
"single-integrated" => Ok(()), // preferred for ease of use
|
||||
"multiple-integrated" => Ok(()), // preferred for performance
|
||||
"generic" => Ok(()), // allows using embassy-time without the embassy executors
|
||||
_ => Err(Error::Validation(format!("Expected 'single-integrated', 'multiple-integrated' or 'generic', found {string}")))
|
||||
}
|
||||
})))
|
||||
},
|
||||
ConfigOption {
|
||||
name: "generic-queue-size",
|
||||
description: "The capacity of the queue when the `generic` timer queue flavour is selected.",
|
||||
default_value: Value::Integer(64),
|
||||
constraint: Some(Validator::PositiveInteger),
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
|
122
esp-hal/build.rs
122
esp-hal/build.rs
@ -9,7 +9,7 @@ use std::{
|
||||
};
|
||||
|
||||
use esp_build::assert_unique_used_features;
|
||||
use esp_config::{generate_config, Validator, Value};
|
||||
use esp_config::{generate_config, ConfigOption, Validator, Value};
|
||||
use esp_metadata::{Chip, Config};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@ -69,89 +69,89 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
// emit config
|
||||
let cfg = generate_config("esp_hal", &[
|
||||
(
|
||||
"place-spi-driver-in-ram",
|
||||
"Places the SPI driver in RAM for better performance",
|
||||
Value::Bool(false),
|
||||
None
|
||||
),
|
||||
(
|
||||
"place-switch-tables-in-ram",
|
||||
"Places switch-tables, some lookup tables and constants related to interrupt handling into RAM - resulting in better performance but slightly more RAM consumption.",
|
||||
Value::Bool(true),
|
||||
None
|
||||
),
|
||||
(
|
||||
"place-anon-in-ram",
|
||||
"Places anonymous symbols into RAM - resulting in better performance at the cost of significant more RAM consumption. Best to be combined with `place-switch-tables-in-ram`.",
|
||||
Value::Bool(false),
|
||||
None
|
||||
),
|
||||
ConfigOption {
|
||||
name: "place-spi-driver-in-ram",
|
||||
description: "Places the SPI driver in RAM for better performance",
|
||||
default_value: Value::Bool(false),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "place-switch-tables-in-ram",
|
||||
description: "Places switch-tables, some lookup tables and constants related to interrupt handling into RAM - resulting in better performance but slightly more RAM consumption.",
|
||||
default_value: Value::Bool(true),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "place-anon-in-ram",
|
||||
description: "Places anonymous symbols into RAM - resulting in better performance at the cost of significant more RAM consumption. Best to be combined with `place-switch-tables-in-ram`.",
|
||||
default_value: Value::Bool(false),
|
||||
constraint: None
|
||||
},
|
||||
// Ideally, we should be able to set any clock frequency for any chip. However, currently
|
||||
// only the 32 and C2 implements any sort of configurability, and the rest have a fixed
|
||||
// clock frequeny.
|
||||
// TODO: only show this configuration for chips that have multiple valid options.
|
||||
(
|
||||
"xtal-frequency",
|
||||
"The frequency of the crystal oscillator, in MHz. Set to `auto` to automatically detect the frequency. `auto` may not be able to identify the clock frequency in some cases. Also, configuring a specific frequency may increase performance slightly.",
|
||||
Value::String(match device_name {
|
||||
ConfigOption {
|
||||
name: "xtal-frequency",
|
||||
description: "The frequency of the crystal oscillator, in MHz. Set to `auto` to automatically detect the frequency. `auto` may not be able to identify the clock frequency in some cases. Also, configuring a specific frequency may increase performance slightly.",
|
||||
default_value: Value::String(match device_name {
|
||||
"esp32" | "esp32c2" => String::from("auto"),
|
||||
// The rest has only one option
|
||||
"esp32c3" | "esp32c6" | "esp32s2" | "esp32s3" => String::from("40"),
|
||||
"esp32h2" => String::from("32"),
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
Some(Validator::Enumeration(match device_name {
|
||||
constraint:Some(Validator::Enumeration(match device_name {
|
||||
"esp32" | "esp32c2" => vec![String::from("auto"), String::from("26"), String::from("40")],
|
||||
// The rest has only one option
|
||||
"esp32c3" | "esp32c6" | "esp32s2" | "esp32s3" => vec![String::from("40")],
|
||||
"esp32h2" => vec![String::from("32")],
|
||||
_ => unreachable!(),
|
||||
})),
|
||||
),
|
||||
},
|
||||
// ideally we should only offer this for ESP32 but the config system doesn't
|
||||
// support per target configs, yet
|
||||
(
|
||||
"spi-address-workaround",
|
||||
"(ESP32 only) Enables a workaround for the issue where SPI in half-duplex mode incorrectly transmits the address on a single line if the data buffer is empty.",
|
||||
Value::Bool(true),
|
||||
None
|
||||
),
|
||||
ConfigOption {
|
||||
name: "spi-address-workaround",
|
||||
description: "(ESP32 only) Enables a workaround for the issue where SPI in half-duplex mode incorrectly transmits the address on a single line if the data buffer is empty.",
|
||||
default_value: Value::Bool(true),
|
||||
constraint: None
|
||||
},
|
||||
// ideally we should only offer this for ESP32-C6/ESP32-H2 but the config system doesn't support per target configs, yet
|
||||
(
|
||||
"flip-link",
|
||||
"(ESP32-C6/ESP32-H2 only): Move the stack to start of RAM to get zero-cost stack overflow protection.",
|
||||
Value::Bool(false),
|
||||
None
|
||||
),
|
||||
ConfigOption {
|
||||
name: "flip-link",
|
||||
description: "(ESP32-C6/ESP32-H2 only): Move the stack to start of RAM to get zero-cost stack overflow protection.",
|
||||
default_value: Value::Bool(false),
|
||||
constraint: None
|
||||
},
|
||||
// ideally we should only offer this for ESP32, ESP32-S2 and `octal` only for ESP32-S3 but the config system doesn't support per target configs, yet
|
||||
(
|
||||
"psram-mode",
|
||||
"(ESP32, ESP32-S2 and ESP32-S3 only, `octal` is only supported for ESP32-S3) SPIRAM chip mode",
|
||||
Value::String(String::from("quad")),
|
||||
Some(Validator::Enumeration(
|
||||
ConfigOption {
|
||||
name: "psram-mode",
|
||||
description: "(ESP32, ESP32-S2 and ESP32-S3 only, `octal` is only supported for ESP32-S3) SPIRAM chip mode",
|
||||
default_value: Value::String(String::from("quad")),
|
||||
constraint: Some(Validator::Enumeration(
|
||||
vec![String::from("quad"), String::from("octal")]
|
||||
)),
|
||||
),
|
||||
},
|
||||
// Rust's stack smashing protection configuration
|
||||
(
|
||||
"stack-guard-offset",
|
||||
"The stack guard variable will be placed this many bytes from the stack's end.",
|
||||
Value::Integer(4096),
|
||||
None
|
||||
),
|
||||
(
|
||||
"stack-guard-value",
|
||||
"The value to be written to the stack guard variable.",
|
||||
Value::Integer(0xDEED_BAAD),
|
||||
None
|
||||
),
|
||||
(
|
||||
"impl-critical-section",
|
||||
"Provide a `critical-section` implementation. Note that if disabled, you will need to provide a `critical-section` implementation which is using `critical-section/restore-state-u32`.",
|
||||
Value::Bool(true),
|
||||
None
|
||||
),
|
||||
ConfigOption {
|
||||
name: "stack-guard-offset",
|
||||
description: "The stack guard variable will be placed this many bytes from the stack's end.",
|
||||
default_value: Value::Integer(4096),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "stack-guard-value",
|
||||
description: "The value to be written to the stack guard variable.",
|
||||
default_value: Value::Integer(0xDEED_BAAD),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "impl-critical-section",
|
||||
description: "Provide a `critical-section` implementation. Note that if disabled, you will need to provide a `critical-section` implementation which is using `critical-section/restore-state-u32`.",
|
||||
default_value: Value::Bool(true),
|
||||
constraint: None
|
||||
},
|
||||
], true);
|
||||
|
||||
// RISC-V and Xtensa devices each require some special handling and processing
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use esp_config::{generate_config, Validator, Value};
|
||||
use esp_config::{generate_config, ConfigOption, Validator, Value};
|
||||
|
||||
fn main() {
|
||||
let out = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
@ -9,12 +9,12 @@ fn main() {
|
||||
// emit config
|
||||
generate_config(
|
||||
"esp_ieee802154",
|
||||
&[(
|
||||
"rx_queue_size",
|
||||
"Size of the RX queue in frames",
|
||||
Value::Integer(50),
|
||||
Some(Validator::PositiveInteger),
|
||||
)],
|
||||
&[ConfigOption {
|
||||
name: "rx_queue_size",
|
||||
description: "Size of the RX queue in frames",
|
||||
default_value: Value::Integer(50),
|
||||
constraint: Some(Validator::PositiveInteger),
|
||||
}],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::{error::Error, str::FromStr};
|
||||
|
||||
use esp_build::assert_unique_used_features;
|
||||
use esp_config::{generate_config, Validator, Value};
|
||||
use esp_config::{generate_config, ConfigOption, Validator, Value};
|
||||
use esp_metadata::{Chip, Config};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@ -99,128 +99,138 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
generate_config(
|
||||
"esp_wifi",
|
||||
&[
|
||||
("rx_queue_size", "Size of the RX queue in frames", Value::Integer(5), Some(Validator::PositiveInteger)),
|
||||
("tx_queue_size", "Size of the TX queue in frames", Value::Integer(3), Some(Validator::PositiveInteger)),
|
||||
(
|
||||
"static_rx_buf_num",
|
||||
"WiFi static RX buffer number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
Value::Integer(10),
|
||||
None
|
||||
),
|
||||
(
|
||||
"dynamic_rx_buf_num",
|
||||
"WiFi dynamic RX buffer number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
Value::Integer(32),
|
||||
None
|
||||
),
|
||||
(
|
||||
"static_tx_buf_num",
|
||||
"WiFi static TX buffer number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
Value::Integer(0),
|
||||
None
|
||||
),
|
||||
(
|
||||
"dynamic_tx_buf_num",
|
||||
"WiFi dynamic TX buffer number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
Value::Integer(32),
|
||||
None
|
||||
),
|
||||
(
|
||||
"ampdu_rx_enable",
|
||||
"WiFi AMPDU RX feature enable flag. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
Value::Bool(true),
|
||||
None
|
||||
),
|
||||
(
|
||||
"ampdu_tx_enable",
|
||||
"WiFi AMPDU TX feature enable flag. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
Value::Bool(true),
|
||||
None
|
||||
),
|
||||
(
|
||||
"amsdu_tx_enable",
|
||||
"WiFi AMSDU TX feature enable flag. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
Value::Bool(false),
|
||||
None
|
||||
),
|
||||
(
|
||||
"rx_ba_win",
|
||||
"WiFi Block Ack RX window size. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
Value::Integer(6),
|
||||
None
|
||||
),
|
||||
(
|
||||
"max_burst_size",
|
||||
"See [smoltcp's documentation](https://docs.rs/smoltcp/0.10.0/smoltcp/phy/struct.DeviceCapabilities.html#structfield.max_burst_size)",
|
||||
Value::Integer(1),
|
||||
None
|
||||
),
|
||||
(
|
||||
"country_code",
|
||||
"Country code. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-country-code)",
|
||||
Value::String("CN".to_owned()),
|
||||
None
|
||||
),
|
||||
(
|
||||
"country_code_operating_class",
|
||||
"If not 0: Operating Class table number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-country-code)",
|
||||
Value::Integer(0),
|
||||
None
|
||||
),
|
||||
(
|
||||
"mtu",
|
||||
"MTU, see [smoltcp's documentation](https://docs.rs/smoltcp/0.10.0/smoltcp/phy/struct.DeviceCapabilities.html#structfield.max_transmission_unit)",
|
||||
Value::Integer(1492),
|
||||
None
|
||||
),
|
||||
(
|
||||
"tick_rate_hz",
|
||||
"Tick rate of the internal task scheduler in hertz",
|
||||
Value::Integer(100),
|
||||
None
|
||||
),
|
||||
(
|
||||
"listen_interval",
|
||||
"Interval for station to listen to beacon from AP. The unit of listen interval is one beacon interval. For example, if beacon interval is 100 ms and listen interval is 3, the interval for station to listen to beacon is 300 ms",
|
||||
Value::Integer(3),
|
||||
None
|
||||
),
|
||||
(
|
||||
"beacon_timeout",
|
||||
"For Station, If the station does not receive a beacon frame from the connected SoftAP during the inactive time, disconnect from SoftAP. Default 6s. Range 6-30",
|
||||
Value::Integer(6),
|
||||
None
|
||||
),
|
||||
(
|
||||
"ap_beacon_timeout",
|
||||
"For SoftAP, If the SoftAP doesn't receive any data from the connected STA during inactive time, the SoftAP will force deauth the STA. Default is 300s",
|
||||
Value::Integer(300),
|
||||
None
|
||||
),
|
||||
(
|
||||
"failure_retry_cnt",
|
||||
"Number of connection retries station will do before moving to next AP. scan_method should be set as WIFI_ALL_CHANNEL_SCAN to use this config. Note: Enabling this may cause connection time to increase incase best AP doesn't behave properly. Defaults to 1",
|
||||
Value::Integer(1),
|
||||
None
|
||||
),
|
||||
(
|
||||
"scan_method",
|
||||
"0 = WIFI_FAST_SCAN, 1 = WIFI_ALL_CHANNEL_SCAN, defaults to 0",
|
||||
Value::Integer(0),
|
||||
None
|
||||
),
|
||||
(
|
||||
"dump_packets",
|
||||
"Dump packets via an info log statement",
|
||||
Value::Bool(false),
|
||||
None
|
||||
),
|
||||
(
|
||||
"phy_enable_usb",
|
||||
"Keeps USB running when using WiFi. This allows debugging and log messages via USB Serial JTAG. Turn off for best WiFi performance.",
|
||||
Value::Bool(true),
|
||||
None
|
||||
),
|
||||
ConfigOption {
|
||||
name: "rx_queue_size",
|
||||
description: "Size of the RX queue in frames",
|
||||
default_value: Value::Integer(5),
|
||||
constraint: Some(Validator::PositiveInteger)
|
||||
},
|
||||
ConfigOption {
|
||||
name: "tx_queue_size",
|
||||
description: "Size of the TX queue in frames",
|
||||
default_value: Value::Integer(3),
|
||||
constraint: Some(Validator::PositiveInteger)
|
||||
},
|
||||
ConfigOption {
|
||||
name: "static_rx_buf_num",
|
||||
description: "WiFi static RX buffer number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
default_value: Value::Integer(10),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "dynamic_rx_buf_num",
|
||||
description: "WiFi dynamic RX buffer number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
default_value: Value::Integer(32),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "static_tx_buf_num",
|
||||
description: "WiFi static TX buffer number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
default_value: Value::Integer(0),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "dynamic_tx_buf_num",
|
||||
description: "WiFi dynamic TX buffer number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
default_value: Value::Integer(32),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "ampdu_rx_enable",
|
||||
description: "WiFi AMPDU RX feature enable flag. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
default_value: Value::Bool(true),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "ampdu_tx_enable",
|
||||
description: "WiFi AMPDU TX feature enable flag. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
default_value: Value::Bool(true),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "amsdu_tx_enable",
|
||||
description: "WiFi AMSDU TX feature enable flag. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
default_value: Value::Bool(false),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "rx_ba_win",
|
||||
description: "WiFi Block Ack RX window size. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv418wifi_init_config_t)",
|
||||
default_value: Value::Integer(6),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "max_burst_size",
|
||||
description: "See [smoltcp's documentation](https://docs.rs/smoltcp/0.10.0/smoltcp/phy/struct.DeviceCapabilities.html#structfield.max_burst_size)",
|
||||
default_value: Value::Integer(1),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "country_code",
|
||||
description: "Country code. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-country-code)",
|
||||
default_value: Value::String("CN".to_owned()),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "country_code_operating_class",
|
||||
description: "If not 0: Operating Class table number. See [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-country-code)",
|
||||
default_value: Value::Integer(0),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "mtu",
|
||||
description: "MTU, see [smoltcp's documentation](https://docs.rs/smoltcp/0.10.0/smoltcp/phy/struct.DeviceCapabilities.html#structfield.max_transmission_unit)",
|
||||
default_value: Value::Integer(1492),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "tick_rate_hz",
|
||||
description: "Tick rate of the internal task scheduler in hertz",
|
||||
default_value: Value::Integer(100),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "listen_interval",
|
||||
description: "Interval for station to listen to beacon from AP. The unit of listen interval is one beacon interval. For example, if beacon interval is 100 ms and listen interval is 3, the interval for station to listen to beacon is 300 ms",
|
||||
default_value: Value::Integer(3),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "beacon_timeout",
|
||||
description: "For Station, If the station does not receive a beacon frame from the connected SoftAP during the inactive time, disconnect from SoftAP. Default 6s. Range 6-30",
|
||||
default_value: Value::Integer(6),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "ap_beacon_timeout",
|
||||
description: "For SoftAP, If the SoftAP doesn't receive any data from the connected STA during inactive time, the SoftAP will force deauth the STA. Default is 300s",
|
||||
default_value: Value::Integer(300),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "failure_retry_cnt",
|
||||
description: "Number of connection retries station will do before moving to next AP. scan_method should be set as WIFI_ALL_CHANNEL_SCAN to use this config. Note: Enabling this may cause connection time to increase incase best AP doesn't behave properly. Defaults to 1",
|
||||
default_value: Value::Integer(1),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "scan_method",
|
||||
description: "0 = WIFI_FAST_SCAN, 1 = WIFI_ALL_CHANNEL_SCAN, defaults to 0",
|
||||
default_value: Value::Integer(0),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "dump_packets",
|
||||
description: "Dump packets via an info log statement",
|
||||
default_value: Value::Bool(false),
|
||||
constraint: None
|
||||
},
|
||||
ConfigOption {
|
||||
name: "phy_enable_usb",
|
||||
description: "Keeps USB running when using WiFi. This allows debugging and log messages via USB Serial JTAG. Turn off for best WiFi performance.",
|
||||
default_value: Value::Bool(true),
|
||||
constraint: None
|
||||
},
|
||||
],
|
||||
true
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user