feat(derive): expose checked form resource constants
Have #[slhx::form] emit an associated FORM resource so checked form handlers can be called with TodoInput::FORM-style values instead of generated_resource.typed::<T>() noise. req: form/001 req: form/004 req: form/006 req: codegen/004
This commit is contained in:
@@ -64,7 +64,7 @@ mod tests {
|
||||
ui::board::slots::notice.text("queued")
|
||||
}
|
||||
|
||||
let effect = inspect(create_card(ui::board::forms::create_card.typed::<CreateCard>()));
|
||||
let effect = inspect(create_card(CreateCard::FORM));
|
||||
|
||||
assert!(effect.has_slot(ui::board::slots::notice));
|
||||
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||
|
||||
@@ -55,9 +55,7 @@ mod tests {
|
||||
ui::control_center::slots::notice.text("queued")
|
||||
}
|
||||
|
||||
let batch = inspect(launch_work(
|
||||
ui::control_center::forms::launch_work.typed::<LaunchWork>(),
|
||||
));
|
||||
let batch = inspect(launch_work(LaunchWork::FORM));
|
||||
|
||||
assert!(batch.has_slot(ui::control_center::slots::notice));
|
||||
assert!(matches!(batch.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||
|
||||
@@ -66,7 +66,7 @@ mod tests {
|
||||
ui::slots::todo_list.text("queued")
|
||||
}
|
||||
|
||||
let effect = inspect(add_todo(ui::forms::new_todo.typed::<TodoInput>()));
|
||||
let effect = inspect(add_todo(TodoInput::FORM));
|
||||
|
||||
assert!(effect.has_slot(ui::slots::todo_list));
|
||||
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||
@@ -103,7 +103,7 @@ mod tests {
|
||||
ui::slots::wizard_step.text("queued")
|
||||
}
|
||||
|
||||
let effect = inspect(next_step(ui::forms::wizard_input.typed::<WizardInput>()));
|
||||
let effect = inspect(next_step(WizardInput::FORM));
|
||||
|
||||
assert!(effect.has_slot(ui::slots::wizard_step));
|
||||
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||
@@ -158,7 +158,7 @@ mod tests {
|
||||
ui::slots::login_status.text("queued")
|
||||
}
|
||||
|
||||
let effect = inspect(login(ui::forms::credentials.typed::<Credentials>()));
|
||||
let effect = inspect(login(Credentials::FORM));
|
||||
|
||||
assert!(effect.has_slot(ui::slots::login_status));
|
||||
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||
|
||||
+29
-6
@@ -88,9 +88,18 @@ pub fn surface(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let form_name = parse_macro_input!(attr as LitStr).value();
|
||||
let form_struct = parse_macro_input!(item as ItemStruct);
|
||||
let errors = form_contract_errors(&form_name, &form_struct);
|
||||
let Some(syms_path) = syms_path() else {
|
||||
let message = "#[slhx::form] requires OUT_DIR; run inside a Cargo crate with slhx_build::app() in build.rs";
|
||||
return quote!(
|
||||
#form_struct
|
||||
compile_error!(#message);
|
||||
)
|
||||
.into();
|
||||
};
|
||||
let errors = form_contract_errors(&syms_path, &form_name, &form_struct);
|
||||
if errors.is_empty() {
|
||||
let ident = &form_struct.ident;
|
||||
let resource_id = form_resource_id(&syms_path, &form_name).expect("checked form exists in slhx.syms");
|
||||
let mut generics = form_struct.generics.clone();
|
||||
for ty in form_parser_types(&form_struct) {
|
||||
generics.make_where_clause().predicates.push(parse_quote!(#ty: ::slhx::FormValue));
|
||||
@@ -99,6 +108,9 @@ pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
quote!(
|
||||
#form_struct
|
||||
impl #impl_generics ::slhx::FormModel for #ident #ty_generics #where_clause {}
|
||||
impl #impl_generics #ident #ty_generics #where_clause {
|
||||
pub const FORM: ::slhx::Form<Self> = ::slhx::Form::new(#resource_id);
|
||||
}
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
@@ -281,17 +293,14 @@ struct GeneratedFormField {
|
||||
multiple: bool,
|
||||
}
|
||||
|
||||
fn form_contract_errors(form_name: &str, form_struct: &ItemStruct) -> Vec<String> {
|
||||
let Some(syms_path) = syms_path() else {
|
||||
return vec!["#[slhx::form] requires OUT_DIR; run inside a Cargo crate with slhx_build::app() in build.rs".to_string()];
|
||||
};
|
||||
fn form_contract_errors(syms_path: &PathBuf, form_name: &str, form_struct: &ItemStruct) -> Vec<String> {
|
||||
if !syms_path.exists() {
|
||||
return vec![format!(
|
||||
"#[slhx::form] could not find {}; add slhx_build::app().run()? to build.rs or check template generation",
|
||||
syms_path.display()
|
||||
)];
|
||||
}
|
||||
let expected = form_fields(&syms_path, form_name);
|
||||
let expected = form_fields(syms_path, form_name);
|
||||
if expected.is_empty() {
|
||||
return vec![format!(
|
||||
"unknown slhx form `{form_name}`; add data-slhx-form=\"{form_name}\" to a template or rename this form binding"
|
||||
@@ -380,6 +389,20 @@ fn generic_inner_type<'a>(ty: &'a Type, name: &str) -> Option<&'a Type> {
|
||||
})
|
||||
}
|
||||
|
||||
fn form_resource_id(path: &PathBuf, form_name: &str) -> Option<u32> {
|
||||
let syms = std::fs::read_to_string(path).ok()?;
|
||||
syms.lines().find_map(|line| {
|
||||
let mut fields = line.split('\t');
|
||||
if !matches!(fields.next(), Some("form")) {
|
||||
return None;
|
||||
}
|
||||
if fields.nth(1)? != form_name {
|
||||
return None;
|
||||
}
|
||||
fields.next()?.parse().ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn form_fields(path: &PathBuf, form_name: &str) -> Vec<GeneratedFormField> {
|
||||
let Ok(syms) = std::fs::read_to_string(path) else {
|
||||
return Vec::new();
|
||||
|
||||
@@ -423,6 +423,10 @@ struct CreateTodo {
|
||||
fn create(_form: slhx::Form<CreateTodo>) -> impl slhx::IntoEffect {
|
||||
slhx::event("created", "")
|
||||
}
|
||||
|
||||
fn smoke() {
|
||||
let _ = create(CreateTodo::FORM);
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user