fix(forms): support reserved control identifiers
Normalize raw Rust identifiers when matching generated form contracts so reserved HTML control names remain usable. req: form/004
This commit is contained in:
+15
-6
@@ -1260,13 +1260,16 @@ fn __hemx_attr(tag: &str, attr: &str) -> Option<::std::string::String> {
|
|||||||
out.push_str(&format!("handle_form\t{handle_ident}\t{form_ident}\n"));
|
out.push_str(&format!("handle_form\t{handle_ident}\t{form_ident}\n"));
|
||||||
}
|
}
|
||||||
for form in self.forms.values() {
|
for form in self.forms.values() {
|
||||||
|
let mut fields = BTreeMap::<&str, (bool, bool)>::new();
|
||||||
for control in &form.controls {
|
for control in &form.controls {
|
||||||
|
let field = fields.entry(&control.name).or_default();
|
||||||
|
field.0 |= control.required;
|
||||||
|
field.1 |= form_control_is_multiple(&control.kind);
|
||||||
|
}
|
||||||
|
for (name, (required, multiple)) in fields {
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"form_field\t{}\t{}\t{}\t{}\n",
|
"form_field\t{}\t{name}\t{required}\t{multiple}\n",
|
||||||
form.resource.ident,
|
form.resource.ident
|
||||||
control.name,
|
|
||||||
control.required,
|
|
||||||
form_control_is_multiple(&control.kind)
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2594,6 +2597,7 @@ mod tests {
|
|||||||
r#"<form data-hemx-handle="save" data-hemx-form="profile">
|
r#"<form data-hemx-handle="save" data-hemx-form="profile">
|
||||||
<input type="hidden" name="__h" value="123">
|
<input type="hidden" name="__h" value="123">
|
||||||
<input name="title" required>
|
<input name="title" required>
|
||||||
|
<template h-if="alternate"><input name="title"></template>
|
||||||
<input name="count" type="number" min="1" max="10" step="1">
|
<input name="count" type="number" min="1" max="10" step="1">
|
||||||
<select name="labels" multiple></select>
|
<select name="labels" multiple></select>
|
||||||
<input name="avatar" type="file">
|
<input name="avatar" type="file">
|
||||||
@@ -2622,7 +2626,12 @@ mod tests {
|
|||||||
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
|
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
|
||||||
assert!(syms.contains("handle_form\tsave\tprofile\n"));
|
assert!(syms.contains("handle_form\tsave\tprofile\n"));
|
||||||
assert!(!syms.contains("form_field\tprofile\t__h\t"));
|
assert!(!syms.contains("form_field\tprofile\t__h\t"));
|
||||||
assert!(syms.contains("form_field\tprofile\ttitle\ttrue\tfalse\n"));
|
assert_eq!(
|
||||||
|
syms.matches("form_field\tprofile\ttitle\ttrue\tfalse\n")
|
||||||
|
.count(),
|
||||||
|
1,
|
||||||
|
"conditional controls with one submitted name are one Rust form field"
|
||||||
|
); // req: form/004 test
|
||||||
assert!(syms.contains("form_field\tprofile\tlabels\tfalse\ttrue\n"));
|
assert!(syms.contains("form_field\tprofile\tlabels\tfalse\ttrue\n"));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ fn form_contract_errors(
|
|||||||
field
|
field
|
||||||
.ident
|
.ident
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|ident| (ident.to_string(), &field.ty))
|
.map(|ident| (form_field_name(ident), &field.ty))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
@@ -469,6 +469,10 @@ fn form_parser_types(form_struct: &ItemStruct) -> Vec<Type> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn form_field_name(ident: &syn::Ident) -> String {
|
||||||
|
ident.to_string().trim_start_matches("r#").to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
fn form_decode_fields(
|
fn form_decode_fields(
|
||||||
syms_path: &PathBuf,
|
syms_path: &PathBuf,
|
||||||
form_name: &str,
|
form_name: &str,
|
||||||
@@ -486,7 +490,9 @@ fn form_decode_fields(
|
|||||||
form_fields(syms_path, form_name)
|
form_fields(syms_path, form_name)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|field| {
|
.filter_map(|field| {
|
||||||
let (ident, ty) = actual.iter().find(|(ident, _)| ident == &&field.ident)?;
|
let (ident, ty) = actual
|
||||||
|
.iter()
|
||||||
|
.find(|(ident, _)| form_field_name(ident) == field.ident)?;
|
||||||
let control_name = field.name;
|
let control_name = field.name;
|
||||||
let parser = parser_type(ty);
|
let parser = parser_type(ty);
|
||||||
Some(if field.multiple {
|
Some(if field.multiple {
|
||||||
|
|||||||
@@ -611,6 +611,57 @@ struct Profile {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_struct_accepts_raw_identifier_for_reserved_control_name() {
|
||||||
|
// req: form/004 test
|
||||||
|
let fixture = Fixture::new("hemx-derive-form-raw-identifier-pass");
|
||||||
|
fixture.write(
|
||||||
|
"Cargo.toml",
|
||||||
|
&format!(
|
||||||
|
r#"[package]
|
||||||
|
name = "hemx-derive-form-raw-identifier-pass"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hemx = {{ path = {:?} }}
|
||||||
|
"#,
|
||||||
|
repo_path("hemx")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
fixture.write(
|
||||||
|
"build.rs",
|
||||||
|
r#"fn main() {
|
||||||
|
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||||
|
std::fs::write(
|
||||||
|
out.join("hemx.syms"),
|
||||||
|
"hemx-syms-v1\nform\ttemplates/app.heml::filter\tfilter\t1\nform_field\tfilter\ttype\ttrue\tfalse\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
fixture.write(
|
||||||
|
"src/lib.rs",
|
||||||
|
r#"#[hemx::form("filter")]
|
||||||
|
struct Filter {
|
||||||
|
r#type: String,
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = check_fixture(&fixture);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"fixture failed to compile:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn form_handle_accepts_checked_form_model() {
|
fn form_handle_accepts_checked_form_model() {
|
||||||
// req: form/001 req: form/004 req: form/006
|
// req: form/001 req: form/004 req: form/006
|
||||||
|
|||||||
Reference in New Issue
Block a user