Skip to main content

aimdb_data_contracts/
lib.rs

1//! # AimDB Data Contracts
2//!
3//! Trait definitions for self-describing data schemas that work identically
4//! across MCU, edge, and cloud.
5//!
6//! This crate provides:
7//! - **Schema types** — Data structures with unique identifiers ([`SchemaType`])
8//! - **Streamable** — Marker for types that cross serialization boundaries ([`Streamable`])
9//! - **Contract profiles** — Configuration for runtime behavior
10//! - **Simulation support** — Generate realistic test data
11//!
12//! ## Design Philosophy
13//!
14//! Contracts separate **what data looks like** (schema) from **how it behaves at runtime**
15//! (policies). This enables:
16//! - Reusable schemas across different deployment configurations
17//! - Type-safe data exchange between systems
18//! - Configurable policies without changing code
19//!
20//! ## Defining a Custom Contract
21//!
22//! ```rust
23//! use aimdb_data_contracts::{SchemaType, Streamable};
24//! use serde::{Serialize, Deserialize};
25//!
26//! #[derive(Clone, Debug, Serialize, Deserialize)]
27//! pub struct MyCustomSensor {
28//!     pub reading: f64,
29//!     pub timestamp: u64,
30//! }
31//!
32//! impl SchemaType for MyCustomSensor {
33//!     const NAME: &'static str = "my_custom_sensor";
34//! }
35//!
36//! // Mark as streamable — can cross WebSocket / WASM boundaries
37//! impl Streamable for MyCustomSensor {}
38//! ```
39
40#![cfg_attr(not(feature = "std"), no_std)]
41
42#[cfg(feature = "std")]
43extern crate std;
44
45extern crate alloc;
46
47mod streamable;
48pub use streamable::Streamable;
49
50#[cfg(feature = "linkable")]
51mod linkable;
52
53#[cfg(feature = "observable")]
54mod observable;
55
56#[cfg(feature = "observable")]
57pub use observable::log_tap;
58
59#[cfg(feature = "simulatable")]
60mod simulatable;
61
62#[cfg(feature = "migratable")]
63mod migratable;
64
65#[cfg(feature = "simulatable")]
66pub use simulatable::{SimulationConfig, SimulationParams};
67
68#[cfg(feature = "migratable")]
69pub use migratable::{MigrationChain, MigrationError, MigrationStep};
70
71// ═══════════════════════════════════════════════════════════════════
72// SCHEMA TRAITS (Implementation-defined)
73// ═══════════════════════════════════════════════════════════════════
74
75/// Identity and metadata for a data contract.
76///
77/// Every schema type has a unique name and version used for:
78/// - Record registration in AimDB
79/// - Profile matching in contract configurations
80/// - Wire protocol identification
81/// - Version compatibility checking
82///
83/// # Versioning and Backward Compatibility
84///
85/// The `VERSION` constant tracks schema evolution. When following backward
86/// compatibility rules, a server running version N can safely ingest data
87/// from producers running any version 1..=N.
88///
89/// ## Compatibility Rules
90///
91/// | Change Type | Allowed? | Example |
92/// |-------------|----------|---------|
93/// | Add optional field | ✅ Yes | `#[serde(default)]` new field |
94/// | Add field with default | ✅ Yes | New field deserializes to default |
95/// | Remove unused field | ✅ Yes | Old data with field still parses |
96/// | Rename field | ⚠️ Migration | Use `MigrationStep` + `migration_chain!` |
97/// | Change field type | ⚠️ Migration | Use `MigrationStep` + `migration_chain!` |
98/// | Add required field | ⚠️ Migration | Use `MigrationStep` + `migration_chain!` |
99///
100/// For breaking changes, implement `MigrationStep` and use `migration_chain!` (requires `migratable` feature)
101/// to provide runtime transformation of older data formats.
102pub trait SchemaType: Sized {
103    /// Unique identifier for this schema (e.g., "temperature", "humidity")
104    const NAME: &'static str;
105
106    /// Schema version. Defaults to 1.
107    ///
108    /// Increment when adding new optional/defaulted fields.
109    const VERSION: u32 = 1;
110}
111
112// ═══════════════════════════════════════════════════════════════════
113// SIMULATABLE SUPPORT (feature = "simulatable")
114// ═══════════════════════════════════════════════════════════════════
115
116/// Generate realistic test/simulation data.
117///
118/// This is an intrinsic capability of the schema type itself,
119/// not a policy decision. If a type can be simulated, implement this.
120#[cfg(feature = "simulatable")]
121pub trait Simulatable: SchemaType {
122    /// Generate a new sample with optional reference to previous value.
123    ///
124    /// # Parameters
125    /// - `config`: Simulation parameters (type-specific)
126    /// - `previous`: Optional reference to last generated value (for random walks, trends)
127    /// - `rng`: Random number generator
128    /// - `timestamp`: Unix timestamp in milliseconds
129    fn simulate<R: rand::Rng>(
130        config: &SimulationConfig,
131        previous: Option<&Self>,
132        rng: &mut R,
133        timestamp: u64,
134    ) -> Self;
135}
136
137/// Construct a schema instance from its primary value.
138///
139/// This defines the canonical way to create a new reading/measurement.
140pub trait Settable: SchemaType {
141    /// The primary value type (e.g., `f32` for temperature)
142    type Value;
143
144    /// Create a new instance from a value.
145    ///
146    /// # Parameters
147    /// - `value`: The primary data value
148    /// - `timestamp`: Unix timestamp in milliseconds
149    fn set(value: Self::Value, timestamp: u64) -> Self;
150}
151
152// ═══════════════════════════════════════════════════════════════════
153// OBSERVABLE SUPPORT
154// ═══════════════════════════════════════════════════════════════════
155
156/// Extract a signal value for observation.
157///
158/// Implement this trait to enable threshold checking, alerting,
159/// and other signal-based operations on your schema type.
160///
161/// The extracted signal can be used by node implementations to:
162/// - Check against configured thresholds
163/// - Trigger alerts when bounds are exceeded
164/// - Compute aggregations (mean, min, max)
165/// - Feed into monitoring systems
166/// - Format log output with `format_log()`
167pub trait Observable: SchemaType {
168    /// The numeric type of the signal (e.g., `f32`, `f64`, `i32`).
169    ///
170    /// Must be comparable and copyable for threshold checks.
171    type Signal: PartialOrd + Copy;
172
173    /// Icon/emoji for log output (e.g., "🌡️", "💧", "📊")
174    ///
175    /// Override this to provide a visual indicator for your data type.
176    const ICON: &'static str = "📊";
177
178    /// Unit label for the signal (e.g., "°C", "%", "hPa")
179    ///
180    /// Override this to display the appropriate unit in log output.
181    const UNIT: &'static str = "";
182
183    /// Extract the signal value from this instance.
184    fn signal(&self) -> Self::Signal;
185
186    /// Format a log entry for this observation.
187    ///
188    /// The default implementation uses `Debug` formatting. Override this
189    /// for prettier, human-readable output.
190    ///
191    /// # Example output
192    /// ```text
193    /// 🌡️ [alpha] Temperature: 22.5°C at 1704326400000
194    /// 💧 [beta] Humidity: 65.3% at 1704326400000
195    /// ```
196    fn format_log(&self, node_id: &str) -> alloc::string::String
197    where
198        Self: core::fmt::Debug,
199    {
200        alloc::format!("{} [{}] {:?}", Self::ICON, node_id, self)
201    }
202}
203
204// ═══════════════════════════════════════════════════════════════════
205// LINKABLE SUPPORT (feature = "linkable")
206// ═══════════════════════════════════════════════════════════════════
207
208/// Types that can be serialized/deserialized for connector links.
209///
210/// Implement this trait to enable `link_from` and `link_to` operations
211/// in AimDB connectors (MQTT, KNX, etc.). This provides the wire format
212/// for transporting schema types across network boundaries.
213///
214/// # Example
215///
216/// ```rust,ignore
217/// use aimdb_data_contracts::Linkable;
218/// use my_app::Temperature;  // user-defined type implementing Linkable
219///
220/// // In connector configuration:
221/// builder.configure::<Temperature>(NODE_ID, |reg| {
222///     reg.buffer(BufferCfg::SingleLatest)
223///         .link_from("mqtt://sensors/temperature")
224///         .with_deserializer(Temperature::from_bytes)
225///         .finish();
226/// });
227/// ```
228#[cfg(feature = "linkable")]
229pub trait Linkable: SchemaType + Sized {
230    /// Deserialize from bytes (e.g., MQTT payload).
231    ///
232    /// Returns `Err` with error message on parse failure.
233    fn from_bytes(data: &[u8]) -> Result<Self, String>;
234
235    /// Serialize to bytes (e.g., for MQTT payload).
236    ///
237    /// Returns `Err` with error message on serialization failure.
238    fn to_bytes(&self) -> Result<Vec<u8>, String>;
239}