Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion typesense/tests/derive/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
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)]
Expand Down
9 changes: 6 additions & 3 deletions typesense_derive/src/field_attributes.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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 }
};

Expand Down Expand Up @@ -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 }
};

Expand Down
6 changes: 6 additions & 0 deletions typesense_derive/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
6 changes: 3 additions & 3 deletions typesense_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ fn impl_typesense_collection(item: ItemStruct) -> syn::Result<TokenStream> {
.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::<syn::Result<Vec<String>>>()?;
Expand Down
Loading