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")
|
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!(effect.has_slot(ui::board::slots::notice));
|
||||||
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ mod tests {
|
|||||||
ui::control_center::slots::notice.text("queued")
|
ui::control_center::slots::notice.text("queued")
|
||||||
}
|
}
|
||||||
|
|
||||||
let batch = inspect(launch_work(
|
let batch = inspect(launch_work(LaunchWork::FORM));
|
||||||
ui::control_center::forms::launch_work.typed::<LaunchWork>(),
|
|
||||||
));
|
|
||||||
|
|
||||||
assert!(batch.has_slot(ui::control_center::slots::notice));
|
assert!(batch.has_slot(ui::control_center::slots::notice));
|
||||||
assert!(matches!(batch.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
assert!(matches!(batch.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ mod tests {
|
|||||||
ui::slots::todo_list.text("queued")
|
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!(effect.has_slot(ui::slots::todo_list));
|
||||||
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||||
@@ -103,7 +103,7 @@ mod tests {
|
|||||||
ui::slots::wizard_step.text("queued")
|
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!(effect.has_slot(ui::slots::wizard_step));
|
||||||
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
||||||
@@ -158,7 +158,7 @@ mod tests {
|
|||||||
ui::slots::login_status.text("queued")
|
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!(effect.has_slot(ui::slots::login_status));
|
||||||
assert!(matches!(effect.ops(), [Effect::Put { payload: Payload::Text(_), .. }]));
|
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 {
|
pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let form_name = parse_macro_input!(attr as LitStr).value();
|
let form_name = parse_macro_input!(attr as LitStr).value();
|
||||||
let form_struct = parse_macro_input!(item as ItemStruct);
|
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() {
|
if errors.is_empty() {
|
||||||
let ident = &form_struct.ident;
|
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();
|
let mut generics = form_struct.generics.clone();
|
||||||
for ty in form_parser_types(&form_struct) {
|
for ty in form_parser_types(&form_struct) {
|
||||||
generics.make_where_clause().predicates.push(parse_quote!(#ty: ::slhx::FormValue));
|
generics.make_where_clause().predicates.push(parse_quote!(#ty: ::slhx::FormValue));
|
||||||
@@ -99,6 +108,9 @@ pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
quote!(
|
quote!(
|
||||||
#form_struct
|
#form_struct
|
||||||
impl #impl_generics ::slhx::FormModel for #ident #ty_generics #where_clause {}
|
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()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
@@ -281,17 +293,14 @@ struct GeneratedFormField {
|
|||||||
multiple: bool,
|
multiple: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn form_contract_errors(form_name: &str, form_struct: &ItemStruct) -> Vec<String> {
|
fn form_contract_errors(syms_path: &PathBuf, 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()];
|
|
||||||
};
|
|
||||||
if !syms_path.exists() {
|
if !syms_path.exists() {
|
||||||
return vec![format!(
|
return vec![format!(
|
||||||
"#[slhx::form] could not find {}; add slhx_build::app().run()? to build.rs or check template generation",
|
"#[slhx::form] could not find {}; add slhx_build::app().run()? to build.rs or check template generation",
|
||||||
syms_path.display()
|
syms_path.display()
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
let expected = form_fields(&syms_path, form_name);
|
let expected = form_fields(syms_path, form_name);
|
||||||
if expected.is_empty() {
|
if expected.is_empty() {
|
||||||
return vec![format!(
|
return vec![format!(
|
||||||
"unknown slhx form `{form_name}`; add data-slhx-form=\"{form_name}\" to a template or rename this form binding"
|
"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> {
|
fn form_fields(path: &PathBuf, form_name: &str) -> Vec<GeneratedFormField> {
|
||||||
let Ok(syms) = std::fs::read_to_string(path) else {
|
let Ok(syms) = std::fs::read_to_string(path) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|||||||
@@ -423,6 +423,10 @@ struct CreateTodo {
|
|||||||
fn create(_form: slhx::Form<CreateTodo>) -> impl slhx::IntoEffect {
|
fn create(_form: slhx::Form<CreateTodo>) -> impl slhx::IntoEffect {
|
||||||
slhx::event("created", "")
|
slhx::event("created", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn smoke() {
|
||||||
|
let _ = create(CreateTodo::FORM);
|
||||||
|
}
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user