From ba3f40a3f0a04977a4bebc9868a0b877a1d99572 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Tue, 26 May 2026 01:28:51 +0200 Subject: [PATCH] feat(build): emit checked param tokens Generate ParamName constants for handle data parameters so public code can refer to checked param metadata instead of stringly typed names. req: codegen/003 --- slhx-build/src/lib.rs | 18 ++++++++++++++++++ slhx-core/src/lib.rs | 29 +++++++++++++++++++++++++++++ slhx-core/tests/effect_batch.rs | 10 +++++++++- slhx/src/lib.rs | 2 +- 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/slhx-build/src/lib.rs b/slhx-build/src/lib.rs index 83507ba..b19a664 100644 --- a/slhx-build/src/lib.rs +++ b/slhx-build/src/lib.rs @@ -425,6 +425,22 @@ impl Resources { )); out.push_str(&format!("{pad}}}\n\n")); + out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod params {{\n")); + let mut emitted_params = BTreeSet::new(); + for res in self.handles.values().filter(|res| component_matches(res, component)) { + if let Some(params) = self.handle_params.get(&res.ident) { + for param in params { + if emitted_params.insert(param.as_str()) { + out.push_str(&format!( + "{inner}pub const {param}: ::slhx::ParamName = ::slhx::ParamName::new({});\n", + rust_str(param) + )); + } + } + } + } + out.push_str(&format!("{pad}}}\n\n")); + out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod atoms {{\n")); for res in self.atoms.values().filter(|res| component_matches(res, component)) { out.push_str(&format!( @@ -1076,6 +1092,8 @@ mod tests { assert!(generated.contains("pub const todos")); assert!(generated.contains("pub mod handles")); assert!(generated.contains("pub const create: ::slhx::Handle<::slhx::Form<::std::string::String>>")); + assert!(generated.contains("pub mod params")); + assert!(generated.contains("pub const todo_id: ::slhx::ParamName = ::slhx::ParamName::new(\"todo_id\")")); assert!(generated.contains("pub mod forms")); assert!(generated.contains("pub mod atoms")); assert!(generated.contains("pub const filter")); diff --git a/slhx-core/src/lib.rs b/slhx-core/src/lib.rs index ab7ccdd..4803c7b 100644 --- a/slhx-core/src/lib.rs +++ b/slhx-core/src/lib.rs @@ -186,6 +186,35 @@ impl core::fmt::Display for CssClass { } } +/// A generated, checked parameter token. +/// req: codegen/003 +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct ParamName { + name: &'static str, +} + +impl ParamName { + pub const fn new(name: &'static str) -> Self { + Self { name } + } + + pub const fn as_str(self) -> &'static str { + self.name + } +} + +impl AsRef for ParamName { + fn as_ref(&self) -> &str { + self.name + } +} + +impl core::fmt::Display for ParamName { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.name) + } +} + /// A generated, checked component token. /// req: component/003 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] diff --git a/slhx-core/tests/effect_batch.rs b/slhx-core/tests/effect_batch.rs index 865347f..ec30e2f 100644 --- a/slhx-core/tests/effect_batch.rs +++ b/slhx-core/tests/effect_batch.rs @@ -1,7 +1,7 @@ use slhx_core::{ event, navigate, redirect, replace, Atom, AtomSnapshot, AtomState, BuildFingerprint, ComponentRef, CssClass, CssClasses, Effect, EffectBatch, Form, Handle, IntoEffect, KeyedSlot, - NavigateMode, Payload, ResourceKind, SafeHtml, ScopeKey, Slot, + NavigateMode, ParamName, Payload, ResourceKind, SafeHtml, ScopeKey, Slot, }; #[test] @@ -93,6 +93,14 @@ fn slot_html_requires_explicit_safe_html() { assert_eq!(payload, Payload::Html(String::from("ok"))); } +#[test] +fn param_names_format_generated_param_names() { + // req: codegen/003 + let param = ParamName::new("todo_id"); + assert_eq!(param.as_str(), "todo_id"); + assert_eq!(param.to_string(), "todo_id"); +} + #[test] fn component_refs_format_generated_component_names() { // req: component/003 diff --git a/slhx/src/lib.rs b/slhx/src/lib.rs index 71dffa9..ae019f2 100644 --- a/slhx/src/lib.rs +++ b/slhx/src/lib.rs @@ -50,7 +50,7 @@ pub mod prelude { pub use slhx_core::{ navigate, push, redirect, replace, Atom, BuildFingerprint, ComponentRef, CssClass, CssClasses, Effect, EventName, Form, FormModel, FormValue, Handle, IntoEffect, KeyedSlot, - Slot, + ParamName, Slot, }; pub use crate::{render_html, RenderKeyedSlotExt, RenderSlotExt}; pub use slhx_derive::{app, component, form, handler, surface};