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)]
1158#[serde(rename_all = "snake_case")]
1159pub enum ErrorCode {
1160 TransportError,
1162 ResolverError,
1164 CacheMiss,
1166 NotFound,
1168 ValidationError,
1170 AuthorizationError,
1172 Timeout,
1174 Conflict,
1176 InternalError,
1178}
1179
1180impl fmt::Display for ErrorCode {
1181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1182 let s = match self {
1183 Self::TransportError => "transport_error",
1184 Self::ResolverError => "resolver_error",
1185 Self::CacheMiss => "cache_miss",
1186 Self::NotFound => "not_found",
1187 Self::ValidationError => "validation_error",
1188 Self::AuthorizationError => "authorization_error",
1189 Self::Timeout => "timeout",
1190 Self::Conflict => "conflict",
1191 Self::InternalError => "internal_error",
1192 };
1193 f.write_str(s)
1194 }
1195}
1196
1197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1201pub struct Error {
1202 pub code: ErrorCode,
1204 pub message: String,
1206 pub subject: Option<Subject>,
1208 pub correlation_id: Option<CorrelationId>,
1210 pub causation_id: Option<CausationId>,
1212 pub details: Option<serde_json::Value>,
1214}
1215
1216impl Error {
1217 #[must_use]
1219 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
1220 Self {
1221 code,
1222 message: message.into(),
1223 subject: None,
1224 correlation_id: None,
1225 causation_id: None,
1226 details: None,
1227 }
1228 }
1229
1230 #[must_use]
1232 pub fn transport_error(msg: impl Into<String>) -> Self {
1233 Self::new(ErrorCode::TransportError, msg)
1234 }
1235
1236 #[must_use]
1238 pub fn resolver_error(msg: impl Into<String>) -> Self {
1239 Self::new(ErrorCode::ResolverError, msg)
1240 }
1241
1242 #[must_use]
1244 pub fn cache_miss(msg: impl Into<String>) -> Self {
1245 Self::new(ErrorCode::CacheMiss, msg)
1246 }
1247
1248 #[must_use]
1253 pub fn not_found(msg: impl Into<String>) -> Self {
1254 Self::new(ErrorCode::NotFound, msg)
1255 }
1256
1257 #[must_use]
1259 pub fn validation_error(msg: impl Into<String>) -> Self {
1260 Self::new(ErrorCode::ValidationError, msg)
1261 }
1262
1263 #[must_use]
1269 pub fn authorization_error(msg: impl Into<String>) -> Self {
1270 Self::new(ErrorCode::AuthorizationError, msg)
1271 }
1272
1273 #[must_use]
1275 pub fn timeout(msg: impl Into<String>) -> Self {
1276 Self::new(ErrorCode::Timeout, msg)
1277 }
1278
1279 #[must_use]
1284 pub fn conflict(msg: impl Into<String>) -> Self {
1285 Self::new(ErrorCode::Conflict, msg)
1286 }
1287
1288 #[must_use]
1290 pub fn internal_error(msg: impl Into<String>) -> Self {
1291 Self::new(ErrorCode::InternalError, msg)
1292 }
1293
1294 #[must_use]
1296 pub fn with_subject(mut self, s: Subject) -> Self {
1297 self.subject = Some(s);
1298 self
1299 }
1300
1301 #[must_use]
1303 pub fn with_correlation(mut self, id: CorrelationId) -> Self {
1304 self.correlation_id = Some(id);
1305 self
1306 }
1307
1308 #[must_use]
1310 pub fn with_causation(mut self, id: CausationId) -> Self {
1311 self.causation_id = Some(id);
1312 self
1313 }
1314
1315 #[must_use]
1317 pub fn with_details(mut self, details: serde_json::Value) -> Self {
1318 self.details = Some(details);
1319 self
1320 }
1321}
1322
1323impl fmt::Display for Error {
1324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1325 write!(f, "{}: {}", self.code, self.message)
1326 }
1327}
1328
1329impl std::error::Error for Error {}
1330
1331impl From<SubjectError> for Error {
1332 fn from(e: SubjectError) -> Self {
1333 Self::validation_error(e.to_string())
1334 }
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1345pub struct Query {
1346 pub resource: Resource,
1348 pub selection: Selection,
1350 pub arguments: Option<serde_json::Value>,
1352 pub projection: Option<Projection>,
1354 pub context: Context,
1356 pub cache_policy: Option<CachePolicy>,
1358 pub deadline: Option<Deadline>,
1360}
1361
1362impl Query {
1363 #[must_use]
1366 pub fn new(resource: Resource) -> Self {
1367 Self {
1368 resource,
1369 selection: Selection::All,
1370 arguments: None,
1371 projection: None,
1372 context: Context::default(),
1373 cache_policy: None,
1374 deadline: None,
1375 }
1376 }
1377
1378 #[must_use]
1380 pub fn with_selection(mut self, s: Selection) -> Self {
1381 self.selection = s;
1382 self
1383 }
1384
1385 #[must_use]
1388 pub fn with_projection(mut self, p: Projection) -> Self {
1389 self.projection = Some(p);
1390 self
1391 }
1392
1393 #[must_use]
1395 pub fn with_arguments(mut self, args: serde_json::Value) -> Self {
1396 self.arguments = Some(args);
1397 self
1398 }
1399}
1400
1401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1408pub struct Message {
1409 pub id: MessageId,
1411 pub timestamp: Timestamp,
1413 pub subject: Subject,
1415 pub operation: Operation,
1417 pub payload: Payload,
1419 pub metadata: Metadata,
1421}
1422
1423impl Message {
1424 pub fn new(subject_str: &str, operation: Operation) -> Result<Self, SubjectError> {
1430 Ok(Self {
1431 id: MessageId::new(),
1432 timestamp: Timestamp::now_monotonic(),
1433 subject: Subject::from_str(subject_str)?,
1434 operation,
1435 payload: Payload::Unit,
1436 metadata: Metadata::default(),
1437 })
1438 }
1439}
1440
1441pub trait Resolver: Send + Sync {
1451 fn resolve<'a>(
1456 &'a self,
1457 query: &'a Query,
1458 ctx: &'a Context,
1459 ) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>>;
1460}
1461
1462pub trait ResolverBoxed: Send + Sync {
1465 #[allow(async_fn_in_trait)]
1470 fn resolve(
1471 &self,
1472 query: &Query,
1473 ctx: &Context,
1474 ) -> impl Future<Output = Result<Response, Error>> + Send;
1475}
1476
1477impl<T> Resolver for T
1478where
1479 T: ResolverBoxed + ?Sized,
1480{
1481 fn resolve<'a>(
1482 &'a self,
1483 query: &'a Query,
1484 ctx: &'a Context,
1485 ) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>> {
1486 Box::pin(<T as ResolverBoxed>::resolve(self, query, ctx))
1487 }
1488}
1489
1490pub trait MessageHandler: Send + Sync {
1495 fn handle<'a>(
1500 &'a self,
1501 msg: &'a Message,
1502 ) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>>;
1503}
1504
1505#[allow(async_fn_in_trait)]
1510pub trait QueryExecutor {
1511 fn execute(
1516 &self,
1517 query: &Query,
1518 ctx: &Context,
1519 ) -> impl Future<Output = Result<Response, Error>>;
1520}
1521
1522#[cfg(test)]
1527mod tests {
1528 use super::*;
1529
1530 #[test]
1533 fn message_id_is_unique() {
1534 let a = MessageId::new();
1535 let b = MessageId::new();
1536 assert_ne!(a, b, "two MessageId::new() must differ");
1537 }
1538
1539 #[test]
1540 fn correlation_id_derives_from_request() {
1541 let req = MessageId::new();
1542 let corr = CorrelationId::for_request(&req);
1543 assert_eq!(corr.0, req.0, "correlation must equal request id");
1544 }
1545
1546 #[test]
1549 fn subject_parses_simple() {
1550 let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1551 assert_eq!(s.segments().len(), 4);
1552 assert_eq!(s.as_str(), "vehicle.sensors.imu.gyro");
1553 assert!(s.is_concrete());
1554 }
1555
1556 #[test]
1557 fn subject_rejects_empty() {
1558 assert!(matches!(
1559 Subject::from_str(""),
1560 Err(SubjectError::EmptySubject)
1561 ));
1562 }
1563
1564 #[test]
1565 fn subject_rejects_trailing_separator() {
1566 assert!(matches!(
1567 Subject::from_str("vehicle."),
1568 Err(SubjectError::TrailingSeparator)
1569 ));
1570 }
1571
1572 #[test]
1573 fn subject_rejects_leading_separator() {
1574 assert!(matches!(
1575 Subject::from_str(".vehicle"),
1576 Err(SubjectError::LeadingSeparator)
1577 ));
1578 }
1579
1580 #[test]
1581 fn subject_rejects_consecutive_separators() {
1582 assert!(matches!(
1583 Subject::from_str("vehicle..sensors"),
1584 Err(SubjectError::ConsecutiveSeparators)
1585 ));
1586 }
1587
1588 #[test]
1589 fn subject_rejects_wildcard_in_concrete() {
1590 assert!(matches!(
1591 Subject::from_str("vehicle.+.sensors"),
1592 Err(SubjectError::WildcardInConcreteSubject)
1593 ));
1594 }
1595
1596 #[test]
1597 fn subject_segment_rejects_invalid_charset() {
1598 assert!(matches!(
1599 SubjectSegment::new("bad/segment"),
1600 Err(SubjectError::InvalidCharset { .. })
1601 ));
1602 }
1603
1604 #[test]
1607 fn pattern_matches_exact() {
1608 let p = SubjectPattern::from_str("vehicle.sensors.imu.gyro").unwrap();
1609 let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1610 assert!(p.matches(&s));
1611 }
1612
1613 #[test]
1614 fn pattern_matches_wildcard_one() {
1615 let p = SubjectPattern::from_str("vehicle.+.imu.gyro").unwrap();
1616 let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1617 assert!(p.matches(&s), "+ must match one segment");
1618 }
1619
1620 #[test]
1621 fn pattern_does_not_match_wildcard_one_when_length_differs() {
1622 let p = SubjectPattern::from_str("vehicle.+.gyro").unwrap();
1623 let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1624 assert!(!p.matches(&s), "+ matches exactly one segment");
1625 }
1626
1627 #[test]
1628 fn pattern_matches_wildcard_multi_trailing() {
1629 let p = SubjectPattern::from_str("vehicle.#").unwrap();
1630 let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1631 assert!(p.matches(&s), "# must match trailing segments");
1632 }
1633
1634 #[test]
1635 fn pattern_wildcard_multi_matches_zero() {
1636 let p = SubjectPattern::from_str("vehicle.sensors.#").unwrap();
1637 let s = Subject::from_str("vehicle.sensors").unwrap();
1638 assert!(p.matches(&s), "# must match zero trailing segments");
1639 }
1640
1641 #[test]
1642 fn pattern_rejects_wildcard_multi_not_last() {
1643 assert!(matches!(
1644 SubjectPattern::from_str("vehicle.#.sensors"),
1645 Err(SubjectError::WildcardMultiNotLast)
1646 ));
1647 }
1648
1649 #[test]
1652 fn operation_serialises_snake_case() {
1653 let json = serde_json::to_string(&Operation::Telemetry).unwrap();
1654 assert_eq!(json, "\"telemetry\"");
1655 let back: Operation = serde_json::from_str("\"telemetry\"").unwrap();
1656 assert_eq!(back, Operation::Telemetry);
1657 }
1658
1659 #[test]
1662 fn projection_include_and_exclude_mutually_exclusive() {
1663 let mut p = Projection::new().include(FieldPath::from_str("position.x").unwrap());
1664 p = p.exclude(FieldPath::from_str("velocity.vx").unwrap());
1665 assert!(!p.is_valid(), "include + exclude must be invalid");
1666 }
1667
1668 #[test]
1669 fn projection_empty_is_valid() {
1670 let p = Projection::new();
1671 assert!(p.is_valid(), "empty projection is valid (returns all)");
1672 }
1673
1674 #[test]
1675 fn selection_default_is_all() {
1676 assert_eq!(Selection::default(), Selection::All);
1677 }
1678
1679 #[test]
1682 fn cache_policy_disabled_is_not_enabled() {
1683 let p = CachePolicy::disabled();
1684 assert!(!p.enabled);
1685 }
1686
1687 #[test]
1688 fn cache_policy_default_is_enabled() {
1689 assert!(CachePolicy::default().enabled);
1690 }
1691
1692 #[test]
1695 fn response_ok_is_ok() {
1696 let r = Response::ok(ResponseValue::Unit, CorrelationId::new());
1697 assert!(r.is_ok());
1698 assert!(!r.is_err());
1699 }
1700
1701 #[test]
1702 fn response_err_is_err() {
1703 let r = Response::err(Error::timeout("deadline"), CorrelationId::new());
1704 assert!(r.is_err());
1705 assert!(!r.is_ok());
1706 }
1707
1708 #[test]
1711 fn subject_error_converts_to_core_error() {
1712 let e: Error = SubjectError::EmptySubject.into();
1713 assert_eq!(e.code, ErrorCode::ValidationError);
1714 }
1715
1716 #[test]
1719 fn message_new_constructs_valid() {
1720 let m = Message::new("vehicle.sensors.imu.gyro", Operation::Telemetry).unwrap();
1721 assert_eq!(m.operation, Operation::Telemetry);
1722 assert_eq!(m.payload, Payload::Unit);
1723 assert_eq!(m.subject.as_str(), "vehicle.sensors.imu.gyro");
1724 }
1725
1726 #[test]
1727 fn message_new_rejects_invalid_subject() {
1728 let r = Message::new("", Operation::Telemetry);
1729 assert!(r.is_err());
1730 }
1731
1732 #[test]
1735 fn message_round_trips_json() {
1736 let m = Message::new("vehicle.sensors.imu.gyro", Operation::Telemetry).unwrap();
1737 let json = serde_json::to_string(&m).unwrap();
1738 let back: Message = serde_json::from_str(&json).unwrap();
1739 assert_eq!(m, back);
1740 }
1741
1742 #[test]
1743 fn query_round_trips_json() {
1744 let q = Query::new(Resource::from_str("vehicle.sensors.imu.gyro").unwrap());
1745 let json = serde_json::to_string(&q).unwrap();
1746 let back: Query = serde_json::from_str(&json).unwrap();
1747 assert_eq!(q, back);
1748 }
1749
1750 #[test]
1753 fn subject_round_trip_preserves_string() {
1754 for s in [
1755 "vehicle",
1756 "vehicle.sensors",
1757 "vehicle.sensors.imu.gyro",
1758 "a.b.c.d.e.f.g",
1759 ] {
1760 let subject = Subject::from_str(s).unwrap();
1761 assert_eq!(subject.as_str(), s, "round-trip must preserve {s}");
1762 }
1763 }
1764
1765 #[test]
1766 fn pattern_matches_self_concrete() {
1767 let p = SubjectPattern::from_str("vehicle.sensors.imu.gyro").unwrap();
1769 assert!(p.matches(&Subject::from_str("vehicle.sensors.imu.gyro").unwrap()));
1770 assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu.accel").unwrap()));
1771 assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu").unwrap()));
1772 assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu.gyro.extra").unwrap()));
1773 }
1774
1775 #[test]
1776 fn cache_key_stability_via_hash() {
1777 use std::collections::hash_map::DefaultHasher;
1778 use std::hash::{Hash, Hasher};
1779 let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
1780 let mut h1 = DefaultHasher::new();
1781 s.hash(&mut h1);
1782 let mut h2 = DefaultHasher::new();
1783 s.hash(&mut h2);
1784 assert_eq!(h1.finish(), h2.finish(), "hash must be stable");
1785 }
1786}