Skip to main content

themql_core/
lib.rs

1//! # themql-core
2//!
3//! Canonical owner of theMQL's core type identities: `Message`, `Query`,
4//! `Response`, `Error`, `Context`, and `Resource` identity.
5//!
6//! Every other crate in theMQL is subordinate to this one for type
7//! definitions. See `specs/core.toml` for the authoritative specification.
8//!
9//! ## Authority
10//!
11//! Per `SPEC.toml` and `prompts/SYSTEM.md`, the canonical types here are
12//! the semantic owners. Transport adapters (`themql-mqtt`,
13//! `themql-graphql`, `themql-sse`) project these types; they do not
14//! redefine them.
15//!
16//! ## Example
17//!
18//! ```
19//! use themql_core::{Message, MessageId, Subject, Operation, Payload,
20//!                    Metadata, Timestamp, TimestampKind};
21//!
22//! let subject = Subject::from_str("vehicle.sensors.imu.gyro")
23//!     .expect("valid subject");
24//! let msg = Message {
25//!     id: MessageId::new(),
26//!     timestamp: Timestamp::now_monotonic(),
27//!     subject,
28//!     operation: Operation::Telemetry,
29//!     payload: Payload::Unit,
30//!     metadata: Metadata::default(),
31//! };
32//! assert_eq!(msg.operation, Operation::Telemetry);
33//! ```
34
35#![forbid(unsafe_code)]
36#![warn(clippy::pedantic)]
37#![warn(missing_docs)]
38#![allow(clippy::module_name_repetitions)]
39
40use std::collections::BTreeMap;
41use std::fmt;
42use std::future::Future;
43use std::pin::Pin;
44
45use serde::{Deserialize, Serialize};
46use uuid::Uuid;
47
48// ===========================================================================
49// Identity types
50// ===========================================================================
51
52/// Unique identifier for a `Message`. Wraps a UUID v7 (monotonic-ish).
53///
54/// Generated via [`MessageId::new`]; equality and ordering are by the
55/// inner UUID. Serialises transparently as the inner UUID string.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
57#[serde(transparent)]
58pub struct MessageId(pub Uuid);
59
60impl MessageId {
61    /// Generate a fresh `MessageId` (UUID v7 — time-ordered).
62    #[must_use]
63    pub fn new() -> Self {
64        Self(Uuid::now_v7())
65    }
66
67    /// The inner UUID.
68    #[must_use]
69    pub fn as_uuid(&self) -> Uuid {
70        self.0
71    }
72}
73
74impl Default for MessageId {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80impl fmt::Display for MessageId {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "{}", self.0)
83    }
84}
85
86/// Links a `Response` (or error) back to the `Query`/`Command` that
87/// caused it. Required on Responses; optional on commands/events.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(transparent)]
90pub struct CorrelationId(pub Uuid);
91
92impl CorrelationId {
93    /// Derive a correlation id from the originating request's id.
94    #[must_use]
95    pub fn for_request(request: &MessageId) -> Self {
96        // Use the same UUID — a response correlates 1:1 with its request.
97        Self(request.0)
98    }
99
100    /// Generate a fresh correlation id.
101    #[must_use]
102    pub fn new() -> Self {
103        Self(Uuid::now_v7())
104    }
105}
106
107impl Default for CorrelationId {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113/// Links an `Event` back to the `Command` that caused it. Required on
114/// Events; optional elsewhere.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
116#[serde(transparent)]
117pub struct CausationId(pub Uuid);
118
119impl CausationId {
120    /// Generate a fresh causation id.
121    #[must_use]
122    pub fn new() -> Self {
123        Self(Uuid::now_v7())
124    }
125}
126
127impl Default for CausationId {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133/// Trace identifier for distributed tracing.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
135#[serde(transparent)]
136pub struct TraceId(pub Uuid);
137
138impl TraceId {
139    /// Generate a fresh trace id.
140    #[must_use]
141    pub fn new() -> Self {
142        Self(Uuid::now_v7())
143    }
144}
145
146impl Default for TraceId {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152// ===========================================================================
153// Subject — hierarchical routing key. Grammar lives in message.toml; the
154// type is defined here because every transport depends on it.
155// ===========================================================================
156
157/// A single non-empty segment of a `Subject`.
158///
159/// Segments are case-sensitive, may contain `a-zA-Z0-9_-`, and must be
160/// non-empty. The separator `.` is between segments, never within.
161#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
162#[serde(transparent)]
163pub struct SubjectSegment(pub String);
164
165impl SubjectSegment {
166    /// Construct a segment, validating charset + non-empty.
167    ///
168    /// # Errors
169    /// Returns `SubjectError::EmptySegment` if the string is empty, or
170    /// `SubjectError::InvalidCharset` if it contains characters outside
171    /// `a-zA-Z0-9_-`.
172    pub fn new(s: &str) -> Result<Self, SubjectError> {
173        if s.is_empty() {
174            return Err(SubjectError::EmptySegment);
175        }
176        if !s
177            .chars()
178            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
179        {
180            return Err(SubjectError::InvalidCharset {
181                segment: s.to_owned(),
182            });
183        }
184        Ok(Self(s.to_owned()))
185    }
186
187    /// The segment string.
188    #[must_use]
189    pub fn as_str(&self) -> &str {
190        &self.0
191    }
192}
193
194impl fmt::Display for SubjectSegment {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.write_str(&self.0)
197    }
198}
199
200/// A concrete, hierarchical, dot-separated routing key.
201///
202/// Example: `vehicle.sensors.imu.gyro`. Subjects are case-sensitive,
203/// have no leading/trailing/consecutive separators, and never contain
204/// wildcards (`+`/`#` — see [`SubjectPattern`]).
205#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
206pub struct Subject {
207    segments: Vec<SubjectSegment>,
208}
209
210impl Subject {
211    /// Construct a `Subject` from segments.
212    ///
213    /// # Errors
214    /// Returns `SubjectError::EmptySubject` if `segments` is empty.
215    pub fn from_segments(segments: Vec<SubjectSegment>) -> Result<Self, SubjectError> {
216        if segments.is_empty() {
217            return Err(SubjectError::EmptySubject);
218        }
219        Ok(Self { segments })
220    }
221
222    /// Parse a `Subject` from a dot-joined string.
223    ///
224    /// # Errors
225    /// Returns a [`SubjectError`] variant for any grammar violation
226    /// (empty segment, invalid charset, leading/trailing/consecutive
227    /// separator, wildcard in concrete subject, empty subject).
228    #[allow(clippy::should_implement_trait)]
229    pub fn from_str(s: &str) -> Result<Self, SubjectError> {
230        if s.is_empty() {
231            return Err(SubjectError::EmptySubject);
232        }
233        if s.starts_with('.') {
234            return Err(SubjectError::LeadingSeparator);
235        }
236        if s.ends_with('.') {
237            return Err(SubjectError::TrailingSeparator);
238        }
239        if s.contains("..") {
240            return Err(SubjectError::ConsecutiveSeparators);
241        }
242        let segments: Vec<SubjectSegment> = s
243            .split('.')
244            .map(|part| {
245                // Wildcards are not valid in concrete Subjects — check before
246                // charset validation so we report the right error variant.
247                if part == "+" || part == "#" {
248                    return Err(SubjectError::WildcardInConcreteSubject);
249                }
250                SubjectSegment::new(part)
251            })
252            .collect::<Result<_, _>>()?;
253        Ok(Self { segments })
254    }
255
256    /// The segments of this subject.
257    #[must_use]
258    pub fn segments(&self) -> &[SubjectSegment] {
259        &self.segments
260    }
261
262    /// Dot-joined string form.
263    #[must_use]
264    pub fn as_str(&self) -> String {
265        self.segments
266            .iter()
267            .map(SubjectSegment::as_str)
268            .collect::<Vec<_>>()
269            .join(".")
270    }
271
272    /// Always `true` for `Subject` (only patterns can be non-concrete).
273    #[must_use]
274    pub fn is_concrete(&self) -> bool {
275        true
276    }
277}
278
279impl fmt::Display for Subject {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        f.write_str(&self.as_str())
282    }
283}
284
285/// A single segment of a [`SubjectPattern`].
286#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
287pub enum PatternSegment {
288    /// Matches one concrete segment by exact string equality.
289    Concrete(SubjectSegment),
290    /// `+` — matches any single concrete segment.
291    WildcardOne,
292    /// `#` — matches zero or more trailing concrete segments. Must be
293    /// the LAST segment of a pattern.
294    WildcardMulti,
295}
296
297/// A `Subject` that may contain wildcards. Used for subscriptions and
298/// cache invalidation patterns, never as a concrete `Message.subject`.
299#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
300pub struct SubjectPattern {
301    segments: Vec<PatternSegment>,
302}
303
304impl SubjectPattern {
305    /// Parse a `SubjectPattern` from a dot-joined string.
306    ///
307    /// # Errors
308    /// Returns `SubjectError::WildcardMultiNotLast` if `#` appears
309    /// before the last segment. Other grammar errors mirror
310    /// [`Subject::from_str`].
311    #[allow(clippy::should_implement_trait)]
312    pub fn from_str(s: &str) -> Result<Self, SubjectError> {
313        if s.is_empty() {
314            return Err(SubjectError::EmptySubject);
315        }
316        if s.starts_with('.') {
317            return Err(SubjectError::LeadingSeparator);
318        }
319        if s.ends_with('.') {
320            return Err(SubjectError::TrailingSeparator);
321        }
322        if s.contains("..") {
323            return Err(SubjectError::ConsecutiveSeparators);
324        }
325        let parts: Vec<&str> = s.split('.').collect();
326        let mut segments = Vec::with_capacity(parts.len());
327        for (i, part) in parts.iter().enumerate() {
328            let seg = match *part {
329                "+" => PatternSegment::WildcardOne,
330                "#" => {
331                    if i != parts.len() - 1 {
332                        return Err(SubjectError::WildcardMultiNotLast);
333                    }
334                    PatternSegment::WildcardMulti
335                }
336                other => PatternSegment::Concrete(SubjectSegment::new(other)?),
337            };
338            segments.push(seg);
339        }
340        Ok(Self { segments })
341    }
342
343    /// Test whether this pattern matches a concrete subject.
344    ///
345    /// - `Concrete` matches by exact string equality;
346    /// - `WildcardOne` (`+`) matches any single segment;
347    /// - `WildcardMulti` (`#`) matches zero or more trailing segments.
348    #[must_use]
349    pub fn matches(&self, subject: &Subject) -> bool {
350        let concrete = subject.segments();
351        let pattern = &self.segments;
352        let mut ci = 0;
353        let mut pi = 0;
354        while pi < pattern.len() && ci < concrete.len() {
355            match &pattern[pi] {
356                PatternSegment::Concrete(seg) => {
357                    if seg != &concrete[ci] {
358                        return false;
359                    }
360                    ci += 1;
361                    pi += 1;
362                }
363                PatternSegment::WildcardOne => {
364                    ci += 1;
365                    pi += 1;
366                }
367                PatternSegment::WildcardMulti => {
368                    // '#' matches the rest; only valid as last pattern segment.
369                    return pi == pattern.len() - 1;
370                }
371            }
372        }
373        // Trailing '#' matches zero-or-more, so a pattern ending in '#' can
374        // match even if the concrete subject is shorter.
375        if pi < pattern.len() && matches!(pattern[pi], PatternSegment::WildcardMulti) {
376            return true;
377        }
378        pi == pattern.len() && ci == concrete.len()
379    }
380
381    /// Dot-joined string form.
382    #[must_use]
383    pub fn as_str(&self) -> String {
384        self.segments
385            .iter()
386            .map(|s| match s {
387                PatternSegment::Concrete(seg) => seg.as_str().to_owned(),
388                PatternSegment::WildcardOne => "+".to_owned(),
389                PatternSegment::WildcardMulti => "#".to_owned(),
390            })
391            .collect::<Vec<_>>()
392            .join(".")
393    }
394
395    /// The pattern segments.
396    #[must_use]
397    pub fn segments(&self) -> &[PatternSegment] {
398        &self.segments
399    }
400}
401
402impl fmt::Display for SubjectPattern {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        f.write_str(&self.as_str())
405    }
406}
407
408/// Errors raised while constructing or parsing [`Subject`] /
409/// [`SubjectPattern`].
410#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
411pub enum SubjectError {
412    /// A segment was empty.
413    #[error("empty segment")]
414    EmptySegment,
415    /// A segment contained characters outside `a-zA-Z0-9_-`.
416    #[error("invalid charset in segment: {segment}")]
417    InvalidCharset {
418        /// The offending segment string.
419        segment: String,
420    },
421    /// A segment contained a `.` (separator belongs between segments).
422    #[error("separator inside segment")]
423    SeparatorInSegment,
424    /// The subject had a trailing `.`.
425    #[error("trailing separator")]
426    TrailingSeparator,
427    /// The subject had a leading `.`.
428    #[error("leading separator")]
429    LeadingSeparator,
430    /// The subject had two consecutive `.` separators.
431    #[error("consecutive separators")]
432    ConsecutiveSeparators,
433    /// A concrete subject contained `+` or `#`.
434    #[error("wildcard in concrete subject")]
435    WildcardInConcreteSubject,
436    /// A pattern had `#` before the last segment.
437    #[error("wildcard '#' must be the last segment")]
438    WildcardMultiNotLast,
439    /// The subject was the empty string.
440    #[error("empty subject")]
441    EmptySubject,
442}
443
444// ===========================================================================
445// Operation — the verb of a Message
446// ===========================================================================
447
448/// The verb of a `Message`. Tags payload semantics.
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
450#[serde(rename_all = "snake_case")]
451pub enum Operation {
452    /// A read-only query against a resource.
453    Query,
454    /// A side-effectful command.
455    Command,
456    /// An event that has occurred.
457    Event,
458    /// A long-lived stream subscription.
459    Stream,
460    /// The response to a query or command.
461    Response,
462    /// A telemetry reading.
463    Telemetry,
464    /// An error.
465    Error,
466}
467
468impl fmt::Display for Operation {
469    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470        let s = match self {
471            Self::Query => "query",
472            Self::Command => "command",
473            Self::Event => "event",
474            Self::Stream => "stream",
475            Self::Response => "response",
476            Self::Telemetry => "telemetry",
477            Self::Error => "error",
478        };
479        f.write_str(s)
480    }
481}
482
483// ===========================================================================
484// Payload — typed, serde-serialisable, format-tagged
485// ===========================================================================
486
487/// Tag identifying the serialisation format of a payload.
488#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
489#[serde(rename_all = "lowercase")]
490pub enum FormatTag {
491    /// JSON.
492    Json,
493    /// Bincode.
494    Bincode,
495    /// Postcard (embedded-friendly).
496    Postcard,
497    /// A custom format identified by name. Must be explicit in
498    /// `Message.metadata`.
499    Custom(String),
500}
501
502impl fmt::Display for FormatTag {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        match self {
505            Self::Json => f.write_str("json"),
506            Self::Bincode => f.write_str("bincode"),
507            Self::Postcard => f.write_str("postcard"),
508            Self::Custom(name) => write!(f, "custom:{name}"),
509        }
510    }
511}
512
513/// Explicitly typed, serde-serialisable payload of a `Message`. The
514/// format is tagged in the payload itself (never inferred from
515/// transport).
516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
517pub enum Payload {
518    /// JSON payload.
519    Json(serde_json::Value),
520    /// Binary payload with an explicit format tag.
521    Bytes(Vec<u8>, FormatTag),
522    /// No payload body (e.g. heartbeat).
523    Unit,
524}
525
526// ===========================================================================
527// Metadata — key-value bag
528// ===========================================================================
529
530/// Auth principal carried in `Metadata`.
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532pub struct Principal {
533    /// Principal identifier (e.g. user id, service id).
534    pub id: String,
535    /// Scopes granted to this principal.
536    pub scopes: Vec<String>,
537}
538
539/// Key-value bag carried on every `Message`. Holds correlation id,
540/// causation id, trace id, auth principal, format hint, and arbitrary
541/// extension keys.
542#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
543pub struct Metadata {
544    /// Correlation id — links responses to requests.
545    pub correlation_id: Option<CorrelationId>,
546    /// Causation id — links events to the command that caused them.
547    pub causation_id: Option<CausationId>,
548    /// Trace id for distributed tracing.
549    pub trace_id: Option<TraceId>,
550    /// Auth principal.
551    pub auth_principal: Option<Principal>,
552    /// Optional explicit format hint.
553    pub format: Option<FormatTag>,
554    /// Extension keys. `BTreeMap` for deterministic iteration and hash.
555    pub extensions: BTreeMap<String, serde_json::Value>,
556}
557
558impl Metadata {
559    /// Construct a `Metadata` with no named fields set and no extensions.
560    #[must_use]
561    pub fn new() -> Self {
562        Self::default()
563    }
564
565    /// Set the correlation id.
566    #[must_use]
567    pub fn with_correlation(mut self, id: CorrelationId) -> Self {
568        self.correlation_id = Some(id);
569        self
570    }
571
572    /// Set the causation id.
573    #[must_use]
574    pub fn with_causation(mut self, id: CausationId) -> Self {
575        self.causation_id = Some(id);
576        self
577    }
578
579    /// Set the trace id.
580    #[must_use]
581    pub fn with_trace(mut self, id: TraceId) -> Self {
582        self.trace_id = Some(id);
583        self
584    }
585
586    /// Add an extension key-value pair. Extensions must not duplicate
587    /// the named fields.
588    #[must_use]
589    pub fn with_extension<K: Into<String>, V: Into<serde_json::Value>>(
590        mut self,
591        key: K,
592        value: V,
593    ) -> Self {
594        self.extensions.insert(key.into(), value.into());
595        self
596    }
597}
598
599// ===========================================================================
600// Timestamp + Deadline
601// ===========================================================================
602
603/// Kind of timestamp — monotonic vs wall-clock.
604#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
605#[serde(rename_all = "snake_case")]
606pub enum TimestampKind {
607    /// Monotonic — safe for ordering, not for cross-process correlation.
608    Monotonic,
609    /// Wall-clock — safe for cross-process correlation, subject to NTP.
610    WallClock,
611}
612
613/// Timestamp on every `Message`. Tagged explicitly so consumers know
614/// whether to trust it for ordering.
615#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
616pub struct Timestamp {
617    /// Whether this is monotonic or wall-clock.
618    pub kind: TimestampKind,
619    /// Nanoseconds since the Unix epoch (wall) or since process start (mono).
620    pub nanos_since_epoch: i64,
621}
622
623impl Timestamp {
624    /// Construct a timestamp from nanoseconds + kind.
625    #[must_use]
626    pub fn new(kind: TimestampKind, nanos_since_epoch: i64) -> Self {
627        Self {
628            kind,
629            nanos_since_epoch,
630        }
631    }
632
633    /// Current monotonic timestamp (best-effort).
634    #[must_use]
635    pub fn now_monotonic() -> Self {
636        Self::new(TimestampKind::Monotonic, 0)
637    }
638
639    /// Current wall-clock timestamp (best-effort).
640    #[must_use]
641    pub fn now_wall_clock() -> Self {
642        Self::new(TimestampKind::WallClock, 0)
643    }
644}
645
646/// Absolute deadline for a `Query` resolution or `Command` dispatch.
647/// Carried in `Context`. Resolvers must check the deadline before
648/// returning a result and return [`Error`] with code
649/// [`ErrorCode::Timeout`] if exceeded.
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
651pub struct Deadline {
652    /// Whether this is monotonic or wall-clock.
653    pub kind: TimestampKind,
654    /// Absolute nanoseconds since epoch (or process start) at which the
655    /// deadline expires.
656    pub nanos_since_epoch: i64,
657}
658
659impl Deadline {
660    /// Construct a deadline.
661    #[must_use]
662    pub fn new(kind: TimestampKind, nanos_since_epoch: i64) -> Self {
663        Self {
664            kind,
665            nanos_since_epoch,
666        }
667    }
668
669    /// Whether the deadline has expired relative to `now`.
670    #[must_use]
671    pub fn is_expired(&self, now: &Timestamp) -> bool {
672        self.kind == now.kind && self.nanos_since_epoch <= now.nanos_since_epoch
673    }
674}
675
676// ===========================================================================
677// Selection / Projection — filter + projection combined (per decision)
678// ===========================================================================
679
680/// A dot-joined path into a resource (e.g. `position.x`).
681#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
682pub struct FieldPath {
683    segments: Vec<String>,
684}
685
686impl FieldPath {
687    /// Construct a field path from segments.
688    #[must_use]
689    pub fn from_segments(segments: Vec<String>) -> Self {
690        Self { segments }
691    }
692
693    /// Parse a field path from a dot-joined string.
694    ///
695    /// # Errors
696    /// Returns [`SubjectError::EmptySegment`] if any segment is empty.
697    #[allow(clippy::should_implement_trait)]
698    pub fn from_str(s: &str) -> Result<Self, SubjectError> {
699        if s.is_empty() {
700            return Err(SubjectError::EmptySegment);
701        }
702        let segments: Vec<String> = s.split('.').map(str::to_owned).collect();
703        if segments.iter().any(String::is_empty) {
704            return Err(SubjectError::EmptySegment);
705        }
706        Ok(Self { segments })
707    }
708
709    /// The path segments.
710    #[must_use]
711    pub fn segments(&self) -> &[String] {
712        &self.segments
713    }
714
715    /// Dot-joined string form.
716    #[must_use]
717    pub fn as_str(&self) -> String {
718        self.segments.join(".")
719    }
720}
721
722impl fmt::Display for FieldPath {
723    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724        f.write_str(&self.as_str())
725    }
726}
727
728/// Comparison operator for a selection predicate.
729#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
730#[serde(rename_all = "snake_case")]
731pub enum Comparison {
732    /// Equal.
733    Eq,
734    /// Not equal.
735    Ne,
736    /// Less than.
737    Lt,
738    /// Less than or equal.
739    Le,
740    /// Greater than.
741    Gt,
742    /// Greater than or equal.
743    Ge,
744    /// In a set.
745    In,
746    /// Contains a value.
747    Contains,
748}
749
750/// A boolean predicate tree over resource fields.
751#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
752pub enum Predicate {
753    /// A field comparison: `path op value`.
754    Field {
755        /// Path to the field being compared.
756        path: FieldPath,
757        /// Comparison operator.
758        op: Comparison,
759        /// Value to compare against.
760        value: serde_json::Value,
761    },
762    /// Conjunction (AND) of sub-predicates.
763    And(Vec<Predicate>),
764    /// Disjunction (OR) of sub-predicates.
765    Or(Vec<Predicate>),
766    /// Negation (NOT) of a sub-predicate.
767    Not(Box<Predicate>),
768}
769
770/// Optional filter predicate selecting a subset of a resource. Combined
771/// with [`Projection`], this forms the 'filter + projection' shape: a
772/// `Query` with `selection = All` returns all entities of the resource;
773/// `selection = Filter(p)` narrows which entities are returned.
774#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
775pub enum Selection {
776    /// Return all entities of the resource.
777    #[default]
778    All,
779    /// Filter entities by a predicate.
780    Filter(Predicate),
781}
782
783impl Selection {
784    /// Construct an 'all' selection.
785    #[must_use]
786    pub fn all() -> Self {
787        Self::All
788    }
789
790    /// Construct a filtered selection.
791    #[must_use]
792    pub fn filter(p: Predicate) -> Self {
793        Self::Filter(p)
794    }
795}
796
797/// Optional field projection narrowing which fields of each selected
798/// entity appear in the `Response`. Combined with [`Selection`], this
799/// completes the 'filter + projection' shape.
800///
801/// `include` and `exclude` are mutually exclusive — if both non-empty,
802/// the resolver must return [`ErrorCode::ValidationError`]. A projection
803/// with both empty is treated the same as `None` (return all fields).
804#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
805pub struct Projection {
806    /// Fields to include. Mutually exclusive with `exclude`.
807    pub include: Vec<FieldPath>,
808    /// Fields to exclude. Mutually exclusive with `include`.
809    pub exclude: Vec<FieldPath>,
810}
811
812impl Projection {
813    /// Construct an empty projection (returns all fields).
814    #[must_use]
815    pub fn new() -> Self {
816        Self::default()
817    }
818
819    /// Add an include path.
820    #[must_use]
821    pub fn include(mut self, path: FieldPath) -> Self {
822        self.include.push(path);
823        self
824    }
825
826    /// Add an exclude path.
827    #[must_use]
828    pub fn exclude(mut self, path: FieldPath) -> Self {
829        self.exclude.push(path);
830        self
831    }
832
833    /// Whether this projection is valid (include and exclude are not
834    /// both non-empty).
835    #[must_use]
836    pub fn is_valid(&self) -> bool {
837        self.include.is_empty() || self.exclude.is_empty()
838    }
839}
840
841// ===========================================================================
842// Resource — named, addressable entity
843// ===========================================================================
844
845/// A named, addressable entity in theMQL. Every `Query` targets a
846/// `Resource`. Resources have a hierarchical identity (a `Subject`).
847#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
848pub struct Resource {
849    /// The hierarchical subject identifying this resource.
850    pub subject: Subject,
851}
852
853impl Resource {
854    /// Construct a resource from a subject.
855    #[must_use]
856    pub fn from_subject(subject: Subject) -> Self {
857        Self { subject }
858    }
859
860    /// Parse a resource from a dot-joined subject string.
861    ///
862    /// # Errors
863    /// Returns [`SubjectError`] if the string is not a valid subject.
864    #[allow(clippy::should_implement_trait)]
865    pub fn from_str(s: &str) -> Result<Self, SubjectError> {
866        Ok(Self {
867            subject: Subject::from_str(s)?,
868        })
869    }
870}
871
872impl fmt::Display for Resource {
873    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
874        write!(f, "{}", self.subject)
875    }
876}
877
878// ===========================================================================
879// CachePolicy — canonical in core (referenced by query, cache)
880// ===========================================================================
881
882/// Tier of the cache stack.
883#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
884pub enum CacheTier {
885    /// L1 — cachelito, process-local, ultra-low latency.
886    L1,
887    /// L2 — moka, process-local, low latency.
888    L2,
889    /// L3 — valkey, distributed, network latency.
890    L3,
891    /// L4 — helix-db, authoritative, persistent.
892    L4,
893}
894
895/// Hint to the cache about when to invalidate an entry.
896#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
897pub enum InvalidationHint {
898    /// Invalidate when a message on this subject pattern is published.
899    OnSubjectChange(SubjectPattern),
900    /// Invalidate after this TTL.
901    AfterTtl(std::time::Duration),
902    /// Invalidate only via explicit `Cache::invalidate`.
903    Explicit,
904}
905
906/// Policy controlling how a `Query` result is cached. Carried in
907/// [`Context`] and optionally overridden on `Query.cache_policy`.
908#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
909pub struct CachePolicy {
910    /// If `false`, do not read from or write to any cache tier.
911    pub enabled: bool,
912    /// Explicit time-to-live; `None` means 'defer to tier default'.
913    pub ttl: Option<std::time::Duration>,
914    /// Preferred tier for this query; `None` means 'tier decides'.
915    pub tier_hint: Option<CacheTier>,
916    /// If `true`, skip cache read but still write-through on miss.
917    pub bypass: bool,
918    /// Optional hint to the cache about when to invalidate.
919    pub invalidation_hint: Option<InvalidationHint>,
920}
921
922impl CachePolicy {
923    /// Construct a default policy: enabled, no TTL, no tier hint, no
924    /// bypass, no invalidation hint.
925    #[must_use]
926    pub fn new() -> Self {
927        Self::default()
928    }
929
930    /// Disable all caching for this query.
931    #[must_use]
932    pub fn disabled() -> Self {
933        Self {
934            enabled: false,
935            ttl: None,
936            tier_hint: None,
937            bypass: false,
938            invalidation_hint: None,
939        }
940    }
941
942    /// Set the TTL.
943    #[must_use]
944    pub fn with_ttl(mut self, ttl: std::time::Duration) -> Self {
945        self.ttl = Some(ttl);
946        self
947    }
948
949    /// Set the tier hint.
950    #[must_use]
951    pub fn with_tier(mut self, tier: CacheTier) -> Self {
952        self.tier_hint = Some(tier);
953        self
954    }
955
956    /// Set the bypass flag.
957    #[must_use]
958    pub fn with_bypass(mut self, bypass: bool) -> Self {
959        self.bypass = bypass;
960        self
961    }
962}
963
964impl Default for CachePolicy {
965    fn default() -> Self {
966        Self {
967            enabled: true,
968            ttl: None,
969            tier_hint: None,
970            bypass: false,
971            invalidation_hint: None,
972        }
973    }
974}
975
976// ===========================================================================
977// Cancellation
978// ===========================================================================
979
980/// Cancellation signal propagated through the resolution chain. A
981/// resolver must check cancellation before returning a result and
982/// return [`ErrorCode::Timeout`] if cancelled.
983///
984/// Phase 1 uses a simple bool flag. Phase 2 may upgrade to a token type
985/// backed by the runtime (tokio `CancellationToken` or embassy signal).
986/// The public API (`is_cancelled` / `cancel`) stays stable.
987#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
988pub struct Cancellation {
989    cancelled: bool,
990}
991
992impl Cancellation {
993    /// Construct a non-cancelled cancellation signal.
994    #[must_use]
995    pub fn new() -> Self {
996        Self { cancelled: false }
997    }
998
999    /// Whether this signal has been cancelled.
1000    #[must_use]
1001    pub fn is_cancelled(&self) -> bool {
1002        self.cancelled
1003    }
1004
1005    /// Cancel this signal.
1006    pub fn cancel(&mut self) {
1007        self.cancelled = true;
1008    }
1009}
1010
1011// ===========================================================================
1012// Context — execution context
1013// ===========================================================================
1014
1015/// Execution context propagated through every query resolution and
1016/// command dispatch. Carries deadline, auth principal, trace id,
1017/// cache policy, and cancellation signal.
1018#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
1019pub struct Context {
1020    /// Absolute deadline for the resolution.
1021    pub deadline: Option<Deadline>,
1022    /// Auth principal.
1023    pub principal: Option<Principal>,
1024    /// Trace id.
1025    pub trace_id: Option<TraceId>,
1026    /// Cache policy.
1027    pub cache_policy: CachePolicy,
1028    /// Cancellation signal.
1029    pub cancellation: Cancellation,
1030}
1031
1032impl Context {
1033    /// Construct a context with default cache policy and no deadline.
1034    #[must_use]
1035    pub fn new() -> Self {
1036        Self::default()
1037    }
1038
1039    /// Set the deadline.
1040    #[must_use]
1041    pub fn with_deadline(mut self, d: Deadline) -> Self {
1042        self.deadline = Some(d);
1043        self
1044    }
1045
1046    /// Set the principal.
1047    #[must_use]
1048    pub fn with_principal(mut self, p: Principal) -> Self {
1049        self.principal = Some(p);
1050        self
1051    }
1052
1053    /// Set the trace id.
1054    #[must_use]
1055    pub fn with_trace(mut self, t: TraceId) -> Self {
1056        self.trace_id = Some(t);
1057        self
1058    }
1059
1060    /// Set the cache policy.
1061    #[must_use]
1062    pub fn with_cache_policy(mut self, p: CachePolicy) -> Self {
1063        self.cache_policy = p;
1064        self
1065    }
1066}
1067
1068// ===========================================================================
1069// Response — Result<ResponseValue, Error>, never both, never neither
1070// ===========================================================================
1071
1072/// The value carried by a successful [`Response`]. Mirrors [`Payload`]
1073/// but is a distinct type so transports can project independently.
1074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1075pub enum ResponseValue {
1076    /// JSON value.
1077    Json(serde_json::Value),
1078    /// Binary value with format tag.
1079    Bytes(Vec<u8>, FormatTag),
1080    /// No body.
1081    Unit,
1082}
1083
1084/// The result of a `Query` or `Command`. Carried inside a `Message`
1085/// with `operation = Response`. A `Response` contains either a value or
1086/// an `Error`; never both, never neither.
1087#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1088pub struct Response {
1089    /// The result. Either a value or an error.
1090    pub result: Result<ResponseValue, Error>,
1091    /// Correlation id — required on responses.
1092    pub correlation_id: CorrelationId,
1093}
1094
1095impl Response {
1096    /// Construct a successful response.
1097    #[must_use]
1098    pub fn ok(value: ResponseValue, correlation_id: CorrelationId) -> Self {
1099        Self {
1100            result: Ok(value),
1101            correlation_id,
1102        }
1103    }
1104
1105    /// Construct an error response.
1106    #[must_use]
1107    pub fn err(error: Error, correlation_id: CorrelationId) -> Self {
1108        Self {
1109            result: Err(error),
1110            correlation_id,
1111        }
1112    }
1113
1114    /// Whether this response is a success.
1115    #[must_use]
1116    pub fn is_ok(&self) -> bool {
1117        self.result.is_ok()
1118    }
1119
1120    /// Whether this response is an error.
1121    #[must_use]
1122    pub fn is_err(&self) -> bool {
1123        self.result.is_err()
1124    }
1125}
1126
1127// ===========================================================================
1128// Error + ErrorCode
1129// ===========================================================================
1130
1131/// Canonical error code. Transport adapters project this into
1132/// GraphQL errors, MQTT error topics, SSE error frames, etc.
1133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1134#[serde(rename_all = "snake_case")]
1135pub enum ErrorCode {
1136    /// Transport-level failure.
1137    TransportError,
1138    /// Resolver could not fulfil the query.
1139    ResolverError,
1140    /// Authoritative miss and no resolver.
1141    CacheMiss,
1142    /// Input failed validation.
1143    ValidationError,
1144    /// Deadline exceeded.
1145    Timeout,
1146    /// Unexpected internal failure.
1147    InternalError,
1148}
1149
1150impl fmt::Display for ErrorCode {
1151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1152        let s = match self {
1153            Self::TransportError => "transport_error",
1154            Self::ResolverError => "resolver_error",
1155            Self::CacheMiss => "cache_miss",
1156            Self::ValidationError => "validation_error",
1157            Self::Timeout => "timeout",
1158            Self::InternalError => "internal_error",
1159        };
1160        f.write_str(s)
1161    }
1162}
1163
1164/// The canonical error type. Transport adapters project this into
1165/// GraphQL errors, MQTT error topics, SSE error frames, etc. Transport
1166/// adapters must not define their own error types.
1167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1168pub struct Error {
1169    /// Canonical error code.
1170    pub code: ErrorCode,
1171    /// Human-readable error message.
1172    pub message: String,
1173    /// Subject the error relates to.
1174    pub subject: Option<Subject>,
1175    /// Correlation id.
1176    pub correlation_id: Option<CorrelationId>,
1177    /// Causation id.
1178    pub causation_id: Option<CausationId>,
1179    /// Optional structured details.
1180    pub details: Option<serde_json::Value>,
1181}
1182
1183impl Error {
1184    /// Construct an error with a code and message.
1185    #[must_use]
1186    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
1187        Self {
1188            code,
1189            message: message.into(),
1190            subject: None,
1191            correlation_id: None,
1192            causation_id: None,
1193            details: None,
1194        }
1195    }
1196
1197    /// Construct a transport error.
1198    #[must_use]
1199    pub fn transport_error(msg: impl Into<String>) -> Self {
1200        Self::new(ErrorCode::TransportError, msg)
1201    }
1202
1203    /// Construct a resolver error.
1204    #[must_use]
1205    pub fn resolver_error(msg: impl Into<String>) -> Self {
1206        Self::new(ErrorCode::ResolverError, msg)
1207    }
1208
1209    /// Construct a cache-miss error.
1210    #[must_use]
1211    pub fn cache_miss(msg: impl Into<String>) -> Self {
1212        Self::new(ErrorCode::CacheMiss, msg)
1213    }
1214
1215    /// Construct a validation error.
1216    #[must_use]
1217    pub fn validation_error(msg: impl Into<String>) -> Self {
1218        Self::new(ErrorCode::ValidationError, msg)
1219    }
1220
1221    /// Construct a timeout error.
1222    #[must_use]
1223    pub fn timeout(msg: impl Into<String>) -> Self {
1224        Self::new(ErrorCode::Timeout, msg)
1225    }
1226
1227    /// Construct an internal error.
1228    #[must_use]
1229    pub fn internal_error(msg: impl Into<String>) -> Self {
1230        Self::new(ErrorCode::InternalError, msg)
1231    }
1232
1233    /// Set the subject.
1234    #[must_use]
1235    pub fn with_subject(mut self, s: Subject) -> Self {
1236        self.subject = Some(s);
1237        self
1238    }
1239
1240    /// Set the correlation id.
1241    #[must_use]
1242    pub fn with_correlation(mut self, id: CorrelationId) -> Self {
1243        self.correlation_id = Some(id);
1244        self
1245    }
1246
1247    /// Set the causation id.
1248    #[must_use]
1249    pub fn with_causation(mut self, id: CausationId) -> Self {
1250        self.causation_id = Some(id);
1251        self
1252    }
1253
1254    /// Set structured details.
1255    #[must_use]
1256    pub fn with_details(mut self, details: serde_json::Value) -> Self {
1257        self.details = Some(details);
1258        self
1259    }
1260}
1261
1262impl fmt::Display for Error {
1263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1264        write!(f, "{}: {}", self.code, self.message)
1265    }
1266}
1267
1268impl std::error::Error for Error {}
1269
1270impl From<SubjectError> for Error {
1271    fn from(e: SubjectError) -> Self {
1272        Self::validation_error(e.to_string())
1273    }
1274}
1275
1276// ===========================================================================
1277// Query — read-only, side-effect-free request
1278// ===========================================================================
1279
1280/// A read-only, side-effect-free request against a named `Resource`.
1281/// A `Query` is always carried inside a `Message` with
1282/// `operation = Query`.
1283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1284pub struct Query {
1285    /// The named target.
1286    pub resource: Resource,
1287    /// Optional filter predicate selecting a subset of the resource.
1288    pub selection: Selection,
1289    /// Optional named arguments to the resolver.
1290    pub arguments: Option<serde_json::Value>,
1291    /// Optional field projection narrowing which fields are returned.
1292    pub projection: Option<Projection>,
1293    /// Execution context (deadline, principal, trace, cache policy).
1294    pub context: Context,
1295    /// Optional override of the cache policy in `Context`.
1296    pub cache_policy: Option<CachePolicy>,
1297    /// Optional override of the deadline in `Context`.
1298    pub deadline: Option<Deadline>,
1299}
1300
1301impl Query {
1302    /// Construct a minimal query against a resource with default
1303    /// selection (`All`) and default context.
1304    #[must_use]
1305    pub fn new(resource: Resource) -> Self {
1306        Self {
1307            resource,
1308            selection: Selection::All,
1309            arguments: None,
1310            projection: None,
1311            context: Context::default(),
1312            cache_policy: None,
1313            deadline: None,
1314        }
1315    }
1316
1317    /// Set the selection.
1318    #[must_use]
1319    pub fn with_selection(mut self, s: Selection) -> Self {
1320        self.selection = s;
1321        self
1322    }
1323
1324    /// Set the projection. Returns an error via the resolver if
1325    /// `include` and `exclude` are both non-empty.
1326    #[must_use]
1327    pub fn with_projection(mut self, p: Projection) -> Self {
1328        self.projection = Some(p);
1329        self
1330    }
1331
1332    /// Set the arguments.
1333    #[must_use]
1334    pub fn with_arguments(mut self, args: serde_json::Value) -> Self {
1335        self.arguments = Some(args);
1336        self
1337    }
1338}
1339
1340// ===========================================================================
1341// Message — the fundamental primitive
1342// ===========================================================================
1343
1344/// The fundamental primitive. Every request, command, event, stream,
1345/// response, telemetry reading, and error is carried by a `Message`.
1346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1347pub struct Message {
1348    /// Unique identifier.
1349    pub id: MessageId,
1350    /// Timestamp.
1351    pub timestamp: Timestamp,
1352    /// Hierarchical routing key.
1353    pub subject: Subject,
1354    /// The verb of this message.
1355    pub operation: Operation,
1356    /// Explicitly typed, serde-serialisable payload.
1357    pub payload: Payload,
1358    /// Metadata (correlation, causation, trace, auth, extensions).
1359    pub metadata: Metadata,
1360}
1361
1362impl Message {
1363    /// Construct a minimal message with the given subject, operation,
1364    /// and unit payload.
1365    ///
1366    /// # Errors
1367    /// Returns [`SubjectError`] if `subject_str` is not a valid subject.
1368    pub fn new(subject_str: &str, operation: Operation) -> Result<Self, SubjectError> {
1369        Ok(Self {
1370            id: MessageId::new(),
1371            timestamp: Timestamp::now_monotonic(),
1372            subject: Subject::from_str(subject_str)?,
1373            operation,
1374            payload: Payload::Unit,
1375            metadata: Metadata::default(),
1376        })
1377    }
1378}
1379
1380// ===========================================================================
1381// Public traits — Resolver, MessageHandler, QueryExecutor
1382// ===========================================================================
1383
1384/// Resolver for queries against a resource. Implementations are
1385/// domain-specific (storage, analysis, telemetry).
1386///
1387/// This trait is dyn-compatible: `resolve` returns a boxed future so
1388/// that transport adapters can hold `Arc<dyn Resolver>`.
1389pub trait Resolver: Send + Sync {
1390    /// Resolve a query against this resolver's resource.
1391    ///
1392    /// # Errors
1393    /// Returns [`Error`] if the query cannot be fulfilled.
1394    fn resolve<'a>(
1395        &'a self,
1396        query: &'a Query,
1397        ctx: &'a Context,
1398    ) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>>;
1399}
1400
1401/// Blanket adapter: any `Resolver` whose `resolve` returns a
1402/// `Send` future can be boxed into `dyn Resolver`.
1403pub trait ResolverBoxed: Send + Sync {
1404    /// Resolve a query against this resolver's resource.
1405    ///
1406    /// # Errors
1407    /// Returns [`Error`] if the query cannot be fulfilled.
1408    #[allow(async_fn_in_trait)]
1409    fn resolve(
1410        &self,
1411        query: &Query,
1412        ctx: &Context,
1413    ) -> impl Future<Output = Result<Response, Error>> + Send;
1414}
1415
1416impl<T> Resolver for T
1417where
1418    T: ResolverBoxed + ?Sized,
1419{
1420    fn resolve<'a>(
1421        &'a self,
1422        query: &'a Query,
1423        ctx: &'a Context,
1424    ) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>> {
1425        Box::pin(<T as ResolverBoxed>::resolve(self, query, ctx))
1426    }
1427}
1428
1429/// Handler for incoming messages.
1430///
1431/// Dyn-compatible: `handle` returns a boxed future so transport
1432/// adapters can hold `Arc<dyn MessageHandler>`.
1433pub trait MessageHandler: Send + Sync {
1434    /// Handle an incoming message.
1435    ///
1436    /// # Errors
1437    /// Returns [`Error`] if the message cannot be handled.
1438    fn handle<'a>(
1439        &'a self,
1440        msg: &'a Message,
1441    ) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>>;
1442}
1443
1444/// Orchestrator around [`Resolver`]: applies caching, batching, deadline
1445/// propagation, and cancellation checks. Declared here so transport
1446/// adapters can depend on the trait without a hard dep on themql-query;
1447/// implemented in themql-query.
1448#[allow(async_fn_in_trait)]
1449pub trait QueryExecutor {
1450    /// Execute a query with cross-cutting concerns applied.
1451    ///
1452    /// # Errors
1453    /// Returns [`Error`] if the query cannot be executed.
1454    fn execute(
1455        &self,
1456        query: &Query,
1457        ctx: &Context,
1458    ) -> impl Future<Output = Result<Response, Error>>;
1459}
1460
1461// ===========================================================================
1462// Tests
1463// ===========================================================================
1464
1465#[cfg(test)]
1466mod tests {
1467    use super::*;
1468
1469    // --- Identity ---------------------------------------------------------
1470
1471    #[test]
1472    fn message_id_is_unique() {
1473        let a = MessageId::new();
1474        let b = MessageId::new();
1475        assert_ne!(a, b, "two MessageId::new() must differ");
1476    }
1477
1478    #[test]
1479    fn correlation_id_derives_from_request() {
1480        let req = MessageId::new();
1481        let corr = CorrelationId::for_request(&req);
1482        assert_eq!(corr.0, req.0, "correlation must equal request id");
1483    }
1484
1485    // --- Subject parsing --------------------------------------------------
1486
1487    #[test]
1488    fn subject_parses_simple() {
1489        let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1490        assert_eq!(s.segments().len(), 4);
1491        assert_eq!(s.as_str(), "vehicle.sensors.imu.gyro");
1492        assert!(s.is_concrete());
1493    }
1494
1495    #[test]
1496    fn subject_rejects_empty() {
1497        assert!(matches!(
1498            Subject::from_str(""),
1499            Err(SubjectError::EmptySubject)
1500        ));
1501    }
1502
1503    #[test]
1504    fn subject_rejects_trailing_separator() {
1505        assert!(matches!(
1506            Subject::from_str("vehicle."),
1507            Err(SubjectError::TrailingSeparator)
1508        ));
1509    }
1510
1511    #[test]
1512    fn subject_rejects_leading_separator() {
1513        assert!(matches!(
1514            Subject::from_str(".vehicle"),
1515            Err(SubjectError::LeadingSeparator)
1516        ));
1517    }
1518
1519    #[test]
1520    fn subject_rejects_consecutive_separators() {
1521        assert!(matches!(
1522            Subject::from_str("vehicle..sensors"),
1523            Err(SubjectError::ConsecutiveSeparators)
1524        ));
1525    }
1526
1527    #[test]
1528    fn subject_rejects_wildcard_in_concrete() {
1529        assert!(matches!(
1530            Subject::from_str("vehicle.+.sensors"),
1531            Err(SubjectError::WildcardInConcreteSubject)
1532        ));
1533    }
1534
1535    #[test]
1536    fn subject_segment_rejects_invalid_charset() {
1537        assert!(matches!(
1538            SubjectSegment::new("bad/segment"),
1539            Err(SubjectError::InvalidCharset { .. })
1540        ));
1541    }
1542
1543    // --- SubjectPattern matching -----------------------------------------
1544
1545    #[test]
1546    fn pattern_matches_exact() {
1547        let p = SubjectPattern::from_str("vehicle.sensors.imu.gyro").unwrap();
1548        let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1549        assert!(p.matches(&s));
1550    }
1551
1552    #[test]
1553    fn pattern_matches_wildcard_one() {
1554        let p = SubjectPattern::from_str("vehicle.+.imu.gyro").unwrap();
1555        let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1556        assert!(p.matches(&s), "+ must match one segment");
1557    }
1558
1559    #[test]
1560    fn pattern_does_not_match_wildcard_one_when_length_differs() {
1561        let p = SubjectPattern::from_str("vehicle.+.gyro").unwrap();
1562        let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1563        assert!(!p.matches(&s), "+ matches exactly one segment");
1564    }
1565
1566    #[test]
1567    fn pattern_matches_wildcard_multi_trailing() {
1568        let p = SubjectPattern::from_str("vehicle.#").unwrap();
1569        let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1570        assert!(p.matches(&s), "# must match trailing segments");
1571    }
1572
1573    #[test]
1574    fn pattern_wildcard_multi_matches_zero() {
1575        let p = SubjectPattern::from_str("vehicle.sensors.#").unwrap();
1576        let s = Subject::from_str("vehicle.sensors").unwrap();
1577        assert!(p.matches(&s), "# must match zero trailing segments");
1578    }
1579
1580    #[test]
1581    fn pattern_rejects_wildcard_multi_not_last() {
1582        assert!(matches!(
1583            SubjectPattern::from_str("vehicle.#.sensors"),
1584            Err(SubjectError::WildcardMultiNotLast)
1585        ));
1586    }
1587
1588    // --- Operation --------------------------------------------------------
1589
1590    #[test]
1591    fn operation_serialises_snake_case() {
1592        let json = serde_json::to_string(&Operation::Telemetry).unwrap();
1593        assert_eq!(json, "\"telemetry\"");
1594        let back: Operation = serde_json::from_str("\"telemetry\"").unwrap();
1595        assert_eq!(back, Operation::Telemetry);
1596    }
1597
1598    // --- Selection / Projection ------------------------------------------
1599
1600    #[test]
1601    fn projection_include_and_exclude_mutually_exclusive() {
1602        let mut p = Projection::new().include(FieldPath::from_str("position.x").unwrap());
1603        p = p.exclude(FieldPath::from_str("velocity.vx").unwrap());
1604        assert!(!p.is_valid(), "include + exclude must be invalid");
1605    }
1606
1607    #[test]
1608    fn projection_empty_is_valid() {
1609        let p = Projection::new();
1610        assert!(p.is_valid(), "empty projection is valid (returns all)");
1611    }
1612
1613    #[test]
1614    fn selection_default_is_all() {
1615        assert_eq!(Selection::default(), Selection::All);
1616    }
1617
1618    // --- CachePolicy ------------------------------------------------------
1619
1620    #[test]
1621    fn cache_policy_disabled_is_not_enabled() {
1622        let p = CachePolicy::disabled();
1623        assert!(!p.enabled);
1624    }
1625
1626    #[test]
1627    fn cache_policy_default_is_enabled() {
1628        assert!(CachePolicy::default().enabled);
1629    }
1630
1631    // --- Response invariants ---------------------------------------------
1632
1633    #[test]
1634    fn response_ok_is_ok() {
1635        let r = Response::ok(ResponseValue::Unit, CorrelationId::new());
1636        assert!(r.is_ok());
1637        assert!(!r.is_err());
1638    }
1639
1640    #[test]
1641    fn response_err_is_err() {
1642        let r = Response::err(Error::timeout("deadline"), CorrelationId::new());
1643        assert!(r.is_err());
1644        assert!(!r.is_ok());
1645    }
1646
1647    // --- Error mapping ----------------------------------------------------
1648
1649    #[test]
1650    fn subject_error_converts_to_core_error() {
1651        let e: Error = SubjectError::EmptySubject.into();
1652        assert_eq!(e.code, ErrorCode::ValidationError);
1653    }
1654
1655    // --- Message construction --------------------------------------------
1656
1657    #[test]
1658    fn message_new_constructs_valid() {
1659        let m = Message::new("vehicle.sensors.imu.gyro", Operation::Telemetry).unwrap();
1660        assert_eq!(m.operation, Operation::Telemetry);
1661        assert_eq!(m.payload, Payload::Unit);
1662        assert_eq!(m.subject.as_str(), "vehicle.sensors.imu.gyro");
1663    }
1664
1665    #[test]
1666    fn message_new_rejects_invalid_subject() {
1667        let r = Message::new("", Operation::Telemetry);
1668        assert!(r.is_err());
1669    }
1670
1671    // --- Round-trip serialisation ---------------------------------------
1672
1673    #[test]
1674    fn message_round_trips_json() {
1675        let m = Message::new("vehicle.sensors.imu.gyro", Operation::Telemetry).unwrap();
1676        let json = serde_json::to_string(&m).unwrap();
1677        let back: Message = serde_json::from_str(&json).unwrap();
1678        assert_eq!(m, back);
1679    }
1680
1681    #[test]
1682    fn query_round_trips_json() {
1683        let q = Query::new(Resource::from_str("vehicle.sensors.imu.gyro").unwrap());
1684        let json = serde_json::to_string(&q).unwrap();
1685        let back: Query = serde_json::from_str(&json).unwrap();
1686        assert_eq!(q, back);
1687    }
1688
1689    // --- Property-style tests --------------------------------------------
1690
1691    #[test]
1692    fn subject_round_trip_preserves_string() {
1693        for s in [
1694            "vehicle",
1695            "vehicle.sensors",
1696            "vehicle.sensors.imu.gyro",
1697            "a.b.c.d.e.f.g",
1698        ] {
1699            let subject = Subject::from_str(s).unwrap();
1700            assert_eq!(subject.as_str(), s, "round-trip must preserve {s}");
1701        }
1702    }
1703
1704    #[test]
1705    fn pattern_matches_self_concrete() {
1706        // A pattern with all-concrete segments matches only itself.
1707        let p = SubjectPattern::from_str("vehicle.sensors.imu.gyro").unwrap();
1708        assert!(p.matches(&Subject::from_str("vehicle.sensors.imu.gyro").unwrap()));
1709        assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu.accel").unwrap()));
1710        assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu").unwrap()));
1711        assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu.gyro.extra").unwrap()));
1712    }
1713
1714    #[test]
1715    fn cache_key_stability_via_hash() {
1716        use std::collections::hash_map::DefaultHasher;
1717        use std::hash::{Hash, Hasher};
1718        let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1719        let mut h1 = DefaultHasher::new();
1720        s.hash(&mut h1);
1721        let mut h2 = DefaultHasher::new();
1722        s.hash(&mut h2);
1723        assert_eq!(h1.finish(), h2.finish(), "hash must be stable");
1724    }
1725}