Skip to content

Commit 66e2f97

Browse files
authored
Fix raw identifiers in collection schema (#76)
* Fix raw identifiers in collection schema * Format * to_string -> to_owned
1 parent 14b5d24 commit 66e2f97

File tree

4 files changed

+75
-7
lines changed

4 files changed

+75
-7
lines changed

typesense/tests/derive/collection.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,66 @@ fn derived_document_handles_nested_and_flattened_fields() {
260260
assert_eq!(serde_json::to_value(schema).unwrap(), expected);
261261
}
262262

263-
// Test 4: All Boolean Shorthand Attributes
263+
// Test 4: Raw Identifiers
264+
//
265+
// Rust raw identifiers (r#type, r#abstract, etc.) must have the r# prefix
266+
// stripped in all generated output: regular field names, flattened prefixes,
267+
// and default_sorting_field validation.
268+
269+
#[allow(dead_code)]
270+
#[derive(Typesense, Serialize, Deserialize)]
271+
#[typesense(collection_name = "raw_ident_docs", default_sorting_field = "type")]
272+
struct RawIdentDoc {
273+
id: String,
274+
#[typesense(type = "int32")]
275+
r#type: String,
276+
r#abstract: Option<String>,
277+
normal_field: i32,
278+
}
279+
280+
#[derive(Typesense, Serialize, Deserialize)]
281+
struct RawIdentNested {
282+
value: String,
283+
}
284+
285+
#[allow(dead_code)]
286+
#[derive(Typesense, Serialize, Deserialize)]
287+
#[typesense(collection_name = "raw_ident_flat")]
288+
struct RawIdentFlat {
289+
id: String,
290+
#[typesense(flatten, skip)]
291+
r#match: RawIdentNested,
292+
}
293+
294+
#[test]
295+
fn derived_document_strips_raw_identifier_prefix() {
296+
// Regular fields: r#type -> "type", r#abstract -> "abstract"
297+
let schema = RawIdentDoc::collection_schema();
298+
let expected = json!({
299+
"name": "raw_ident_docs",
300+
"default_sorting_field": "type",
301+
"fields": [
302+
{ "name": "id", "type": "string" },
303+
{ "name": "type", "type": "int32" },
304+
{ "name": "abstract", "type": "string", "optional": true },
305+
{ "name": "normal_field", "type": "int32" }
306+
]
307+
});
308+
assert_eq!(serde_json::to_value(schema).unwrap(), expected);
309+
310+
// Flattened prefix: r#match -> "match.value"
311+
let schema = RawIdentFlat::collection_schema();
312+
let expected = json!({
313+
"name": "raw_ident_flat",
314+
"fields": [
315+
{ "name": "id", "type": "string" },
316+
{ "name": "match.value", "type": "string" }
317+
]
318+
});
319+
assert_eq!(serde_json::to_value(schema).unwrap(), expected);
320+
}
321+
322+
// Test 5: All Boolean Shorthand Attributes
264323

265324
#[allow(dead_code)]
266325
#[derive(Typesense, Serialize, Deserialize)]

typesense_derive/src/field_attributes.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::{bool_literal, get_inner_type, i32_literal, skip_eq, string_literal, ty_inner_type};
1+
use crate::{
2+
bool_literal, get_inner_type, i32_literal, skip_eq, string_literal, strip_raw_prefix,
3+
ty_inner_type,
4+
};
25
use proc_macro2::TokenTree;
36
use quote::quote;
47
use syn::{Attribute, Field};
@@ -271,7 +274,7 @@ fn build_regular_field(field: &Field, field_attrs: &FieldAttributes) -> proc_mac
271274
let field_name = if let Some(rename) = &field_attrs.rename {
272275
quote! { #rename }
273276
} else {
274-
let name_ident = field.ident.as_ref().unwrap().to_string();
277+
let name_ident = strip_raw_prefix(&field.ident.as_ref().unwrap().to_string());
275278
quote! { #name_ident }
276279
};
277280

@@ -322,7 +325,7 @@ pub(crate) fn process_field(
322325
let prefix = if let Some(rename_prefix) = &field_attrs.rename {
323326
quote! { #rename_prefix }
324327
} else {
325-
let name_ident = field.ident.as_ref().unwrap().to_string();
328+
let name_ident = strip_raw_prefix(&field.ident.as_ref().unwrap().to_string());
326329
quote! { #name_ident }
327330
};
328331

typesense_derive/src/helpers.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ pub(crate) fn ty_inner_type<'a>(ty: &'a syn::Type, wrapper: &'static str) -> Opt
133133
None
134134
}
135135

136+
/// Strip the `r#` prefix from raw identifiers.
137+
/// ie, 'r#' in `r#type`should not appear in generated output like schema field names.
138+
pub(crate) fn strip_raw_prefix(s: &str) -> String {
139+
s.strip_prefix("r#").unwrap_or(s).to_owned()
140+
}
141+
136142
/// Helper to get the inner-most type from nested Option/Vec wrappers.
137143
pub(crate) fn get_inner_type(mut ty: &syn::Type) -> &syn::Type {
138144
while let Some(inner) = ty_inner_type(ty, "Option").or_else(|| ty_inner_type(ty, "Vec")) {

typesense_derive/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ fn impl_typesense_collection(item: ItemStruct) -> syn::Result<TokenStream> {
5656
.iter()
5757
.map(|field| {
5858
extract_field_attrs(field).map(|attrs| {
59-
attrs
60-
.rename
61-
.unwrap_or_else(|| field.ident.as_ref().unwrap().to_string())
59+
attrs.rename.unwrap_or_else(|| {
60+
strip_raw_prefix(&field.ident.as_ref().unwrap().to_string())
61+
})
6262
})
6363
})
6464
.collect::<syn::Result<Vec<String>>>()?;

0 commit comments

Comments
 (0)