1#![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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
57#[serde(transparent)]
58pub struct MessageId(pub Uuid);
59
60impl MessageId {
61 #[must_use]
63 pub fn new() -> Self {
64 Self(Uuid::now_v7())
65 }
66
67 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(transparent)]
90pub struct CorrelationId(pub Uuid);
91
92impl CorrelationId {
93 #[must_use]
95 pub fn for_request(request: &MessageId) -> Self {
96 Self(request.0)
98 }
99
100 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
116#[serde(transparent)]
117pub struct CausationId(pub Uuid);
118
119impl CausationId {
120 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
135#[serde(transparent)]
136pub struct TraceId(pub Uuid);
137
138impl TraceId {
139 #[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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
162#[serde(transparent)]
163pub struct SubjectSegment(pub String);
164
165impl SubjectSegment {
166 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 #[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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
206pub struct Subject {
207 segments: Vec<SubjectSegment>,
208}
209
210impl Subject {
211 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 #[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 if part == "+" || part == "#" {
248 return Err(SubjectError::WildcardInConcreteSubject);
249 }
250 SubjectSegment::new(part)
251 })
252 .collect::<Result<_, _>>()?;
253 Ok(Self { segments })
254 }
255
256 #[must_use]
258 pub fn segments(&self) -> &[SubjectSegment] {
259 &self.segments
260 }
261
262 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
287pub enum PatternSegment {
288 Concrete(SubjectSegment),
290 WildcardOne,
292 WildcardMulti,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
300pub struct SubjectPattern {
301 segments: Vec<PatternSegment>,
302}
303
304impl SubjectPattern {
305 #[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 #[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 return pi == pattern.len() - 1;
370 }
371 }
372 }
373 if pi < pattern.len() && matches!(pattern[pi], PatternSegment::WildcardMulti) {
376 return true;
377 }
378 pi == pattern.len() && ci == concrete.len()
379 }
380
381 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
411pub enum SubjectError {
412 #[error("empty segment")]
414 EmptySegment,
415 #[error("invalid charset in segment: {segment}")]
417 InvalidCharset {
418 segment: String,
420 },
421 #[error("separator inside segment")]
423 SeparatorInSegment,
424 #[error("trailing separator")]
426 TrailingSeparator,
427 #[error("leading separator")]
429 LeadingSeparator,
430 #[error("consecutive separators")]
432 ConsecutiveSeparators,
433 #[error("wildcard in concrete subject")]
435 WildcardInConcreteSubject,
436 #[error("wildcard '#' must be the last segment")]
438 WildcardMultiNotLast,
439 #[error("empty subject")]
441 EmptySubject,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
450#[serde(rename_all = "snake_case")]
451pub enum Operation {
452 Query,
454 Command,
456 Event,
458 Stream,
460 Response,
462 Telemetry,
464 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
489#[serde(rename_all = "lowercase")]
490pub enum FormatTag {
491 Json,
493 Bincode,
495 Postcard,
497 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
517pub enum Payload {
518 Json(serde_json::Value),
520 Bytes(Vec<u8>, FormatTag),
522 Unit,
524}
525
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532pub struct Principal {
533 pub id: String,
535 pub scopes: Vec<String>,
537}
538
539#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
543pub struct Metadata {
544 pub correlation_id: Option<CorrelationId>,
546 pub causation_id: Option<CausationId>,
548 pub trace_id: Option<TraceId>,
550 pub auth_principal: Option<Principal>,
552 pub format: Option<FormatTag>,
554 pub extensions: BTreeMap<String, serde_json::Value>,
556}
557
558impl Metadata {
559 #[must_use]
561 pub fn new() -> Self {
562 Self::default()
563 }
564
565 #[must_use]
567 pub fn with_correlation(mut self, id: CorrelationId) -> Self {
568 self.correlation_id = Some(id);
569 self
570 }
571
572 #[must_use]
574 pub fn with_causation(mut self, id: CausationId) -> Self {
575 self.causation_id = Some(id);
576 self
577 }
578
579 #[must_use]
581 pub fn with_trace(mut self, id: TraceId) -> Self {
582 self.trace_id = Some(id);
583 self
584 }
585
586 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
605#[serde(rename_all = "snake_case")]
606pub enum TimestampKind {
607 Monotonic,
609 WallClock,
611}
612
613#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
616pub struct Timestamp {
617 pub kind: TimestampKind,
619 pub nanos_since_epoch: i64,
621}
622
623impl Timestamp {
624 #[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 #[must_use]
635 pub fn now_monotonic() -> Self {
636 Self::new(TimestampKind::Monotonic, 0)
637 }
638
639 #[must_use]
641 pub fn now_wall_clock() -> Self {
642 Self::new(TimestampKind::WallClock, 0)
643 }
644}
645
646#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
651pub struct Deadline {
652 pub kind: TimestampKind,
654 pub nanos_since_epoch: i64,
657}
658
659impl Deadline {
660 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
682pub struct FieldPath {
683 segments: Vec<String>,
684}
685
686impl FieldPath {
687 #[must_use]
689 pub fn from_segments(segments: Vec<String>) -> Self {
690 Self { segments }
691 }
692
693 #[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 #[must_use]
711 pub fn segments(&self) -> &[String] {
712 &self.segments
713 }
714
715 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
730#[serde(rename_all = "snake_case")]
731pub enum Comparison {
732 Eq,
734 Ne,
736 Lt,
738 Le,
740 Gt,
742 Ge,
744 In,
746 Contains,
748}
749
750#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
752pub enum Predicate {
753 Field {
755 path: FieldPath,
757 op: Comparison,
759 value: serde_json::Value,
761 },
762 And(Vec<Predicate>),
764 Or(Vec<Predicate>),
766 Not(Box<Predicate>),
768}
769
770#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
775pub enum Selection {
776 #[default]
778 All,
779 Filter(Predicate),
781}
782
783impl Selection {
784 #[must_use]
786 pub fn all() -> Self {
787 Self::All
788 }
789
790 #[must_use]
792 pub fn filter(p: Predicate) -> Self {
793 Self::Filter(p)
794 }
795}
796
797#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
805pub struct Projection {
806 pub include: Vec<FieldPath>,
808 pub exclude: Vec<FieldPath>,
810}
811
812impl Projection {
813 #[must_use]
815 pub fn new() -> Self {
816 Self::default()
817 }
818
819 #[must_use]
821 pub fn include(mut self, path: FieldPath) -> Self {
822 self.include.push(path);
823 self
824 }
825
826 #[must_use]
828 pub fn exclude(mut self, path: FieldPath) -> Self {
829 self.exclude.push(path);
830 self
831 }
832
833 #[must_use]
836 pub fn is_valid(&self) -> bool {
837 self.include.is_empty() || self.exclude.is_empty()
838 }
839}
840
841#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
848pub struct Resource {
849 pub subject: Subject,
851}
852
853impl Resource {
854 #[must_use]
856 pub fn from_subject(subject: Subject) -> Self {
857 Self { subject }
858 }
859
860 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
884pub enum CacheTier {
885 L1,
887 L2,
889 L3,
891 L4,
893}
894
895#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
897pub enum InvalidationHint {
898 OnSubjectChange(SubjectPattern),
900 AfterTtl(std::time::Duration),
902 Explicit,
904}
905
906#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
909pub struct CachePolicy {
910 pub enabled: bool,
912 pub ttl: Option<std::time::Duration>,
914 pub tier_hint: Option<CacheTier>,
916 pub bypass: bool,
918 pub invalidation_hint: Option<InvalidationHint>,
920}
921
922impl CachePolicy {
923 #[must_use]
926 pub fn new() -> Self {
927 Self::default()
928 }
929
930 #[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 #[must_use]
944 pub fn with_ttl(mut self, ttl: std::time::Duration) -> Self {
945 self.ttl = Some(ttl);
946 self
947 }
948
949 #[must_use]
951 pub fn with_tier(mut self, tier: CacheTier) -> Self {
952 self.tier_hint = Some(tier);
953 self
954 }
955
956 #[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#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
988pub struct Cancellation {
989 cancelled: bool,
990}
991
992impl Cancellation {
993 #[must_use]
995 pub fn new() -> Self {
996 Self { cancelled: false }
997 }
998
999 #[must_use]
1001 pub fn is_cancelled(&self) -> bool {
1002 self.cancelled
1003 }
1004
1005 pub fn cancel(&mut self) {
1007 self.cancelled = true;
1008 }
1009}
1010
1011#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
1019pub struct Context {
1020 pub deadline: Option<Deadline>,
1022 pub principal: Option<Principal>,
1024 pub trace_id: Option<TraceId>,
1026 pub cache_policy: CachePolicy,
1028 pub cancellation: Cancellation,
1030}
1031
1032impl Context {
1033 #[must_use]
1035 pub fn new() -> Self {
1036 Self::default()
1037 }
1038
1039 #[must_use]
1041 pub fn with_deadline(mut self, d: Deadline) -> Self {
1042 self.deadline = Some(d);
1043 self
1044 }
1045
1046 #[must_use]
1048 pub fn with_principal(mut self, p: Principal) -> Self {
1049 self.principal = Some(p);
1050 self
1051 }
1052
1053 #[must_use]
1055 pub fn with_trace(mut self, t: TraceId) -> Self {
1056 self.trace_id = Some(t);
1057 self
1058 }
1059
1060 #[must_use]
1062 pub fn with_cache_policy(mut self, p: CachePolicy) -> Self {
1063 self.cache_policy = p;
1064 self
1065 }
1066}
1067
1068#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1075pub enum ResponseValue {
1076 Json(serde_json::Value),
1078 Bytes(Vec<u8>, FormatTag),
1080 Unit,
1082}
1083
1084#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1088pub struct Response {
1089 pub result: Result<ResponseValue, Error>,
1091 pub correlation_id: CorrelationId,
1093}
1094
1095impl Response {
1096 #[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 #[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 #[must_use]
1116 pub fn is_ok(&self) -> bool {
1117 self.result.is_ok()
1118 }
1119
1120 #[must_use]
1122 pub fn is_err(&self) -> bool {
1123 self.result.is_err()
1124 }
1125}
1126
1127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1134#[serde(rename_all = "snake_case")]
1135pub enum ErrorCode {
1136 TransportError,
1138 ResolverError,
1140 CacheMiss,
1142 ValidationError,
1144 Timeout,
1146 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1168pub struct Error {
1169 pub code: ErrorCode,
1171 pub message: String,
1173 pub subject: Option<Subject>,
1175 pub correlation_id: Option<CorrelationId>,
1177 pub causation_id: Option<CausationId>,
1179 pub details: Option<serde_json::Value>,
1181}
1182
1183impl Error {
1184 #[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 #[must_use]
1199 pub fn transport_error(msg: impl Into<String>) -> Self {
1200 Self::new(ErrorCode::TransportError, msg)
1201 }
1202
1203 #[must_use]
1205 pub fn resolver_error(msg: impl Into<String>) -> Self {
1206 Self::new(ErrorCode::ResolverError, msg)
1207 }
1208
1209 #[must_use]
1211 pub fn cache_miss(msg: impl Into<String>) -> Self {
1212 Self::new(ErrorCode::CacheMiss, msg)
1213 }
1214
1215 #[must_use]
1217 pub fn validation_error(msg: impl Into<String>) -> Self {
1218 Self::new(ErrorCode::ValidationError, msg)
1219 }
1220
1221 #[must_use]
1223 pub fn timeout(msg: impl Into<String>) -> Self {
1224 Self::new(ErrorCode::Timeout, msg)
1225 }
1226
1227 #[must_use]
1229 pub fn internal_error(msg: impl Into<String>) -> Self {
1230 Self::new(ErrorCode::InternalError, msg)
1231 }
1232
1233 #[must_use]
1235 pub fn with_subject(mut self, s: Subject) -> Self {
1236 self.subject = Some(s);
1237 self
1238 }
1239
1240 #[must_use]
1242 pub fn with_correlation(mut self, id: CorrelationId) -> Self {
1243 self.correlation_id = Some(id);
1244 self
1245 }
1246
1247 #[must_use]
1249 pub fn with_causation(mut self, id: CausationId) -> Self {
1250 self.causation_id = Some(id);
1251 self
1252 }
1253
1254 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1284pub struct Query {
1285 pub resource: Resource,
1287 pub selection: Selection,
1289 pub arguments: Option<serde_json::Value>,
1291 pub projection: Option<Projection>,
1293 pub context: Context,
1295 pub cache_policy: Option<CachePolicy>,
1297 pub deadline: Option<Deadline>,
1299}
1300
1301impl Query {
1302 #[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 #[must_use]
1319 pub fn with_selection(mut self, s: Selection) -> Self {
1320 self.selection = s;
1321 self
1322 }
1323
1324 #[must_use]
1327 pub fn with_projection(mut self, p: Projection) -> Self {
1328 self.projection = Some(p);
1329 self
1330 }
1331
1332 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1347pub struct Message {
1348 pub id: MessageId,
1350 pub timestamp: Timestamp,
1352 pub subject: Subject,
1354 pub operation: Operation,
1356 pub payload: Payload,
1358 pub metadata: Metadata,
1360}
1361
1362impl Message {
1363 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
1380pub trait Resolver: Send + Sync {
1390 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
1401pub trait ResolverBoxed: Send + Sync {
1404 #[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
1429pub trait MessageHandler: Send + Sync {
1434 fn handle<'a>(
1439 &'a self,
1440 msg: &'a Message,
1441 ) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>>;
1442}
1443
1444#[allow(async_fn_in_trait)]
1449pub trait QueryExecutor {
1450 fn execute(
1455 &self,
1456 query: &Query,
1457 ctx: &Context,
1458 ) -> impl Future<Output = Result<Response, Error>>;
1459}
1460
1461#[cfg(test)]
1466mod tests {
1467 use super::*;
1468
1469 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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}