update grid4 & heroledger models
This commit is contained in:
@@ -14,4 +14,6 @@ quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
heromodels_core = { path = "../heromodels_core" }
|
@@ -1,6 +1,6 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{Data, DeriveInput, Fields, parse_macro_input};
|
||||
use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta, MetaList, MetaNameValue};
|
||||
|
||||
/// Convert a string to snake_case
|
||||
fn to_snake_case(s: &str) -> String {
|
||||
@@ -47,86 +47,165 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let db_prefix = to_snake_case(&name_str);
|
||||
|
||||
// Extract fields with #[index] attribute
|
||||
let mut indexed_fields = Vec::new();
|
||||
let mut custom_index_names = std::collections::HashMap::new();
|
||||
// Supports both top-level (no args) and nested path-based indexes declared on a field
|
||||
#[derive(Clone)]
|
||||
enum IndexDecl {
|
||||
TopLevel {
|
||||
field_ident: syn::Ident,
|
||||
field_ty: syn::Type,
|
||||
},
|
||||
NestedPath {
|
||||
on_field_ident: syn::Ident,
|
||||
path: String, // dotted path relative to the field
|
||||
},
|
||||
}
|
||||
|
||||
let mut index_decls: Vec<IndexDecl> = Vec::new();
|
||||
|
||||
if let Data::Struct(ref mut data_struct) = input.data {
|
||||
if let Fields::Named(ref mut fields_named) = data_struct.fields {
|
||||
for field in &mut fields_named.named {
|
||||
let mut attr_idx = None;
|
||||
let mut to_remove: Vec<usize> = Vec::new();
|
||||
for (i, attr) in field.attrs.iter().enumerate() {
|
||||
if attr.path().is_ident("index") {
|
||||
attr_idx = Some(i);
|
||||
if let Some(ref field_name) = field.ident {
|
||||
// Check if the attribute has parameters
|
||||
let mut custom_name = None;
|
||||
if !attr.path().is_ident("index") {
|
||||
continue;
|
||||
}
|
||||
to_remove.push(i);
|
||||
|
||||
// Parse attribute arguments if any
|
||||
let meta = attr.meta.clone();
|
||||
if let syn::Meta::List(list) = meta {
|
||||
if let Ok(nested) = list.parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated) {
|
||||
for meta in nested {
|
||||
if let syn::Meta::NameValue(name_value) = meta {
|
||||
if name_value.path.is_ident("name") {
|
||||
if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) = name_value.value {
|
||||
custom_name = Some(lit_str.value());
|
||||
}
|
||||
if let Some(ref field_name) = field.ident {
|
||||
match &attr.meta {
|
||||
Meta::Path(_) => {
|
||||
// Simple top-level index on this field
|
||||
index_decls.push(IndexDecl::TopLevel {
|
||||
field_ident: field_name.clone(),
|
||||
field_ty: field.ty.clone(),
|
||||
});
|
||||
}
|
||||
Meta::List(MetaList { .. }) => {
|
||||
// Parse for path = "..."; name is assumed equal to path
|
||||
// We support syntax: #[index(path = "a.b.c")]
|
||||
if let Ok(nested) = attr.parse_args_with(
|
||||
syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
|
||||
) {
|
||||
for meta in nested {
|
||||
if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
|
||||
if path.is_ident("path") {
|
||||
if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(lit_str), .. }) = value {
|
||||
let p = lit_str.value();
|
||||
index_decls.push(IndexDecl::NestedPath {
|
||||
on_field_ident: field_name.clone(),
|
||||
path: p,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indexed_fields.push((field_name.clone(), field.ty.clone()));
|
||||
|
||||
if let Some(name) = custom_name {
|
||||
custom_index_names.insert(field_name.to_string(), name);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = attr_idx {
|
||||
// remove all #[index] attributes we processed
|
||||
// remove from the back to keep indices valid
|
||||
to_remove.sort_unstable();
|
||||
to_remove.drain(..).rev().for_each(|idx| {
|
||||
field.attrs.remove(idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Model trait implementation
|
||||
let db_keys_impl = if indexed_fields.is_empty() {
|
||||
let db_keys_impl = if index_decls.is_empty() {
|
||||
quote! {
|
||||
fn db_keys(&self) -> Vec<heromodels_core::IndexKey> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let field_keys = indexed_fields.iter().map(|(field_name, _)| {
|
||||
let name_str = custom_index_names
|
||||
.get(&field_name.to_string())
|
||||
.cloned()
|
||||
.unwrap_or(field_name.to_string());
|
||||
quote! {
|
||||
heromodels_core::IndexKey {
|
||||
name: #name_str,
|
||||
value: self.#field_name.to_string(),
|
||||
// Build code for keys from each index declaration
|
||||
let mut key_snippets: Vec<proc_macro2::TokenStream> = Vec::new();
|
||||
|
||||
for decl in &index_decls {
|
||||
match decl.clone() {
|
||||
IndexDecl::TopLevel { field_ident, .. } => {
|
||||
let name_str = field_ident.to_string();
|
||||
key_snippets.push(quote! {
|
||||
keys.push(heromodels_core::IndexKey {
|
||||
name: #name_str,
|
||||
value: self.#field_ident.to_string(),
|
||||
});
|
||||
});
|
||||
}
|
||||
IndexDecl::NestedPath { on_field_ident, path } => {
|
||||
// Name is equal to provided path
|
||||
let name_str = path.clone();
|
||||
// Generate traversal code using serde_json to support arrays and objects generically
|
||||
// Split the path into static segs for iteration
|
||||
let segs: Vec<String> = path.split('.').map(|s| s.to_string()).collect();
|
||||
let segs_iter = segs.iter().map(|s| s.as_str());
|
||||
let segs_array = quote! { [ #( #segs_iter ),* ] };
|
||||
|
||||
key_snippets.push(quote! {
|
||||
// Serialize the target field to JSON for generic traversal
|
||||
let __hm_json_val = ::serde_json::to_value(&self.#on_field_ident).unwrap_or(::serde_json::Value::Null);
|
||||
let mut __hm_stack: Vec<&::serde_json::Value> = vec![&__hm_json_val];
|
||||
for __hm_seg in #segs_array.iter() {
|
||||
let mut __hm_next: Vec<&::serde_json::Value> = Vec::new();
|
||||
for __hm_v in &__hm_stack {
|
||||
match __hm_v {
|
||||
::serde_json::Value::Array(arr) => {
|
||||
for __hm_e in arr {
|
||||
if let ::serde_json::Value::Object(map) = __hm_e {
|
||||
if let Some(x) = map.get(*__hm_seg) { __hm_next.push(x); }
|
||||
}
|
||||
}
|
||||
}
|
||||
::serde_json::Value::Object(map) => {
|
||||
if let Some(x) = map.get(*__hm_seg) { __hm_next.push(x); }
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
__hm_stack = __hm_next;
|
||||
if __hm_stack.is_empty() { break; }
|
||||
}
|
||||
for __hm_leaf in __hm_stack {
|
||||
match __hm_leaf {
|
||||
::serde_json::Value::Null => {},
|
||||
::serde_json::Value::Array(_) => {},
|
||||
::serde_json::Value::Object(_) => {},
|
||||
other => {
|
||||
// Convert primitives to string without surrounding quotes for strings
|
||||
let mut s = other.to_string();
|
||||
if let ::serde_json::Value::String(_) = other { s = s.trim_matches('"').to_string(); }
|
||||
keys.push(heromodels_core::IndexKey { name: #name_str, value: s });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
quote! {
|
||||
fn db_keys(&self) -> Vec<heromodels_core::IndexKey> {
|
||||
vec![
|
||||
#(#field_keys),*
|
||||
]
|
||||
let mut keys: Vec<heromodels_core::IndexKey> = Vec::new();
|
||||
#(#key_snippets)*
|
||||
keys
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let indexed_field_names = indexed_fields
|
||||
let indexed_field_names: Vec<String> = index_decls
|
||||
.iter()
|
||||
.map(|f| f.0.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
.map(|d| match d {
|
||||
IndexDecl::TopLevel { field_ident, .. } => field_ident.to_string(),
|
||||
IndexDecl::NestedPath { path, .. } => path.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let model_impl = quote! {
|
||||
impl heromodels_core::Model for #struct_name {
|
||||
@@ -152,51 +231,33 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
}
|
||||
};
|
||||
|
||||
// Generate Index trait implementations
|
||||
// Generate Index trait implementations only for top-level fields, keep existing behavior
|
||||
let mut index_impls = proc_macro2::TokenStream::new();
|
||||
for decl in &index_decls {
|
||||
if let IndexDecl::TopLevel { field_ident, field_ty } = decl {
|
||||
let name_str = field_ident.to_string();
|
||||
let index_struct_name = format_ident!("{}", &name_str);
|
||||
let field_type = field_ty.clone();
|
||||
|
||||
for (field_name, field_type) in &indexed_fields {
|
||||
let name_str = field_name.to_string();
|
||||
let index_impl = quote! {
|
||||
pub struct #index_struct_name;
|
||||
|
||||
// Get custom index name if specified, otherwise use field name
|
||||
let index_key = match custom_index_names.get(&name_str) {
|
||||
Some(custom_name) => custom_name.clone(),
|
||||
None => name_str.clone(),
|
||||
};
|
||||
impl heromodels_core::Index for #index_struct_name {
|
||||
type Model = super::#struct_name;
|
||||
type Key = #field_type;
|
||||
|
||||
// Convert field name to PascalCase for struct name
|
||||
// let struct_name_str = to_pascal_case(&name_str);
|
||||
// let index_struct_name = format_ident!("{}", struct_name_str);
|
||||
let index_struct_name = format_ident!("{}", &name_str);
|
||||
fn key() -> &'static str { #name_str }
|
||||
|
||||
// Default to str for key type
|
||||
let index_impl = quote! {
|
||||
pub struct #index_struct_name;
|
||||
|
||||
impl heromodels_core::Index for #index_struct_name {
|
||||
type Model = super::#struct_name;
|
||||
type Key = #field_type;
|
||||
|
||||
fn key() -> &'static str {
|
||||
#index_key
|
||||
fn field_name() -> &'static str { #name_str }
|
||||
}
|
||||
|
||||
fn field_name() -> &'static str {
|
||||
#name_str
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
index_impls.extend(index_impl);
|
||||
};
|
||||
index_impls.extend(index_impl);
|
||||
}
|
||||
}
|
||||
|
||||
if !index_impls.is_empty() {
|
||||
let index_mod_name = format_ident!("{}_index", db_prefix);
|
||||
index_impls = quote! {
|
||||
pub mod #index_mod_name {
|
||||
#index_impls
|
||||
}
|
||||
}
|
||||
index_impls = quote! { pub mod #index_mod_name { #index_impls } };
|
||||
}
|
||||
|
||||
// Combine the original struct with the generated implementations
|
||||
|
@@ -1,7 +1,38 @@
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Define the necessary structs and traits for testing
|
||||
// Make the current crate visible as an extern crate named `heromodels_core`
|
||||
extern crate self as heromodels_core;
|
||||
extern crate serde_json; // ensure ::serde_json path resolves
|
||||
|
||||
// Mock the heromodels_core API at crate root (visible via the alias above)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexKey {
|
||||
pub name: &'static str,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub trait Model: std::fmt::Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static {
|
||||
fn db_prefix() -> &'static str
|
||||
where
|
||||
Self: Sized;
|
||||
fn get_id(&self) -> u32;
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData;
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
Vec::new()
|
||||
}
|
||||
fn indexed_fields() -> Vec<&'static str> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Index {
|
||||
type Model: Model;
|
||||
type Key: ToString + ?Sized;
|
||||
fn key() -> &'static str;
|
||||
fn field_name() -> &'static str;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BaseModelData {
|
||||
pub id: u32,
|
||||
@@ -11,41 +42,18 @@ pub struct BaseModelData {
|
||||
}
|
||||
|
||||
impl BaseModelData {
|
||||
pub fn new(id: u32) -> Self {
|
||||
let now = 1000; // Mock timestamp
|
||||
Self {
|
||||
id,
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
comments: Vec::new(),
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
let now = 1000;
|
||||
Self { id: 0, created_at: now, modified_at: now, comments: Vec::new() }
|
||||
}
|
||||
pub fn update_modified(&mut self) { self.modified_at += 1; }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexKey {
|
||||
pub name: &'static str,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub trait Model: std::fmt::Debug + Clone {
|
||||
fn db_prefix() -> &'static str;
|
||||
fn get_id(&self) -> u32;
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData;
|
||||
fn db_keys(&self) -> Vec<IndexKey>;
|
||||
}
|
||||
|
||||
pub trait Index {
|
||||
type Model: Model;
|
||||
type Key: ?Sized;
|
||||
fn key() -> &'static str;
|
||||
}
|
||||
|
||||
// Test struct using the model macro
|
||||
// Top-level field index tests
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[model]
|
||||
struct TestUser {
|
||||
base_data: BaseModelData,
|
||||
pub struct TestUser {
|
||||
base_data: heromodels_core::BaseModelData,
|
||||
|
||||
#[index]
|
||||
username: String,
|
||||
@@ -54,25 +62,12 @@ struct TestUser {
|
||||
is_active: bool,
|
||||
}
|
||||
|
||||
// Test struct with custom index name
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[model]
|
||||
struct TestUserWithCustomIndex {
|
||||
base_data: BaseModelData,
|
||||
|
||||
#[index(name = "custom_username")]
|
||||
username: String,
|
||||
|
||||
#[index]
|
||||
is_active: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_model() {
|
||||
assert_eq!(TestUser::db_prefix(), "test_user");
|
||||
|
||||
let user = TestUser {
|
||||
base_data: BaseModelData::new(1),
|
||||
base_data: heromodels_core::BaseModelData::new(),
|
||||
username: "test".to_string(),
|
||||
is_active: true,
|
||||
};
|
||||
@@ -85,22 +80,47 @@ fn test_basic_model() {
|
||||
assert_eq!(keys[1].value, "true");
|
||||
}
|
||||
|
||||
// Nested path index tests (including vector traversal)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
struct GPU { gpu_brand: String }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
struct CPU { cpu_brand: String }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
struct DeviceInfo { vendor: String, cpu: Vec<CPU>, gpu: Vec<GPU> }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[model]
|
||||
pub struct NodeLike {
|
||||
base_data: heromodels_core::BaseModelData,
|
||||
|
||||
#[index(path = "vendor")]
|
||||
#[index(path = "cpu.cpu_brand")]
|
||||
#[index(path = "gpu.gpu_brand")]
|
||||
devices: DeviceInfo,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_index_name() {
|
||||
let user = TestUserWithCustomIndex {
|
||||
base_data: BaseModelData::new(1),
|
||||
username: "test".to_string(),
|
||||
is_active: true,
|
||||
fn test_nested_indexes() {
|
||||
let n = NodeLike {
|
||||
base_data: heromodels_core::BaseModelData::new(),
|
||||
devices: DeviceInfo {
|
||||
vendor: "SuperVendor".to_string(),
|
||||
cpu: vec![CPU { cpu_brand: "Intel".into() }, CPU { cpu_brand: "AMD".into() }],
|
||||
gpu: vec![GPU { gpu_brand: "NVIDIA".into() }, GPU { gpu_brand: "AMD".into() }],
|
||||
},
|
||||
};
|
||||
|
||||
// Check that the Username struct uses the custom index name
|
||||
assert_eq!(Username::key(), "custom_username");
|
||||
let mut keys = n.db_keys();
|
||||
// Sort for deterministic assertions
|
||||
keys.sort_by(|a,b| a.name.cmp(b.name).then(a.value.cmp(&b.value)));
|
||||
|
||||
// Check that the db_keys method returns the correct keys
|
||||
let keys = user.db_keys();
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert_eq!(keys[0].name, "custom_username");
|
||||
assert_eq!(keys[0].value, "test");
|
||||
assert_eq!(keys[1].name, "is_active");
|
||||
assert_eq!(keys[1].value, "true");
|
||||
// Expect 1 (vendor) + 2 (cpu brands) + 2 (gpu brands) = 5 keys
|
||||
assert_eq!(keys.len(), 5);
|
||||
assert!(keys.iter().any(|k| k.name == "vendor" && k.value == "SuperVendor"));
|
||||
assert!(keys.iter().any(|k| k.name == "cpu.cpu_brand" && k.value == "Intel"));
|
||||
assert!(keys.iter().any(|k| k.name == "cpu.cpu_brand" && k.value == "AMD"));
|
||||
assert!(keys.iter().any(|k| k.name == "gpu.gpu_brand" && k.value == "NVIDIA"));
|
||||
assert!(keys.iter().any(|k| k.name == "gpu.gpu_brand" && k.value == "AMD"));
|
||||
}
|
||||
|
Reference in New Issue
Block a user