aimdb_core/remote/
metadata.rs

1//! Record metadata types for remote introspection
2
3use core::any::TypeId;
4use serde::{Deserialize, Serialize};
5use std::string::String;
6
7use crate::record_id::{RecordId, RecordKey};
8
9/// Metadata about a registered record type
10///
11/// Provides information for remote introspection, including buffer
12/// configuration, producer/consumer counts, and timestamps.
13///
14/// When the `metrics` feature is enabled, additional fields are included
15/// for buffer-level statistics (produced_count, consumed_count, etc.).
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct RecordMetadata {
18    /// Unique record identifier (index in the storage)
19    pub record_id: u32,
20
21    /// Unique record key (stable identifier for lookup)
22    pub record_key: String,
23
24    /// Record type name (Rust type name)
25    pub name: String,
26
27    /// TypeId as hexadecimal string
28    pub type_id: String,
29
30    /// Buffer type: "spmc_ring", "single_latest", "mailbox", or "none"
31    pub buffer_type: String,
32
33    /// Buffer capacity (None for unbounded or no buffer)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub buffer_capacity: Option<usize>,
36
37    /// Number of registered producer services
38    pub producer_count: usize,
39
40    /// Number of registered consumer services
41    pub consumer_count: usize,
42
43    /// Whether write operations are permitted for this record
44    pub writable: bool,
45
46    /// When the record was registered (ISO 8601)
47    pub created_at: String,
48
49    /// Last update timestamp (ISO 8601), None if never updated
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub last_update: Option<String>,
52
53    /// Number of outbound connector links registered
54    pub outbound_connector_count: usize,
55
56    // ===== Buffer metrics (feature-gated) =====
57    /// Total items pushed to the buffer (metrics feature only)
58    #[cfg(feature = "metrics")]
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub produced_count: Option<u64>,
61
62    /// Total items consumed from the buffer (metrics feature only)
63    #[cfg(feature = "metrics")]
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub consumed_count: Option<u64>,
66
67    /// Total items dropped due to overflow/lag (metrics feature only)
68    #[cfg(feature = "metrics")]
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub dropped_count: Option<u64>,
71
72    /// Current buffer occupancy: (items, capacity) (metrics feature only)
73    #[cfg(feature = "metrics")]
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub occupancy: Option<(usize, usize)>,
76}
77
78impl RecordMetadata {
79    /// Creates a new record metadata entry
80    ///
81    /// # Arguments
82    /// * `record_id` - The RecordId index
83    /// * `record_key` - The unique record key
84    /// * `type_id` - The TypeId of the record
85    /// * `name` - The Rust type name
86    /// * `buffer_type` - Buffer type string
87    /// * `buffer_capacity` - Optional buffer capacity
88    /// * `producer_count` - Number of producers
89    /// * `consumer_count` - Number of consumers
90    /// * `writable` - Whether writes are permitted
91    /// * `created_at` - Creation timestamp (ISO 8601)
92    /// * `outbound_connector_count` - Number of outbound connectors
93    #[allow(clippy::too_many_arguments)]
94    pub fn new<K: RecordKey>(
95        record_id: RecordId,
96        record_key: K,
97        type_id: TypeId,
98        name: String,
99        buffer_type: String,
100        buffer_capacity: Option<usize>,
101        producer_count: usize,
102        consumer_count: usize,
103        writable: bool,
104        created_at: String,
105        outbound_connector_count: usize,
106    ) -> Self {
107        Self {
108            record_id: record_id.raw(),
109            record_key: record_key.as_str().to_string(),
110            name,
111            type_id: format!("{:?}", type_id),
112            buffer_type,
113            buffer_capacity,
114            producer_count,
115            consumer_count,
116            writable,
117            created_at,
118            last_update: None,
119            outbound_connector_count,
120            #[cfg(feature = "metrics")]
121            produced_count: None,
122            #[cfg(feature = "metrics")]
123            consumed_count: None,
124            #[cfg(feature = "metrics")]
125            dropped_count: None,
126            #[cfg(feature = "metrics")]
127            occupancy: None,
128        }
129    }
130
131    /// Sets the last update timestamp
132    pub fn with_last_update(mut self, timestamp: String) -> Self {
133        self.last_update = Some(timestamp);
134        self
135    }
136
137    /// Sets the last update timestamp from an Option
138    pub fn with_last_update_opt(mut self, timestamp: Option<String>) -> Self {
139        self.last_update = timestamp;
140        self
141    }
142
143    /// Sets buffer metrics from a snapshot (metrics feature only)
144    ///
145    /// Populates produced_count, consumed_count, dropped_count, and occupancy
146    /// from the provided metrics snapshot.
147    #[cfg(feature = "metrics")]
148    pub fn with_buffer_metrics(mut self, snapshot: crate::buffer::BufferMetricsSnapshot) -> Self {
149        self.produced_count = Some(snapshot.produced_count);
150        self.consumed_count = Some(snapshot.consumed_count);
151        self.dropped_count = Some(snapshot.dropped_count);
152        // Only include occupancy if it's meaningful (non-zero capacity)
153        if snapshot.occupancy.1 > 0 {
154            self.occupancy = Some(snapshot.occupancy);
155        }
156        self
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::record_id::StringKey;
164
165    #[test]
166    fn test_record_metadata_creation() {
167        let type_id = TypeId::of::<i32>();
168        let metadata = RecordMetadata::new(
169            RecordId::new(0),
170            StringKey::new("test.record"),
171            type_id,
172            "i32".to_string(),
173            "spmc_ring".to_string(),
174            Some(100),
175            1,
176            2,
177            false,
178            "2025-10-31T10:00:00.000Z".to_string(),
179            0,
180        );
181
182        assert_eq!(metadata.record_id, 0);
183        assert_eq!(metadata.record_key, "test.record");
184        assert_eq!(metadata.name, "i32");
185        assert_eq!(metadata.buffer_type, "spmc_ring");
186        assert_eq!(metadata.buffer_capacity, Some(100));
187        assert_eq!(metadata.producer_count, 1);
188        assert_eq!(metadata.consumer_count, 2);
189        assert_eq!(metadata.outbound_connector_count, 0);
190        assert!(!metadata.writable);
191    }
192
193    #[test]
194    fn test_record_metadata_serialization() {
195        let type_id = TypeId::of::<String>();
196        let metadata = RecordMetadata::new(
197            RecordId::new(1),
198            StringKey::new("app.config"),
199            type_id,
200            "String".to_string(),
201            "single_latest".to_string(),
202            None,
203            1,
204            1,
205            true,
206            "2025-10-31T10:00:00.000Z".to_string(),
207            2,
208        )
209        .with_last_update("2025-10-31T12:00:00.000Z".to_string());
210
211        let json = serde_json::to_string(&metadata).unwrap();
212        assert!(json.contains("\"record_id\":1"));
213        assert!(json.contains("\"record_key\":\"app.config\""));
214        assert!(json.contains("\"name\":\"String\""));
215        assert!(json.contains("\"buffer_type\":\"single_latest\""));
216        assert!(json.contains("\"writable\":true"));
217        assert!(json.contains("\"outbound_connector_count\":2"));
218    }
219}