From 3456a20b000315d727baab5adcdfd42f4ef485ee Mon Sep 17 00:00:00 2001 From: Sachymetsu Date: Sat, 24 Jan 2026 09:21:42 +0000 Subject: [PATCH] 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. --- .tangled/workflows/test.yml | 5 ++++- sachy-config/src/lib.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- 2 file(s) changed, 94 insertion(s)(+), 15 deletion(s)(-) diff --git a/.tangled/workflows/test.yml b/.tangled/workflows/test.yml --- a/.tangled/workflows/test.yml +++ b/.tangled/workflows/test.yml @@ -10,9 +10,12 @@ - cargo - rustfmt - protobuf + - cargo-nextest steps: - name: Format check command: cargo fmt --all --check - name: Tests - command: cargo test --workspace --locked + command: cargo nextest run --workspace --locked --no-fail-fast + - name: Doc Tests + command: cargo test --workspace --locked --doc --no-fail-fast diff --git a/sachy-config/src/lib.rs b/sachy-config/src/lib.rs --- a/sachy-config/src/lib.rs +++ b/sachy-config/src/lib.rs @@ -1,6 +1,29 @@ +use core::{error::Error, fmt::Debug, str::FromStr}; use std::io::Write; -use miette::{IntoDiagnostic, Result, miette}; +use miette::{Context, IntoDiagnostic, Result, bail, miette}; +use toml_edit::Value; + +fn validate_kind_to_string( + name: &str, + kind: &str, + value: &Value, +) -> Result +where + T::Err: Send + Sync + Error + 'static, +{ + value + .as_str() + .map_or_else( + || Err(miette!("{} is not a value", value)), + |value_str| { + value_str.parse().into_diagnostic().wrap_err_with(|| { + format!("Parsing failure of constant \"{}\" as {}", name, kind) + }) + }, + ) + .map(|value: T| format!("pub const {}: {} = {:?};\n", name, kind, value)) +} pub fn output_config(config: &str, mut writer: W) -> Result<()> { let output = parse_config(config)?; @@ -55,13 +78,25 @@ ) })?; - let constant_output = format!( - "pub const {}: {} = {};\n", - name, - kind, - value.clone().decorated("", "") - ); - output.push_str(&constant_output); + let line = match kind { + "u8" => validate_kind_to_string::(name, kind, value)?, + "i8" => validate_kind_to_string::(name, kind, value)?, + "u16" => validate_kind_to_string::(name, kind, value)?, + "i16" => validate_kind_to_string::(name, kind, value)?, + "u32" => validate_kind_to_string::(name, kind, value)?, + "i32" => validate_kind_to_string::(name, kind, value)?, + "u64" => validate_kind_to_string::(name, kind, value)?, + "i64" => validate_kind_to_string::(name, kind, value)?, + "u128" => validate_kind_to_string::(name, kind, value)?, + "i128" => validate_kind_to_string::(name, kind, value)?, + "f32" => validate_kind_to_string::(name, kind, value)?, + "f64" => validate_kind_to_string::(name, kind, value)?, + "&str" => bail!("{} is a {}, use [statics] for these", name, kind), + _ => bail!("Unsupported type: {}", kind), + }; + + output.push_str(&line); + Ok(()) })?; @@ -77,8 +112,8 @@ #[test] fn it_parses_config() -> Result<()> { let input = r#"[constants] -FIRST = { type = "u8", value = 2 } -VALUE = { type = "f32", value = 42.0 } +FIRST = { type = "u8", value = "2" } +VALUE = { type = "f32", value = "42.0" } [statics] EXAMPLE = "thing" "#; @@ -92,6 +127,47 @@ assert_eq!(&output, expected); Ok(()) + } + + #[test] + fn it_validates_value_types() -> Result<()> { + let input = r#"[constants] +FIRST = { type = "u8", value = "1234" } +VALUE = { type = "f32", value = "42.0" } +[statics] +EXAMPLE = "thing" +"#; + + let output = parse_config(input); + + let report = output.expect_err("Output was somehow successful"); + + let mut error_chain = report.chain(); + + assert_eq!( + "Parsing failure of constant \"FIRST\" as u8", + error_chain.next().unwrap().to_string() + ); + assert_eq!( + "number too large to fit in target type", + error_chain.next().unwrap().to_string() + ); + assert!(error_chain.next().is_none()); + + Ok(()) + } + + #[test] + fn it_disallows_string_constants() { + let input = r#"[constants] +FIRST = { type = "&str", value = "42" } +"#; + + let expected = "FIRST is a &str, use [statics] for these"; + + let output = parse_config(input).expect_err("Output somehow successfully parsed"); + + assert_eq!(output.to_string(), expected); } #[test] @@ -112,7 +188,7 @@ #[test] fn it_handles_only_constants() -> Result<()> { let input = r#"[constants] - FIRST = { type = "u8", value = 42 } +FIRST = { type = "u8", value = "42" } "#; let expected = "pub const FIRST: u8 = 42;\n"; @@ -149,13 +225,13 @@ #[test] fn it_outputs_to_file() -> Result<()> { let input = r#"[constants] -FIRST = { type = "u8", value = 2 } -VALUE = { type = "u64", value = 42 } +FIRST = { type = "u128", value = "340282366920938463463374607431768211455" } +VALUE = { type = "u64", value = "42" } [statics] EXAMPLE = "thing" "#; let expected = r#"pub static EXAMPLE: &str = "thing"; -pub const FIRST: u8 = 2; +pub const FIRST: u128 = 340282366920938463463374607431768211455; pub const VALUE: u64 = 42; "#; let output: Vec = Vec::new(); -- tangled.sh