diff --git a/hemx-build/src/lib.rs b/hemx-build/src/lib.rs index 00eceef..bc5cf90 100644 --- a/hemx-build/src/lib.rs +++ b/hemx-build/src/lib.rs @@ -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")); } for form in self.forms.values() { + let mut fields = BTreeMap::<&str, (bool, bool)>::new(); 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!( - "form_field\t{}\t{}\t{}\t{}\n", - form.resource.ident, - control.name, - control.required, - form_control_is_multiple(&control.kind) + "form_field\t{}\t{name}\t{required}\t{multiple}\n", + form.resource.ident )); } } @@ -2594,6 +2597,7 @@ mod tests { r#"
+ @@ -2622,7 +2626,12 @@ mod tests { let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap(); assert!(syms.contains("handle_form\tsave\tprofile\n")); 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")); let _ = std::fs::remove_dir_all(&base); diff --git a/hemx-derive/src/lib.rs b/hemx-derive/src/lib.rs index cc429f6..3b643c2 100644 --- a/hemx-derive/src/lib.rs +++ b/hemx-derive/src/lib.rs @@ -422,7 +422,7 @@ fn form_contract_errors( field .ident .as_ref() - .map(|ident| (ident.to_string(), &field.ty)) + .map(|ident| (form_field_name(ident), &field.ty)) }) .collect::>(); let mut errors = Vec::new(); @@ -469,6 +469,10 @@ fn form_parser_types(form_struct: &ItemStruct) -> Vec { .collect() } +fn form_field_name(ident: &syn::Ident) -> String { + ident.to_string().trim_start_matches("r#").to_owned() +} + fn form_decode_fields( syms_path: &PathBuf, form_name: &str, @@ -486,7 +490,9 @@ fn form_decode_fields( form_fields(syms_path, form_name) .into_iter() .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 parser = parser_type(ty); Some(if field.multiple { diff --git a/hemx-derive/tests/compile_fail.rs b/hemx-derive/tests/compile_fail.rs index d3d1ef0..b84fea2 100644 --- a/hemx-derive/tests/compile_fail.rs +++ b/hemx-derive/tests/compile_fail.rs @@ -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] fn form_handle_accepts_checked_form_model() { // req: form/001 req: form/004 req: form/006