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![
652 json!({
653 "total_volume": 1000,
654 "metrics": {
655 "count": 5
656 }
657 })
658 ];
659
660 let compiled_paths = HashMap::new();
661 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
662
663 assert_eq!(ctx.get::<u64>("total_volume"), Some(1000));
664 assert_eq!(ctx.get::<u64>("metrics.count"), Some(5));
665 }
666
667 #[test]
668 fn test_set_field() {
669 let mut registers = vec![json!({})];
670 let compiled_paths = HashMap::new();
671 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
672
673 ctx.set("total_volume", 2000u64);
674 assert_eq!(ctx.get::<u64>("total_volume"), Some(2000));
675 }
676
677 #[test]
678 fn test_increment() {
679 let mut registers = vec![json!({"count": 10})];
680 let compiled_paths = HashMap::new();
681 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
682
683 ctx.increment("count", 5);
684 assert_eq!(ctx.get::<u64>("count"), Some(15));
685
686 // Test incrementing non-existent field
687 ctx.increment("new_count", 3);
688 assert_eq!(ctx.get::<u64>("new_count"), Some(3));
689 }
690
691 #[test]
692 fn test_context_metadata() {
693 let mut registers = vec![json!({})];
694 let compiled_paths = HashMap::new();
695 let ctx = MetricsContext::new(
696 0,
697 &mut registers,
698 &compiled_paths,
699 Some(12345),
700 Some("abc123".to_string()),
701 1000000,
702 );
703
704 assert_eq!(ctx.slot(), 12345);
705 assert_eq!(ctx.signature(), "abc123");
706 assert_eq!(ctx.timestamp(), 1000000);
707 }
708
709 #[test]
710 fn test_enhanced_field_descriptor_api() {
711 // Define a struct representing our entity
712 struct TradingMetrics {
713 total_volume: u64,
714 trade_count: u64,
715 }
716
717 // Generate field descriptors for the struct
718 impl_field_descriptors!(TradingMetrics {
719 total_volume: u64,
720 trade_count: u64
721 });
722
723 let mut registers = vec![json!({
724 "total_volume": 1000,
725 "trade_count": 5
726 })];
727
728 let compiled_paths = HashMap::new();
729 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
730
731 // Test enhanced API - direct struct field access
732 assert_eq!(ctx.get_field(TradingMetrics::total_volume()), Some(1000));
733 assert_eq!(ctx.get_field(TradingMetrics::trade_count()), Some(5));
734
735 // Test type-safe set
736 ctx.set_field(TradingMetrics::total_volume(), 2000);
737 assert_eq!(ctx.get_field(TradingMetrics::total_volume()), Some(2000));
738
739 // Test type-safe increment
740 ctx.increment_field(TradingMetrics::trade_count(), 3);
741 assert_eq!(ctx.get_field(TradingMetrics::trade_count()), Some(8));
742
743 // Test type-safe sum
744 ctx.sum_field(TradingMetrics::total_volume(), 500);
745 assert_eq!(ctx.get_field(TradingMetrics::total_volume()), Some(2500));
746 }
747
748 #[test]
749 fn test_legacy_field_accessor_api() {
750 // Define field accessors using the legacy macro
751 field_accessor!(TotalVolume, u64, "total_volume");
752 field_accessor!(TradeCount, u64, "trade_count");
753 field_accessor!(LastPrice, u64, "reserves.last_price");
754
755 let mut registers = vec![json!({
756 "total_volume": 1000,
757 "trade_count": 5,
758 "reserves": {
759 "last_price": 250
760 }
761 })];
762
763 let compiled_paths = HashMap::new();
764 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
765
766 // Test legacy type-safe get
767 assert_eq!(ctx.get_field_legacy(TotalVolume), Some(1000));
768 assert_eq!(ctx.get_field_legacy(TradeCount), Some(5));
769 assert_eq!(ctx.get_field_legacy(LastPrice), Some(250));
770
771 // Test legacy type-safe set
772 ctx.set_field_legacy(TotalVolume, 2000);
773 assert_eq!(ctx.get_field_legacy(TotalVolume), Some(2000));
774
775 // Test legacy type-safe increment
776 ctx.increment_field_legacy(TradeCount, 3);
777 assert_eq!(ctx.get_field_legacy(TradeCount), Some(8));
778
779 // Test legacy type-safe sum
780 ctx.sum_field_legacy(TotalVolume, 500);
781 assert_eq!(ctx.get_field_legacy(TotalVolume), Some(2500));
782
783 // Test nested path
784 ctx.set_field_legacy(LastPrice, 300);
785 assert_eq!(ctx.get_field_legacy(LastPrice), Some(300));
786 }
787
788 #[test]
789 fn test_enhanced_api_with_different_types() {
790 // Test with different field types
791 struct PriceMetrics {
792 average_price: f64,
793 volume: u64,
794 }
795
796 impl_field_descriptors!(PriceMetrics {
797 average_price: f64,
798 volume: u64
799 });
800
801 let mut registers = vec![json!({})];
802 let compiled_paths = HashMap::new();
803 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
804
805 ctx.set_field(PriceMetrics::average_price(), 123.45);
806 assert_eq!(ctx.get_field(PriceMetrics::average_price()), Some(123.45));
807
808 ctx.set_field(PriceMetrics::volume(), 1000);
809 assert_eq!(ctx.get_field(PriceMetrics::volume()), Some(1000));
810 }
811
812 #[test]
813 fn test_legacy_api_with_different_types() {
814 // Test legacy API with f64 type
815 field_accessor!(AveragePrice, f64, "average_price");
816
817 let mut registers = vec![json!({})];
818 let compiled_paths = HashMap::new();
819 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
820
821 ctx.set_field_legacy(AveragePrice, 123.45);
822 assert_eq!(ctx.get_field_legacy(AveragePrice), Some(123.45));
823 }
824
825 #[test]
826 fn test_field_ref_api() {
827 // Define a struct to represent our entity
828 struct TradingMetrics {
829 total_volume: u64,
830 trade_count: u64,
831 last_price: f64,
832 }
833
834 // Create an instance (the actual field values don't matter, we just need the struct for field! macro)
835 let entity = TradingMetrics {
836 total_volume: 0,
837 trade_count: 0,
838 last_price: 0.0,
839 };
840
841 let mut registers = vec![json!({
842 "total_volume": 1000,
843 "trade_count": 5,
844 "last_price": 250.5
845 })];
846
847 let compiled_paths = HashMap::new();
848 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
849
850 // ========================================================================
851 // Test new field reference API - cleaner than field_accessor!
852 // ========================================================================
853
854 // Test get_ref
855 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, total_volume)), Some(1000));
856 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, trade_count)), Some(5));
857 assert_eq!(ctx.get_ref::<f64, _>(&field!(entity, last_price)), Some(250.5));
858
859 // Test set_ref
860 ctx.set_ref(&field!(entity, total_volume), 2000u64);
861 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, total_volume)), Some(2000));
862
863 ctx.set_ref(&field!(entity, last_price), 300.75);
864 assert_eq!(ctx.get_ref::<f64, _>(&field!(entity, last_price)), Some(300.75));
865
866 // Test increment_ref
867 ctx.increment_ref(&field!(entity, trade_count), 3);
868 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, trade_count)), Some(8));
869
870 // Test sum_ref
871 ctx.sum_ref(&field!(entity, total_volume), 500);
872 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, total_volume)), Some(2500));
873 }
874
875 #[test]
876 fn test_field_ref_with_nested_struct() {
877 // Test with nested fields
878 struct Metrics {
879 reserves: Reserves,
880 }
881
882 struct Reserves {
883 last_price: f64,
884 }
885
886 let entity = Metrics {
887 reserves: Reserves { last_price: 0.0 },
888 };
889
890 let mut registers = vec![json!({
891 "reserves": {
892 "last_price": 100.5
893 }
894 })];
895
896 let compiled_paths = HashMap::new();
897 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
898
899 // Test nested field access with dot notation
900 assert_eq!(ctx.get_ref::<f64, _>(&field!(entity, reserves.last_price)), Some(100.5));
901
902 ctx.set_ref(&field!(entity, reserves.last_price), 200.75);
903 assert_eq!(ctx.get_ref::<f64, _>(&field!(entity, reserves.last_price)), Some(200.75));
904 }
905
906 #[test]
907 fn test_enhanced_api_field_initialization() {
908 // Test that increment/sum works when field doesn't exist yet
909 struct WhaleMetrics {
910 whale_trade_count: u64,
911 total_whale_volume: u64,
912 }
913
914 impl_field_descriptors!(WhaleMetrics {
915 whale_trade_count: u64,
916 total_whale_volume: u64
917 });
918
919 let mut registers = vec![json!({})];
920 let compiled_paths = HashMap::new();
921 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
922
923 // Increment on non-existent field should initialize to the amount
924 ctx.increment_field(WhaleMetrics::whale_trade_count(), 1);
925 assert_eq!(ctx.get_field(WhaleMetrics::whale_trade_count()), Some(1));
926
927 // Subsequent increment should add to existing value
928 ctx.increment_field(WhaleMetrics::whale_trade_count(), 2);
929 assert_eq!(ctx.get_field(WhaleMetrics::whale_trade_count()), Some(3));
930
931 // Test sum field initialization
932 ctx.sum_field(WhaleMetrics::total_whale_volume(), 5000);
933 assert_eq!(ctx.get_field(WhaleMetrics::total_whale_volume()), Some(5000));
934
935 ctx.sum_field(WhaleMetrics::total_whale_volume(), 3000);
936 assert_eq!(ctx.get_field(WhaleMetrics::total_whale_volume()), Some(8000));
937 }
938
939 #[test]
940 fn test_legacy_field_ref_initialization() {
941 // Test legacy field ref API that increment_ref works when field doesn't exist yet
942 struct Metrics {
943 whale_trade_count: u64,
944 }
945
946 let entity = Metrics { whale_trade_count: 0 };
947
948 let mut registers = vec![json!({})];
949 let compiled_paths = HashMap::new();
950 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
951
952 // Increment on non-existent field should initialize to the amount
953 ctx.increment_ref(&field!(entity, whale_trade_count), 1);
954 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, whale_trade_count)), Some(1));
955
956 // Subsequent increment should add to existing value
957 ctx.increment_ref(&field!(entity, whale_trade_count), 2);
958 assert_eq!(ctx.get_ref::<u64, _>(&field!(entity, whale_trade_count)), Some(3));
959 }
960
961 #[test]
962 fn test_backward_compatibility() {
963 // Verify that old string-based API still works
964 let mut registers = vec![json!({
965 "volume": 100
966 })];
967
968 let compiled_paths = HashMap::new();
969 let mut ctx = MetricsContext::new(0, &mut registers, &compiled_paths, None, None, 0);
970
971 // Old string API should still work
972 assert_eq!(ctx.get::<u64>("volume"), Some(100));
973 ctx.set("volume", 200u64);
974 assert_eq!(ctx.get::<u64>("volume"), Some(200));
975 ctx.increment("volume", 50);
976 assert_eq!(ctx.get::<u64>("volume"), Some(250));
977 }
978}