diff --git a/typesense/tests/derive/collection.rs b/typesense/tests/derive/collection.rs index ca8c2c3..ad986d3 100644 --- a/typesense/tests/derive/collection.rs +++ b/typesense/tests/derive/collection.rs @@ -260,7 +260,66 @@ fn derived_document_handles_nested_and_flattened_fields() { assert_eq!(serde_json::to_value(schema).unwrap(), expected); } -// Test 4: All Boolean Shorthand Attributes +// Test 4: Raw Identifiers +// +// Rust raw identifiers (r#type, r#abstract, etc.) must have the r# prefix +// stripped in all generated output: regular field names, flattened prefixes, +// and default_sorting_field validation. + +#[allow(dead_code)] +#[derive(Typesense, Serialize, Deserialize)] +#[typesense(collection_name = "raw_ident_docs", default_sorting_field = "type")] +struct RawIdentDoc { + id: String, + #[typesense(type = "int32")] + r#type: String, + r#abstract: Option, + normal_field: i32, +} + +#[derive(Typesense, Serialize, Deserialize)] +struct RawIdentNested { + value: String, +} + +#[allow(dead_code)] +#[derive(Typesense, Serialize, Deserialize)] +#[typesense(collection_name = "raw_ident_flat")] +struct RawIdentFlat { + id: String, + #[typesense(flatten, skip)] + r#match: RawIdentNested, +} + +#[test] +fn derived_document_strips_raw_identifier_prefix() { + // Regular fields: r#type -> "type", r#abstract -> "abstract" + let schema = RawIdentDoc::collection_schema(); + let expected = json!({ + "name": "raw_ident_docs", + "default_sorting_field": "type", + "fields": [ + { "name": "id", "type": "string" }, + { "name": "type", "type": "int32" }, + { "name": "abstract", "type": "string", "optional": true }, + { "name": "normal_field", "type": "int32" } + ] + }); + assert_eq!(serde_json::to_value(schema).unwrap(), expected); + + // Flattened prefix: r#match -> "match.value" + let schema = RawIdentFlat::collection_schema(); + let expected = json!({ + "name": "raw_ident_flat", + "fields": [ + { "name": "id", "type": "string" }, + { "name": "match.value", "type": "string" } + ] + }); + assert_eq!(serde_json::to_value(schema).unwrap(), expected); +} + +// Test 5: All Boolean Shorthand Attributes #[allow(dead_code)] #[derive(Typesense, Serialize, Deserialize)] diff --git a/typesense_derive/src/field_attributes.rs b/typesense_derive/src/field_attributes.rs index b1f455e..2956142 100644 --- a/typesense_derive/src/field_attributes.rs +++ b/typesense_derive/src/field_attributes.rs @@ -1,4 +1,7 @@ -use crate::{bool_literal, get_inner_type, i32_literal, skip_eq, string_literal, ty_inner_type}; +use crate::{ + bool_literal, get_inner_type, i32_literal, skip_eq, string_literal, strip_raw_prefix, + ty_inner_type, +}; use proc_macro2::TokenTree; use quote::quote; use syn::{Attribute, Field}; @@ -271,7 +274,7 @@ fn build_regular_field(field: &Field, field_attrs: &FieldAttributes) -> proc_mac let field_name = if let Some(rename) = &field_attrs.rename { quote! { #rename } } else { - let name_ident = field.ident.as_ref().unwrap().to_string(); + let name_ident = strip_raw_prefix(&field.ident.as_ref().unwrap().to_string()); quote! { #name_ident } }; @@ -322,7 +325,7 @@ pub(crate) fn process_field( let prefix = if let Some(rename_prefix) = &field_attrs.rename { quote! { #rename_prefix } } else { - let name_ident = field.ident.as_ref().unwrap().to_string(); + let name_ident = strip_raw_prefix(&field.ident.as_ref().unwrap().to_string()); quote! { #name_ident } }; diff --git a/typesense_derive/src/helpers.rs b/typesense_derive/src/helpers.rs index 337fc11..a258584 100644 --- a/typesense_derive/src/helpers.rs +++ b/typesense_derive/src/helpers.rs @@ -133,6 +133,12 @@ pub(crate) fn ty_inner_type<'a>(ty: &'a syn::Type, wrapper: &'static str) -> Opt None } +/// Strip the `r#` prefix from raw identifiers. +/// ie, 'r#' in `r#type`should not appear in generated output like schema field names. +pub(crate) fn strip_raw_prefix(s: &str) -> String { + s.strip_prefix("r#").unwrap_or(s).to_owned() +} + /// Helper to get the inner-most type from nested Option/Vec wrappers. pub(crate) fn get_inner_type(mut ty: &syn::Type) -> &syn::Type { while let Some(inner) = ty_inner_type(ty, "Option").or_else(|| ty_inner_type(ty, "Vec")) { diff --git a/typesense_derive/src/lib.rs b/typesense_derive/src/lib.rs index 0e1b658..d31834b 100644 --- a/typesense_derive/src/lib.rs +++ b/typesense_derive/src/lib.rs @@ -56,9 +56,9 @@ fn impl_typesense_collection(item: ItemStruct) -> syn::Result { .iter() .map(|field| { extract_field_attrs(field).map(|attrs| { - attrs - .rename - .unwrap_or_else(|| field.ident.as_ref().unwrap().to_string()) + attrs.rename.unwrap_or_else(|| { + strip_raw_prefix(&field.ident.as_ref().unwrap().to_string()) + }) }) }) .collect::>>()?;