feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
+1
@@ -0,0 +1 @@
|
||||
{"files":{".cargo_vcs_info.json":"82f70552968381e1635b5f5efe2ca09291cbee8e671603306dd2f54664396a66","CHANGELOG.md":"ee6e9c5a4716493253ae8e86d3c4d6a070867921a1dc2013c4a8c7d0378ba6c2","Cargo.toml":"f231f7990314682ebcd56dfb246d2a8fef68bd21b29ef7b09646af6f5e42eb8f","Cargo.toml.orig":"24c10b01ca27e37da715d58c5a82c7447a81a4c87ccb9c91e67e92fbc2a79c49","README.md":"5e3f5bcefe06c905bc3712b5e16e1789de20c074f90eb7f13b04ed4523a89914","src/generate.rs":"a0b66f8721f5b5cbe751169b835600e5d1866a62b1a57d1161939093dfe0013f","src/lib.rs":"2039bbbfbfab670e418a636c880a76b8aaff500b6659af31a3be2f50ec0d76ba"},"package":"f7584e4cd1dac06201fd92fff1c84b396be5458ac4d93e9457e7a89b1b42c60e"}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "bdfec3781fd59c4d51173706e3cbee3760bbf8b6"
|
||||
},
|
||||
"path_in_vcs": "esp-config"
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
### Fixed
|
||||
|
||||
### Changed
|
||||
|
||||
### Removed
|
||||
|
||||
## 0.2.0 - 2024-11-20
|
||||
|
||||
### Added
|
||||
|
||||
- Add configuration validation (#2475)
|
||||
|
||||
## 0.1.0 - 2024-10-10
|
||||
|
||||
- Initial release
|
||||
|
||||
[Unreleased]: https://github.com/esp-rs/esp-hal/commits/main/esp-config?since=2024-11-20
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.79.0"
|
||||
name = "esp-config"
|
||||
version = "0.2.0"
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Configure projects using esp-hal and related packages"
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/esp-rs/esp-hal"
|
||||
|
||||
[lib]
|
||||
name = "esp_config"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.document-features]
|
||||
version = "0.2.10"
|
||||
|
||||
[dev-dependencies.temp-env]
|
||||
version = "0.3.6"
|
||||
|
||||
[features]
|
||||
build = []
|
||||
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
# esp-config
|
||||
|
||||
[](https://crates.io/crates/esp-config)
|
||||
[](https://docs.rs/esp-config)
|
||||

|
||||

|
||||
[](https://matrix.to/#/#esp-rs:matrix.org)
|
||||
|
||||
## [Documentation](https://docs.rs/crate/esp-config)
|
||||
|
||||
## Usage
|
||||
|
||||
`esp-config` takes a prefix (usually the crate name) and a set of configuration keys and default values to produce a configuration system that supports:
|
||||
|
||||
- Emitting rustc cfg's for boolean keys
|
||||
- Emitting environment variables for numbers
|
||||
- Along with decimal parsing, it supports Hex, Octal and Binary with the respective `0x`, `0o` and `0b` prefixes.
|
||||
- Emitting environment variables string values
|
||||
|
||||
### Viewing the configuration
|
||||
|
||||
The possible configuration values are output as a markdown table in the crates `OUT_DIR` with the format `{prefix}_config_table.md`, this can then be included into the crates top level documentation. Here is an example of the output:
|
||||
|
||||
|
||||
| Name | Description | Default value |
|
||||
|------|-------------|---------------|
|
||||
|**ESP_HAL_PLACE_SPI_DRIVER_IN_RAM**|Places the SPI driver in RAM for better performance|false|
|
||||
|
||||
### Setting configuration options
|
||||
|
||||
For any available configuration option, the environment variable or cfg is _always_ set based on the default value specified in the table. Users can override this by setting environment variables locally in their shell _or_ the preferred option is to utilize cargo's [`env` section](https://doc.rust-lang.org/cargo/reference/config.html#env).
|
||||
|
||||
It's important to note that due to a [bug in cargo](https://github.com/rust-lang/cargo/issues/10358), any modifications to the environment, local or otherwise will only get picked up on a full clean build of the project.
|
||||
|
||||
To see the final selected configuration another table is output to the `OUT_DIR` with the format `{prefix}_selected_config.md`.
|
||||
|
||||
### Capturing configuration values in the downstream crate
|
||||
|
||||
For all supported data types, there are helper macros that emit `const` code for parsing the configuration values.
|
||||
|
||||
- Numbers - `esp_config_int!(integer_type, "ENV")`
|
||||
- Strings - `esp_config_str!("ENV")`
|
||||
- Bool - `esp_config_bool!("ENV")`
|
||||
|
||||
In addition to environment variables, for boolean types rust `cfg`'s are emitted in snake case _without_ the prefix.
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
This crate is guaranteed to compile on stable Rust 1.79 and up. It _might_
|
||||
compile with older versions but that may change in any new patch release.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of:
|
||||
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](../LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT license ([LICENSE-MIT](../LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in
|
||||
the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without
|
||||
any additional terms or conditions.
|
||||
Vendored
+685
@@ -0,0 +1,685 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
fmt::{self, Write as _},
|
||||
fs,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
const DOC_TABLE_HEADER: &str = r#"
|
||||
| Name | Description | Default 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)]
|
||||
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.
|
||||
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>),
|
||||
/// A custom validation function to run against any supported value type.
|
||||
Custom(Box<dyn Fn(&Value) -> Result<(), Error>>),
|
||||
}
|
||||
|
||||
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::Custom(validator_fn) => validator_fn(value)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
prefix: &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.
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
#[cfg(not(test))]
|
||||
env_change_work_around();
|
||||
|
||||
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 = screaming_snake_case(prefix);
|
||||
|
||||
// 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(&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();
|
||||
}
|
||||
}
|
||||
|
||||
emit_configuration(&prefix, &configs, &mut selected_config);
|
||||
|
||||
if emit_md_tables {
|
||||
let file_name = snake_case(&prefix);
|
||||
write_config_tables(&file_name, doc_table, selected_config);
|
||||
}
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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() {
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
dotcargo.join("config.toml").display()
|
||||
);
|
||||
}
|
||||
if dotcargo.join("config").exists() {
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
dotcargo.join("config").display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config(
|
||||
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));
|
||||
configs.insert(name.clone(), default.clone());
|
||||
|
||||
// Write documentation table line:
|
||||
let default = default.to_string();
|
||||
writeln!(doc_table, "|**{name}**|{description}|{default}|").unwrap();
|
||||
|
||||
// Rebuild if config environment variable changed:
|
||||
println!("cargo:rerun-if-env-changed={name}");
|
||||
}
|
||||
|
||||
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.strip_prefix(prefix).is_some() {
|
||||
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(
|
||||
prefix: &str,
|
||||
configs: &HashMap<String, Value>,
|
||||
selected_config: &mut String,
|
||||
) {
|
||||
for (name, value) in configs.iter() {
|
||||
let cfg_name = snake_case(name.trim_start_matches(&format!("{prefix}_")));
|
||||
println!("cargo:rustc-check-cfg=cfg({cfg_name})");
|
||||
|
||||
if let Value::Bool(true) = value {
|
||||
println!("cargo:rustc-cfg={cfg_name}");
|
||||
}
|
||||
|
||||
let value = value.to_string();
|
||||
|
||||
// Values that haven't been seen will be output here with the default value:
|
||||
println!("cargo:rustc-env={}={}", name, value);
|
||||
writeln!(selected_config, "|**{name}**|{value}|").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
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_NUMBER", Some("0xaa")),
|
||||
("ESP_TEST_NUMBER_SIGNED", Some("-999")),
|
||||
("ESP_TEST_STRING", Some("Hello world!")),
|
||||
("ESP_TEST_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_NUMBER").unwrap() {
|
||||
Value::Integer(num) => *num,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
0xaa
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_NUMBER_SIGNED").unwrap() {
|
||||
Value::Integer(num) => *num,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
-999
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_STRING").unwrap() {
|
||||
Value::String(val) => val,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"Hello world!"
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_BOOL").unwrap() {
|
||||
Value::Bool(val) => *val,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// the rest are the defaults
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_NUMBER_DEFAULT").unwrap() {
|
||||
Value::Integer(num) => *num,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
999
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_STRING_DEFAULT").unwrap() {
|
||||
Value::String(val) => val,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"Demo"
|
||||
);
|
||||
assert_eq!(
|
||||
match configs.get("ESP_TEST_BOOL_DEFAULT").unwrap() {
|
||||
Value::Bool(val) => *val,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_validation_passes() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("ESP_TEST_POSITIVE_NUMBER", Some("7")),
|
||||
("ESP_TEST_NEGATIVE_NUMBER", Some("-1")),
|
||||
("ESP_TEST_NON_NEGATIVE_NUMBER", Some("0")),
|
||||
("ESP_TEST_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_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_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_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_NUMBER", Some("0xaa")),
|
||||
("ESP_TEST_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_NUMBER", Some("Hello world"))], || {
|
||||
generate_config(
|
||||
"esp-test",
|
||||
&[("number", "NA", Value::Integer(999), None)],
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+110
@@ -0,0 +1,110 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
//! ## Feature Flags
|
||||
#![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/46717278")]
|
||||
#![cfg_attr(not(feature = "build"), no_std)]
|
||||
#![deny(missing_docs, rust_2018_idioms)]
|
||||
|
||||
#[cfg(feature = "build")]
|
||||
mod generate;
|
||||
#[cfg(feature = "build")]
|
||||
pub use generate::{generate_config, Error, Validator, Value};
|
||||
|
||||
/// Parse the value of an environment variable as a [bool] at compile time.
|
||||
#[macro_export]
|
||||
macro_rules! esp_config_bool {
|
||||
( $var:expr ) => {
|
||||
match env!($var).as_bytes() {
|
||||
b"true" => true,
|
||||
b"false" => false,
|
||||
_ => ::core::panic!("boolean value must be either 'true' or 'false'"),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: From 1.82 on, we can use `<$ty>::from_str_radix(env!($var), 10)`
|
||||
/// Parse the value of an environment variable as an integer at compile time.
|
||||
#[macro_export]
|
||||
macro_rules! esp_config_int {
|
||||
( $ty:ty, $var:expr ) => {
|
||||
const {
|
||||
const BYTES: &[u8] = env!($var).as_bytes();
|
||||
esp_config_int_parse!($ty, BYTES)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the string value of an environment variable at compile time.
|
||||
#[macro_export]
|
||||
macro_rules! esp_config_str {
|
||||
( $var:expr ) => {
|
||||
env!($var)
|
||||
};
|
||||
}
|
||||
|
||||
/// Parse a string like "777" into an integer, which _can_ be used in a `const`
|
||||
/// context
|
||||
#[doc(hidden)] // To avoid confusion with `esp_config_int`, hide this in the docs
|
||||
#[macro_export]
|
||||
macro_rules! esp_config_int_parse {
|
||||
( $ty:ty, $bytes:expr ) => {{
|
||||
let mut bytes = $bytes;
|
||||
let mut val: $ty = 0;
|
||||
let mut sign_seen = false;
|
||||
let mut is_negative = false;
|
||||
|
||||
while let [byte, rest @ ..] = bytes {
|
||||
match *byte {
|
||||
b'0'..=b'9' => {
|
||||
val = val * 10 + (*byte - b'0') as $ty;
|
||||
}
|
||||
b'-' | b'+' if !sign_seen => {
|
||||
is_negative = *byte == b'-';
|
||||
sign_seen = true;
|
||||
}
|
||||
_ => ::core::panic!("invalid character encountered while parsing integer"),
|
||||
}
|
||||
|
||||
bytes = rest;
|
||||
}
|
||||
|
||||
if is_negative {
|
||||
let original = val;
|
||||
// Subtract the value twice to get a negative:
|
||||
val -= original;
|
||||
val -= original;
|
||||
}
|
||||
|
||||
val
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// We can only test success in the const context
|
||||
const _: () = {
|
||||
core::assert!(esp_config_int_parse!(i64, "-77777".as_bytes()) == -77777);
|
||||
core::assert!(esp_config_int_parse!(isize, "-7777".as_bytes()) == -7777);
|
||||
core::assert!(esp_config_int_parse!(i32, "-999".as_bytes()) == -999);
|
||||
core::assert!(esp_config_int_parse!(i16, "-99".as_bytes()) == -99);
|
||||
core::assert!(esp_config_int_parse!(i8, "-9".as_bytes()) == -9);
|
||||
|
||||
core::assert!(esp_config_int_parse!(u64, "77777".as_bytes()) == 77777);
|
||||
core::assert!(esp_config_int_parse!(usize, "7777".as_bytes()) == 7777);
|
||||
core::assert!(esp_config_int_parse!(u32, "999".as_bytes()) == 999);
|
||||
core::assert!(esp_config_int_parse!(u16, "99".as_bytes()) == 99);
|
||||
core::assert!(esp_config_int_parse!(u8, "9".as_bytes()) == 9);
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_expect_positive() {
|
||||
esp_config_int_parse!(u8, "-5".as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_digit() {
|
||||
esp_config_int_parse!(u32, "a".as_bytes());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user