hyperstack_interpreter/metrics_context.rs
1use crate::vm::{Register, RegisterValue};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5
6// ============================================================================
7// Type-safe field access traits and macros
8// ============================================================================
9
10/// Trait that describes how to access a field on a struct (NEW ENHANCED API)
11///
12/// This trait enables direct struct field access without the need for field_accessor! macros.
13/// Use the `impl_field_descriptors!` macro to automatically implement for all fields.
14///
15/// # Example
16/// ```ignore
17/// struct TradingMetrics {
18/// total_volume: u64,
19/// trade_count: u64,
20/// }
21///
22/// impl_field_descriptors!(TradingMetrics {
23/// total_volume: u64,
24/// trade_count: u64
25/// });
26///
27/// // Use struct fields directly
28/// ctx.get_field(TradingMetrics::total_volume()) // returns Option<u64>
29/// ctx.set_field(TradingMetrics::total_volume(), 1000)
30/// ctx.increment_field(TradingMetrics::trade_count(), 1)
31/// ```
32pub trait FieldDescriptor<T> {
33 /// The type of the field value
34 type Value: Serialize + for<'de> Deserialize<'de>;
35
36 /// The path to this field (e.g., "total_volume")
37 fn path(&self) -> &'static str;
38}
39
40/// Trait for direct field references - legacy approach (still supported)
41///
42/// This trait enables compile-time field name extraction and type checking.
43/// Use the `field!` macro to create field references directly from struct fields.
44///
45/// # Example
46/// ```ignore
47/// struct TradingMetrics {
48/// total_volume: u64,
49/// trade_count: u64,
50/// }
51///
52/// // Use fields directly with the field! macro
53/// let volume = ctx.get(&field!(entity, total_volume));
54/// ctx.set(&field!(entity, total_volume), 1000);
55/// ctx.increment(&field!(entity, trade_count), 1);
56/// ```
57pub trait FieldRef<T> {
58 /// Get the field path (e.g., "total_volume")
59 fn path(&self) -> &'static str;
60
61 /// Get the field value from a reference (for type inference)
62 fn get_ref<'a>(&self, _source: &'a T) -> Option<&'a T> {
63 None // Not used at runtime, only for type inference
64 }
65}
66
67/// Trait for type-safe field access without string literals (LEGACY API)
68///
69/// Implement this trait to create compile-time checked field accessors:
70/// ```ignore
71/// struct TotalVolume;
72/// impl FieldAccessor for TotalVolume {
73/// type Value = u64;
74/// fn path() -> &'static str { "total_volume" }
75/// }
76/// ```
77///
78/// Or use the `field_accessor!` macro for convenience.
79///
80/// **DEPRECATED**: Consider using the new `field!` macro instead for cleaner syntax.
81pub trait FieldAccessor {
82 /// The type of the field value
83 type Value: Serialize + for<'de> Deserialize<'de>;
84
85 /// The path to this field (e.g., "total_volume" or "reserves.last_price")
86 fn path() -> &'static str;
87
88 /// The nested path segments if needed (auto-computed from path)
89 fn segments() -> Vec<&'static str> {
90 Self::path().split('.').collect()
91 }
92}
93
94// Helper macro to join field names with dots (internal use)
95#[macro_export]
96#[doc(hidden)]
97macro_rules! __field_path {
98 ($first:ident) => {
99 stringify!($first)
100 };
101 ($first:ident, $($rest:ident),+) => {
102 concat!(stringify!($first), ".", $crate::__field_path!($($rest),+))
103 };
104}
105
106/// Macro to implement field descriptors for struct fields
107///
108/// This macro generates FieldDescriptor implementations and static methods for each field,
109/// allowing direct struct field access without the need for separate accessor types.
110///
111/// # Example
112/// ```ignore
113/// struct TradingMetrics {
114/// total_volume: u64,
115/// trade_count: u64,
116/// }
117///
118/// impl_field_descriptors!(TradingMetrics {
119/// total_volume: u64,
120/// trade_count: u64
121/// });
122///
123/// // Now you can use:
124/// ctx.get_field(TradingMetrics::total_volume())
125/// ctx.set_field(TradingMetrics::total_volume(), 1000)
126/// ```
127#[macro_export]
128macro_rules! impl_field_descriptors {
129 ($struct_name:ident { $( $field_name:ident : $field_type:ty ),* $(,)? }) => {
130 impl $struct_name {
131 $(
132 /// Returns a field descriptor for this field
133 pub fn $field_name() -> impl $crate::metrics_context::FieldDescriptor<$struct_name, Value = $field_type> {
134 struct FieldDescriptorImpl;
135
136 impl $crate::metrics_context::FieldDescriptor<$struct_name> for FieldDescriptorImpl {
137 type Value = $field_type;
138
139 fn path(&self) -> &'static str {
140 stringify!($field_name)
141 }
142 }
143
144 FieldDescriptorImpl
145 }
146 )*
147 }
148 };
149}
150
151/// Creates a field reference for direct struct field access (LEGACY API)
152///
153/// This macro captures the field name at compile time and creates a zero-cost
154/// field reference that can be used with MetricsContext methods.
155///
156/// # Examples
157///
158/// ```ignore
159/// struct TradingMetrics {
160/// total_volume: u64,
161/// trade_count: u64,
162/// }
163///
164/// let entity = TradingMetrics { total_volume: 0, trade_count: 0 };
165///
166/// // Create field references
167/// let volume_field = field!(entity, total_volume);
168/// let count_field = field!(entity, trade_count);
169///
170/// // Use with MetricsContext
171/// ctx.get_ref(&volume_field) // Option<u64>
172/// ctx.set_ref(&count_field, 100) // Set trade_count to 100
173/// ctx.increment_ref(&count_field, 1) // Increment by 1
174///
175/// // Nested fields also work
176/// let price_field = field!(entity, reserves.last_price);
177/// ctx.set_ref(&price_field, 123.45);
178/// ```
179///
180/// # Advantages over field_accessor!
181/// - No need to define separate accessor structs
182/// - Field names are validated at compile time
183/// - Type inference works automatically from struct definition
184/// - Less boilerplate code
185#[macro_export]
186macro_rules! field {
187 // Simple field (no dots)
188 ($struct_expr:expr, $field:ident) => {{
189 // Create a zero-sized type that captures the field name
190 struct __FieldRef;
191
192 impl<T> $crate::metrics_context::FieldRef<T> for __FieldRef {
193 fn path(&self) -> &'static str {
194 stringify!($field)
195 }
196 }
197
198 // Return the field reference
199 __FieldRef
200 }};
201
202 // Nested fields with dot notation
203 ($struct_expr:expr, $($field:ident).+) => {{
204 struct __FieldRef;
205
206 impl<T> $crate::metrics_context::FieldRef<T> for __FieldRef {
207 fn path(&self) -> &'static str {
208 $crate::__field_path!($($field),+)
209 }
210 }
211
212 __FieldRef
213 }};
214}
215
216/// Macro to define type-safe field accessors (LEGACY API)
217///
218/// # Examples
219///
220/// ```ignore
221/// // Simple field accessor
222/// field_accessor!(TotalVolume, u64, "total_volume");
223///
224/// // Nested field accessor
225/// field_accessor!(LastPrice, f64, "reserves.last_price");
226///
227/// // Usage with MetricsContext
228/// ctx.get_field(TotalVolume) // returns Option<u64>
229/// ctx.set_field(TotalVolume, 1000)
230/// ```
231///
232/// **DEPRECATED**: Consider using the new `field!` macro instead:
233/// ```ignore
234/// ctx.get(&field!(entity, total_volume)) // Cleaner, no accessor struct needed
235/// ctx.set(&field!(entity, total_volume), 1000)
236/// ```
237#[macro_export]
238macro_rules! field_accessor {
239 ($name:ident, $type:ty, $path:expr) => {
240 pub struct $name;
241
242 impl $crate::metrics_context::FieldAccessor for $name {
243 type Value = $type;
244
245 fn path() -> &'static str {
246 $path
247 }
248 }
249 };
250}
251
252/// Re-export CompiledPath from vm module for public API
253pub use crate::vm::CompiledPath;
254
255/// MetricsContext provides an imperative API for complex aggregation logic
256/// in instruction hooks generated by declarative macros.
257///
258/// **Note:** You don't write instruction hooks directly. Instead, use declarative macros:
259/// - `#[aggregate]` for aggregations (Sum, Count, Min, Max, etc.)
260/// - `#[track_from]` for field tracking
261/// - `#[register_pda]` for PDA mappings
262///
263/// These macros generate instruction hooks internally that use MetricsContext.
264///
265/// It wraps VM registers to provide type-safe access to:
266/// - Instruction data (accounts, args)
267/// - Entity state (current field values)
268/// - Context metadata (slot, signature, timestamp)
269///
270/// # Enhanced Field Descriptor API (NEW - Recommended)
271/// ```ignore
272/// // Define your entity struct with declarative macros
273/// struct TradingMetrics {
274/// #[aggregate(
275/// from = [Buy, Sell],
276/// field = data::amount,
277/// strategy = Sum,
278/// lookup_by = accounts::mint
279/// )]
280/// total_volume: u64,
281///
282/// #[aggregate(
283/// from = [Buy, Sell],
284/// strategy = Count,
285/// lookup_by = accounts::mint
286/// )]
287/// trade_count: u64,
288/// }
289///
290/// // The macro generates hooks that use MetricsContext internally
291/// // to implement the aggregation logic.
292/// ```
293///
294/// # Direct MetricsContext Usage (Internal/Advanced)
295///
296/// If you're implementing custom runtime logic or extending the macro system,
297/// you can use MetricsContext directly with field descriptors:
298///
299/// ```ignore
300/// // Generate field descriptors - replaces field_accessor! macro
301/// impl_field_descriptors!(TradingMetrics {
302/// total_volume: u64,
303/// trade_count: u64
304/// });
305///
306/// // In generated hook function (example - you don't write this)
307/// fn generated_update_metrics(ctx: &mut MetricsContext) {
308/// let volume = ctx.get_field(TradingMetrics::total_volume());
309/// ctx.set_field(TradingMetrics::total_volume(), 1000);
310/// ctx.increment_field(TradingMetrics::trade_count(), 1);
311/// }
312/// ```
313///
314/// # Field Accessor API (Legacy - Still Supported)
315/// ```ignore
316/// // Define field accessors once
317/// field_accessor!(TotalVolume, u64, "total_volume");
318/// field_accessor!(TradeCount, u64, "trade_count");
319///
320/// // Use with compile-time type checking
321/// ctx.get_field_legacy(TotalVolume) // returns Option<u64>
322/// ctx.set_field_legacy(TotalVolume, 100) // type-checked at compile time
323/// ctx.increment_field_legacy(TradeCount, 1)
324/// ```
325///
326/// # String-based API (Legacy - Use for dynamic field access only)
327/// ```ignore
328/// ctx.get::<u64>("total_volume") // String-based, runtime errors possible
329/// ctx.set("total_volume", 100)
330/// ctx.increment("trade_count", 1)
331/// ```
332pub struct MetricsContext<'a> {
333 /// Register holding the current entity state
334 state_reg: Register,
335 /// All VM registers (mutable access for updates)
336 registers: &'a mut Vec<RegisterValue>,
337 /// Compiled field paths for efficient access
338 #[allow(dead_code)]
339 compiled_paths: &'a HashMap<String, CompiledPath>,
340 /// Blockchain slot number
341 slot: Option<u64>,
342 /// Transaction signature
343 signature: Option<String>,
344 /// Unix timestamp (milliseconds)
345 timestamp: i64,
346}
347
348impl<'a> MetricsContext<'a> {
349 /// Create a new MetricsContext wrapping VM state
350 pub fn new(
351 state_reg: Register,
352 registers: &'a mut Vec<RegisterValue>,
353 compiled_paths: &'a HashMap<String, CompiledPath>,
354 slot: Option<u64>,
355 signature: Option<String>,
356 timestamp: i64,
357 ) -> Self {
358 Self {
359 state_reg,
360 registers,
361 compiled_paths,
362 slot,
363 signature,
364 timestamp,
365 }
366 }
367
368 // ========================================================================
369 // Read instruction data
370 // ========================================================================
371
372 /// Get an account address from the instruction by name
373 /// Example: `ctx.account("user")` returns the user account address
374 pub fn account(&self, name: &str) -> Option<String> {
375 // Accounts are stored in registers, typically in a source register
376 // For now, we'll return None - full implementation requires access to source register
377 // This is a placeholder for the actual implementation
378 let _ = name;
379 None
380 }
381
382 /// Get a typed field from instruction data
383 /// Example: `ctx.data::<u64>("amount")` returns the amount field
384 pub fn data<T: for<'de> Deserialize<'de>>(&self, field: &str) -> Option<T> {
385 // Data fields are accessed via compiled paths
386 // For now, placeholder - full implementation requires source register access
387 let _ = field;
388 None
389 }
390
391 // ========================================================================
392 // Read current entity state
393 // ========================================================================
394
395 /// Get a typed value from the current entity state (string-based API)
396 /// Example: `ctx.get::<u64>("total_volume")` returns the current total_volume value
397 pub fn get<T: for<'de> Deserialize<'de>>(&self, field_path: &str) -> Option<T> {
398 let state = self.registers.get(self.state_reg)?;
399
400 // Navigate the field path
401 let segments: Vec<&str> = field_path.split('.').collect();
402 let mut current = state;
403
404 for segment in segments {
405 current = current.get(segment)?;
406 }
407
408 // Deserialize the value
409 serde_json::from_value(current.clone()).ok()
410 }
411
412 /// Get a typed value using a field reference (NEW RECOMMENDED API)
413 /// Example: `ctx.get_ref(&field!(entity, total_volume))` returns Option<u64>
414 ///
415 /// This provides compile-time field name validation and type inference.
416 pub fn get_ref<T, F>(&self, field_ref: &F) -> Option<T>
417 where
418 T: for<'de> Deserialize<'de>,
419 F: FieldRef<T>,
420 {
421 self.get(field_ref.path())
422 }
423
424 /// Type-safe field getter using struct field descriptors (NEW ENHANCED API)
425 /// Example: `ctx.get_field(TradingMetrics::total_volume())` returns `Option<u64>`
426 ///
427 /// This provides compile-time type checking with direct struct field access.
428 pub fn get_field<T, F>(&self, field: F) -> Option<F::Value>
429 where
430 F: FieldDescriptor<T>,
431 {
432 self.get(field.path())
433 }
434
435 /// Type-safe field getter using legacy FieldAccessor trait
436 /// Example: `ctx.get_field_legacy(TotalVolume)` returns `Option<u64>`
437 ///
438 /// This eliminates string literals and provides compile-time type checking.
439 pub fn get_field_legacy<F: FieldAccessor>(&self, _field: F) -> Option<F::Value> {
440 self.get(F::path())
441 }
442
443 // ========================================================================
444 // Update entity state
445 // ========================================================================
446
447 /// Set a field value in the entity state (string-based API)
448 /// Example: `ctx.set("last_trade_timestamp", ctx.timestamp())`
449 pub fn set<T: Serialize>(&mut self, field: &str, value: T) {
450 if let Ok(json_value) = serde_json::to_value(value) {
451 self.set_field_value(field, json_value);
452 }
453 }
454
455 /// Set a field value using a field reference (NEW RECOMMENDED API)
456 /// Example: `ctx.set_ref(&field!(entity, total_volume), 1000)`
457 ///
458 /// This provides compile-time field name validation and type checking.
459 pub fn set_ref<T, F>(&mut self, field_ref: &F, value: T)
460 where
461 T: Serialize,
462 F: FieldRef<T>,
463 {
464 if let Ok(json_value) = serde_json::to_value(value) {
465 self.set_field_value(field_ref.path(), json_value);
466 }
467 }
468
469 /// Type-safe field setter using struct field descriptors (NEW ENHANCED API)
470 /// Example: `ctx.set_field(TradingMetrics::total_volume(), 1000)` sets total_volume to 1000
471 ///
472 /// This provides compile-time type checking with direct struct field access.
473 pub fn set_field<T, F>(&mut self, field: F, value: F::Value)
474 where
475 F: FieldDescriptor<T>,
476 {
477 self.set(field.path(), value)
478 }
479
480 /// Type-safe field setter using legacy FieldAccessor trait
481 /// Example: `ctx.set_field_legacy(TotalVolume, 1000)` sets total_volume to 1000
482 ///
483 /// This eliminates string literals and provides compile-time type checking.
484 pub fn set_field_legacy<F: FieldAccessor>(&mut self, _field: F, value: F::Value) {
485 self.set(F::path(), value)
486 }
487
488 /// Increment a numeric field by a given amount (string-based API)
489 /// Example: `ctx.increment("whale_trade_count", 1)`
490 pub fn increment(&mut self, field: &str, amount: u64) {
491 if let Some(current) = self.get::<u64>(field) {
492 self.set(field, current + amount);
493 } else {
494 self.set(field, amount);
495 }
496 }
497
498 /// Increment a numeric field using a field reference (NEW RECOMMENDED API)
499 /// Example: `ctx.increment_ref(&field!(entity, trade_count), 1)`
500 ///
501 /// This provides compile-time field name validation. Works with u64 fields.
502 pub fn increment_ref<F>(&mut self, field_ref: &F, amount: u64)
503 where
504 F: FieldRef<u64>,
505 {
506 let path = field_ref.path();
507 if let Some(current) = self.get::<u64>(path) {
508 self.set(path, current + amount);
509 } else {
510 self.set(path, amount);
511 }
512 }
513
514 /// Type-safe increment using struct field descriptors (NEW ENHANCED API)
515 /// Example: `ctx.increment_field(TradingMetrics::trade_count(), 1)`
516 ///
517 /// Works with u64 fields and provides compile-time type checking.
518 pub fn increment_field<T, F>(&mut self, field: F, amount: u64)
519 where
520 F: FieldDescriptor<T, Value = u64>,
521 {
522 self.increment(field.path(), amount)
523 }
524
525 /// Type-safe increment using legacy FieldAccessor trait
526 /// Example: `ctx.increment_field_legacy(TradeCount, 1)`
527 ///
528 /// Works with any numeric type that can convert to/from u64.
529 pub fn increment_field_legacy<F: FieldAccessor>(&mut self, _field: F, amount: u64)
530 where
531 F::Value: Into<u64> + From<u64>,
532 {
533 self.increment(F::path(), amount)
534 }
535
536 /// Add a value to a numeric accumulator (alias for increment - string-based API)
537 /// Example: `ctx.sum("total_fees", fee_amount)`
538 pub fn sum(&mut self, field: &str, value: u64) {
539 self.increment(field, value);
540 }
541
542 /// Add a value to a numeric accumulator using a field reference (NEW RECOMMENDED API)
543 /// Example: `ctx.sum_ref(&field!(entity, total_fees), fee_amount)`
544 ///
545 /// This is an alias for `increment_ref()` that may be clearer for accumulation use cases.
546 pub fn sum_ref<F>(&mut self, field_ref: &F, value: u64)
547 where
548 F: FieldRef<u64>,
549 {
550 self.increment_ref(field_ref, value)
551 }
552
553 /// Type-safe sum using struct field descriptors (NEW ENHANCED API)
554 /// Example: `ctx.sum_field(TradingMetrics::total_fees(), fee_amount)`
555 ///
556 /// Works with u64 fields and provides compile-time type checking.
557 pub fn sum_field<T, F>(&mut self, field: F, value: u64)
558 where
559 F: FieldDescriptor<T, Value = u64>,
560 {
561 self.sum(field.path(), value)
562 }
563
564 /// Type-safe sum using legacy FieldAccessor trait
565 /// Example: `ctx.sum_field_legacy(TotalFees, fee_amount)`
566 ///
567 /// Works with any numeric type that can convert to/from u64.
568 pub fn sum_field_legacy<F: FieldAccessor>(&mut self, _field: F, value: u64)
569 where
570 F::Value: Into<u64> + From<u64>,
571 {
572 self.sum(F::path(), value)
573 }
574
575 /// Add a value to a unique set and update the count field
576 /// Example: `ctx.add_unique("unique_traders", user_address)`
577 pub fn add_unique(&mut self, field: &str, value: String) {
578 // Get the internal set field name (conventionally field + "_set")
579 let set_field = format!("{}_set", field);
580
581 // Get existing set or create new one
582 let mut set: HashSet<String> = self.get::<HashSet<String>>(&set_field).unwrap_or_default();
583
584 // Add the value
585 set.insert(value);
586
587 // Update the set and count
588 let count = set.len() as u64;
589 self.set(&set_field, set);
590 self.set(field, count);
591 }
592
593 // ========================================================================
594 // Access context metadata
595 // ========================================================================
596
597 /// Get the current timestamp in milliseconds
598 pub fn timestamp(&self) -> i64 {
599 self.timestamp
600 }
601
602 /// Get the blockchain slot number
603 pub fn slot(&self) -> u64 {
604 self.slot.unwrap_or(0)
605 }
606
607 /// Get the transaction signature
608 pub fn signature(&self) -> &str {
609 self.signature.as_deref().unwrap_or("")
610 }
611
612 // ========================================================================
613 // Internal helpers
614 // ========================================================================
615
616 fn set_field_value(&mut self, field_path: &str, value: Value) {
617 if let Some(state) = self.registers.get_mut(self.state_reg) {
618 if !state.is_object() {
619 *state = Value::Object(serde_json::Map::new());
620 }
621
622 let segments: Vec<&str> = field_path.split('.').collect();
623 let mut current = state;
624
625 // Navigate to the parent object
626 for segment in &segments[..segments.len() - 1] {
627 if current.get(segment).is_none() {
628 current[segment] = Value::Object(serde_json::Map::new());
629 }
630 current = current.get_mut(segment).unwrap();
631 }
632
633 // Set the final field
634 if let Some(last_segment) = segments.last() {
635 current[*last_segment] = value;
636 }
637 }
638 }
639}
640
641// Re-export HashSet for use in add_unique
642use std::collections::HashSet;
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use serde_json::json;
648
649 #[test]
650 fn test_get_field() {
651 let mut registers = vec![json!({
652 "total_volume": 1000,
653 "metrics": {
654 "count": 5
655 }
656 })];
657
658 let compiled_paths = HashMap::new();
659 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
660
661 assert_eq!(ctx.get::<u64>("total_volume"), Some(1000));
662 assert_eq!(ctx.get::<u64>("metrics.count"), Some(5));
663 }
664
665 #[test]
666 fn test_set_field() {
667 let mut registers = vec![json!({})];
668 let compiled_paths = HashMap::new();
669 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
670
671 ctx.set("total_volume", 2000u64);
672 assert_eq!(ctx.get::<u64>("total_volume"), Some(2000));
673 }
674
675 #[test]
676 fn test_increment() {
677 let mut registers = vec![json!({"count": 10})];
678 let compiled_paths = HashMap::new();
679 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
680
681 ctx.increment("count", 5);
682 assert_eq!(ctx.get::<u64>("count"), Some(15));
683
684 // Test incrementing non-existent field
685 ctx.increment("new_count", 3);
686 assert_eq!(ctx.get::<u64>("new_count"), Some(3));
687 }
688
689 #[test]
690 fn test_context_metadata() {
691 let mut registers = vec![json!({})];
692 let compiled_paths = HashMap::new();
693 let ctx = MetricsContext::new(
694 0,
695 &mut registers,
696 &compiled_paths,
697 Some(12345),
698 Some("abc123".to_string()),
699 1000000,
700 );
701
702 assert_eq!(ctx.slot(), 12345);
703 assert_eq!(ctx.signature(), "abc123");
704 assert_eq!(ctx.timestamp(), 1000000);
705 }
706
707 #[test]
708 fn test_enhanced_field_descriptor_api() {
709 // Define a struct representing our entity
710 struct TradingMetrics {
711 total_volume: u64,
712 trade_count: u64,
713 }
714
715 // Generate field descriptors for the struct
716 impl_field_descriptors!(TradingMetrics {
717 total_volume: u64,
718 trade_count: u64
719 });
720
721 let mut registers = vec![json!({
722 "total_volume": 1000,
723 "trade_count": 5
724 })];
725
726 let compiled_paths = HashMap::new();
727 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
728
729 // Test enhanced API - direct struct field access
730 assert_eq!(ctx.get_field(TradingMetrics::total_volume()), Some(1000));
731 assert_eq!(ctx.get_field(TradingMetrics::trade_count()), Some(5));
732
733 // Test type-safe set
734 ctx.set_field(TradingMetrics::total_volume(), 2000);
735 assert_eq!(ctx.get_field(TradingMetrics::total_volume()), Some(2000));
736
737 // Test type-safe increment
738 ctx.increment_field(TradingMetrics::trade_count(), 3);
739 assert_eq!(ctx.get_field(TradingMetrics::trade_count()), Some(8));
740
741 // Test type-safe sum
742 ctx.sum_field(TradingMetrics::total_volume(), 500);
743 assert_eq!(ctx.get_field(TradingMetrics::total_volume()), Some(2500));
744 }
745
746 #[test]
747 fn test_legacy_field_accessor_api() {
748 // Define field accessors using the legacy macro
749 field_accessor!(TotalVolume, u64, "total_volume");
750 field_accessor!(TradeCount, u64, "trade_count");
751 field_accessor!(LastPrice, u64, "reserves.last_price");
752
753 let mut registers = vec![json!({
754 "total_volume": 1000,
755 "trade_count": 5,
756 "reserves": {
757 "last_price": 250
758 }
759 })];
760
761 let compiled_paths = HashMap::new();
762 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
763
764 // Test legacy type-safe get
765 assert_eq!(ctx.get_field_legacy(TotalVolume), Some(1000));
766 assert_eq!(ctx.get_field_legacy(TradeCount), Some(5));
767 assert_eq!(ctx.get_field_legacy(LastPrice), Some(250));
768
769 // Test legacy type-safe set
770 ctx.set_field_legacy(TotalVolume, 2000);
771 assert_eq!(ctx.get_field_legacy(TotalVolume), Some(2000));
772
773 // Test legacy type-safe increment
774 ctx.increment_field_legacy(TradeCount, 3);
775 assert_eq!(ctx.get_field_legacy(TradeCount), Some(8));
776
777 // Test legacy type-safe sum
778 ctx.sum_field_legacy(TotalVolume, 500);
779 assert_eq!(ctx.get_field_legacy(TotalVolume), Some(2500));
780
781 // Test nested path
782 ctx.set_field_legacy(LastPrice, 300);
783 assert_eq!(ctx.get_field_legacy(LastPrice), Some(300));
784 }
785
786 #[test]
787 fn test_enhanced_api_with_different_types() {
788 // Test with different field types
789 struct PriceMetrics {
790 average_price: f64,
791 volume: u64,
792 }
793
794 impl_field_descriptors!(PriceMetrics {
795 average_price: f64,
796 volume: u64
797 });
798
799 let mut registers = vec![json!({})];
800 let compiled_paths = HashMap::new();
801 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
802
803 ctx.set_field(PriceMetrics::average_price(), 123.45);
804 assert_eq!(ctx.get_field(PriceMetrics::average_price()), Some(123.45));
805
806 ctx.set_field(PriceMetrics::volume(), 1000);
807 assert_eq!(ctx.get_field(PriceMetrics::volume()), Some(1000));
808 }
809
810 #[test]
811 fn test_legacy_api_with_different_types() {
812 // Test legacy API with f64 type
813 field_accessor!(AveragePrice, f64, "average_price");
814
815 let mut registers = vec![json!({})];
816 let compiled_paths = HashMap::new();
817 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
818
819 ctx.set_field_legacy(AveragePrice, 123.45);
820 assert_eq!(ctx.get_field_legacy(AveragePrice), Some(123.45));
821 }
822
823 #[test]
824 fn test_field_ref_api() {
825 // Define a struct to represent our entity
826 struct TradingMetrics {
827 total_volume: u64,
828 trade_count: u64,
829 last_price: f64,
830 }
831
832 // Create an instance (the actual field values don't matter, we just need the struct for field! macro)
833 let entity = TradingMetrics {
834 total_volume: 0,
835 trade_count: 0,
836 last_price: 0.0,
837 };
838
839 let mut registers = vec![json!({
840 "total_volume": 1000,
841 "trade_count": 5,
842 "last_price": 250.5
843 })];
844
845 let compiled_paths = HashMap::new();
846 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
847
848 // ========================================================================
849 // Test new field reference API - cleaner than field_accessor!
850 // ========================================================================
851
852 // Test get_ref
853 assert_eq!(
854 ctx.get_ref::<u64, _>(&field!(entity, total_volume)),
855 Some(1000)
856 );
857 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, trade_count)), Some(5));
858 assert_eq!(
859 ctx.get_ref::<f64, _>(&field!(entity, last_price)),
860 Some(250.5)
861 );
862
863 // Test set_ref
864 ctx.set_ref(&field!(entity, total_volume), 2000u64);
865 assert_eq!(
866 ctx.get_ref::<u64, _>(&field!(entity, total_volume)),
867 Some(2000)
868 );
869
870 ctx.set_ref(&field!(entity, last_price), 300.75);
871 assert_eq!(
872 ctx.get_ref::<f64, _>(&field!(entity, last_price)),
873 Some(300.75)
874 );
875
876 // Test increment_ref
877 ctx.increment_ref(&field!(entity, trade_count), 3);
878 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, trade_count)), Some(8));
879
880 // Test sum_ref
881 ctx.sum_ref(&field!(entity, total_volume), 500);
882 assert_eq!(
883 ctx.get_ref::<u64, _>(&field!(entity, total_volume)),
884 Some(2500)
885 );
886 }
887
888 #[test]
889 fn test_field_ref_with_nested_struct() {
890 // Test with nested fields
891 struct Metrics {
892 reserves: Reserves,
893 }
894
895 struct Reserves {
896 last_price: f64,
897 }
898
899 let entity = Metrics {
900 reserves: Reserves { last_price: 0.0 },
901 };
902
903 let mut registers = vec![json!({
904 "reserves": {
905 "last_price": 100.5
906 }
907 })];
908
909 let compiled_paths = HashMap::new();
910 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
911
912 // Test nested field access with dot notation
913 assert_eq!(
914 ctx.get_ref::<f64, _>(&field!(entity, reserves.last_price)),
915 Some(100.5)
916 );
917
918 ctx.set_ref(&field!(entity, reserves.last_price), 200.75);
919 assert_eq!(
920 ctx.get_ref::<f64, _>(&field!(entity, reserves.last_price)),
921 Some(200.75)
922 );
923 }
924
925 #[test]
926 fn test_enhanced_api_field_initialization() {
927 // Test that increment/sum works when field doesn't exist yet
928 struct WhaleMetrics {
929 whale_trade_count: u64,
930 total_whale_volume: u64,
931 }
932
933 impl_field_descriptors!(WhaleMetrics {
934 whale_trade_count: u64,
935 total_whale_volume: u64
936 });
937
938 let mut registers = vec![json!({})];
939 let compiled_paths = HashMap::new();
940 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
941
942 // Increment on non-existent field should initialize to the amount
943 ctx.increment_field(WhaleMetrics::whale_trade_count(), 1);
944 assert_eq!(ctx.get_field(WhaleMetrics::whale_trade_count()), Some(1));
945
946 // Subsequent increment should add to existing value
947 ctx.increment_field(WhaleMetrics::whale_trade_count(), 2);
948 assert_eq!(ctx.get_field(WhaleMetrics::whale_trade_count()), Some(3));
949
950 // Test sum field initialization
951 ctx.sum_field(WhaleMetrics::total_whale_volume(), 5000);
952 assert_eq!(
953 ctx.get_field(WhaleMetrics::total_whale_volume()),
954 Some(5000)
955 );
956
957 ctx.sum_field(WhaleMetrics::total_whale_volume(), 3000);
958 assert_eq!(
959 ctx.get_field(WhaleMetrics::total_whale_volume()),
960 Some(8000)
961 );
962 }
963
964 #[test]
965 fn test_legacy_field_ref_initialization() {
966 // Test legacy field ref API that increment_ref works when field doesn't exist yet
967 struct Metrics {
968 whale_trade_count: u64,
969 }
970
971 let entity = Metrics {
972 whale_trade_count: 0,
973 };
974
975 let mut registers = vec![json!({})];
976 let compiled_paths = HashMap::new();
977 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
978
979 // Increment on non-existent field should initialize to the amount
980 ctx.increment_ref(&field!(entity, whale_trade_count), 1);
981 assert_eq!(
982 ctx.get_ref::<u64, _>(&field!(entity, whale_trade_count)),
983 Some(1)
984 );
985
986 // Subsequent increment should add to existing value
987 ctx.increment_ref(&field!(entity, whale_trade_count), 2);
988 assert_eq!(
989 ctx.get_ref::<u64, _>(&field!(entity, whale_trade_count)),
990 Some(3)
991 );
992 }
993
994 #[test]
995 fn test_backward_compatibility() {
996 // Verify that old string-based API still works
997 let mut registers = vec![json!({
998 "volume": 100
999 })];
1000
1001 let compiled_paths = HashMap::new();
1002 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
1003
1004 // Old string API should still work
1005 assert_eq!(ctx.get::<u64>("volume"), Some(100));
1006 ctx.set("volume", 200u64);
1007 assert_eq!(ctx.get::<u64>("volume"), Some(200));
1008 ctx.increment("volume", 50);
1009 assert_eq!(ctx.get::<u64>("volume"), Some(250));
1010 }
1011}