Repo of no-std crates for my personal embedded projects

feat: Config format tweak with stronger type validation

Fixes the config format to require values to be strings, so to avoid problems with toml Value not being able to accommodate
certain types, but then adds parsing according to the declared type to ensure the values are valid before being
formatted into constants.

+94 -15
+4 -1
.tangled/workflows/test.yml
··· 10 10 - cargo 11 11 - rustfmt 12 12 - protobuf 13 + - cargo-nextest 13 14 14 15 steps: 15 16 - name: Format check 16 17 command: cargo fmt --all --check 17 18 - name: Tests 18 - command: cargo test --workspace --locked 19 + command: cargo nextest run --workspace --locked --no-fail-fast 20 + - name: Doc Tests 21 + command: cargo test --workspace --locked --doc --no-fail-fast
+90 -14
sachy-config/src/lib.rs
··· 1 + use core::{error::Error, fmt::Debug, str::FromStr}; 1 2 use std::io::Write; 2 3 3 - use miette::{IntoDiagnostic, Result, miette}; 4 + use miette::{Context, IntoDiagnostic, Result, bail, miette}; 5 + use toml_edit::Value; 6 + 7 + fn validate_kind_to_string<T: Debug + FromStr + Send + Sync>( 8 + name: &str, 9 + kind: &str, 10 + value: &Value, 11 + ) -> Result<String> 12 + where 13 + T::Err: Send + Sync + Error + 'static, 14 + { 15 + value 16 + .as_str() 17 + .map_or_else( 18 + || Err(miette!("{} is not a value", value)), 19 + |value_str| { 20 + value_str.parse().into_diagnostic().wrap_err_with(|| { 21 + format!("Parsing failure of constant \"{}\" as {}", name, kind) 22 + }) 23 + }, 24 + ) 25 + .map(|value: T| format!("pub const {}: {} = {:?};\n", name, kind, value)) 26 + } 4 27 5 28 pub fn output_config<W: Write>(config: &str, mut writer: W) -> Result<()> { 6 29 let output = parse_config(config)?; ··· 55 78 ) 56 79 })?; 57 80 58 - let constant_output = format!( 59 - "pub const {}: {} = {};\n", 60 - name, 61 - kind, 62 - value.clone().decorated("", "") 63 - ); 64 - output.push_str(&constant_output); 81 + let line = match kind { 82 + "u8" => validate_kind_to_string::<u8>(name, kind, value)?, 83 + "i8" => validate_kind_to_string::<i8>(name, kind, value)?, 84 + "u16" => validate_kind_to_string::<u16>(name, kind, value)?, 85 + "i16" => validate_kind_to_string::<i16>(name, kind, value)?, 86 + "u32" => validate_kind_to_string::<u32>(name, kind, value)?, 87 + "i32" => validate_kind_to_string::<i32>(name, kind, value)?, 88 + "u64" => validate_kind_to_string::<u64>(name, kind, value)?, 89 + "i64" => validate_kind_to_string::<i64>(name, kind, value)?, 90 + "u128" => validate_kind_to_string::<u128>(name, kind, value)?, 91 + "i128" => validate_kind_to_string::<i128>(name, kind, value)?, 92 + "f32" => validate_kind_to_string::<f32>(name, kind, value)?, 93 + "f64" => validate_kind_to_string::<f64>(name, kind, value)?, 94 + "&str" => bail!("{} is a {}, use [statics] for these", name, kind), 95 + _ => bail!("Unsupported type: {}", kind), 96 + }; 97 + 98 + output.push_str(&line); 99 + 65 100 Ok(()) 66 101 })?; 67 102 ··· 77 112 #[test] 78 113 fn it_parses_config() -> Result<()> { 79 114 let input = r#"[constants] 80 - FIRST = { type = "u8", value = 2 } 81 - VALUE = { type = "f32", value = 42.0 } 115 + FIRST = { type = "u8", value = "2" } 116 + VALUE = { type = "f32", value = "42.0" } 82 117 [statics] 83 118 EXAMPLE = "thing" 84 119 "#; ··· 95 130 } 96 131 97 132 #[test] 133 + fn it_validates_value_types() -> Result<()> { 134 + let input = r#"[constants] 135 + FIRST = { type = "u8", value = "1234" } 136 + VALUE = { type = "f32", value = "42.0" } 137 + [statics] 138 + EXAMPLE = "thing" 139 + "#; 140 + 141 + let output = parse_config(input); 142 + 143 + let report = output.expect_err("Output was somehow successful"); 144 + 145 + let mut error_chain = report.chain(); 146 + 147 + assert_eq!( 148 + "Parsing failure of constant \"FIRST\" as u8", 149 + error_chain.next().unwrap().to_string() 150 + ); 151 + assert_eq!( 152 + "number too large to fit in target type", 153 + error_chain.next().unwrap().to_string() 154 + ); 155 + assert!(error_chain.next().is_none()); 156 + 157 + Ok(()) 158 + } 159 + 160 + #[test] 161 + fn it_disallows_string_constants() { 162 + let input = r#"[constants] 163 + FIRST = { type = "&str", value = "42" } 164 + "#; 165 + 166 + let expected = "FIRST is a &str, use [statics] for these"; 167 + 168 + let output = parse_config(input).expect_err("Output somehow successfully parsed"); 169 + 170 + assert_eq!(output.to_string(), expected); 171 + } 172 + 173 + #[test] 98 174 fn it_handles_only_statics() -> Result<()> { 99 175 let input = r#"[statics] 100 176 EXAMPLE = "thing" ··· 112 188 #[test] 113 189 fn it_handles_only_constants() -> Result<()> { 114 190 let input = r#"[constants] 115 - FIRST = { type = "u8", value = 42 } 191 + FIRST = { type = "u8", value = "42" } 116 192 "#; 117 193 118 194 let expected = "pub const FIRST: u8 = 42;\n"; ··· 149 225 #[test] 150 226 fn it_outputs_to_file() -> Result<()> { 151 227 let input = r#"[constants] 152 - FIRST = { type = "u8", value = 2 } 153 - VALUE = { type = "u64", value = 42 } 228 + FIRST = { type = "u128", value = "340282366920938463463374607431768211455" } 229 + VALUE = { type = "u64", value = "42" } 154 230 [statics] 155 231 EXAMPLE = "thing" 156 232 "#; 157 233 let expected = r#"pub static EXAMPLE: &str = "thing"; 158 - pub const FIRST: u8 = 2; 234 + pub const FIRST: u128 = 340282366920938463463374607431768211455; 159 235 pub const VALUE: u64 = 42; 160 236 "#; 161 237 let output: Vec<u8> = Vec::new();