#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![warn(missing_docs)]
#![allow(clippy::module_name_repetitions)]
use std::collections::BTreeMap;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MessageId(pub Uuid);
impl MessageId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
#[must_use]
pub fn as_uuid(&self) -> Uuid {
self.0
}
}
impl Default for MessageId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for MessageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CorrelationId(pub Uuid);
impl CorrelationId {
#[must_use]
pub fn for_request(request: &MessageId) -> Self {
Self(request.0)
}
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
}
impl Default for CorrelationId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CausationId(pub Uuid);
impl CausationId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
}
impl Default for CausationId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TraceId(pub Uuid);
impl TraceId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
}
impl Default for TraceId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SubjectSegment(pub String);
impl SubjectSegment {
pub fn new(s: &str) -> Result<Self, SubjectError> {
if s.is_empty() {
return Err(SubjectError::EmptySegment);
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err(SubjectError::InvalidCharset {
segment: s.to_owned(),
});
}
Ok(Self(s.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SubjectSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Subject {
segments: Vec<SubjectSegment>,
}
impl Subject {
pub fn from_segments(segments: Vec<SubjectSegment>) -> Result<Self, SubjectError> {
if segments.is_empty() {
return Err(SubjectError::EmptySubject);
}
Ok(Self { segments })
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, SubjectError> {
if s.is_empty() {
return Err(SubjectError::EmptySubject);
}
if s.starts_with('.') {
return Err(SubjectError::LeadingSeparator);
}
if s.ends_with('.') {
return Err(SubjectError::TrailingSeparator);
}
if s.contains("..") {
return Err(SubjectError::ConsecutiveSeparators);
}
let segments: Vec<SubjectSegment> = s
.split('.')
.map(|part| {
if part == "+" || part == "#" {
return Err(SubjectError::WildcardInConcreteSubject);
}
SubjectSegment::new(part)
})
.collect::<Result<_, _>>()?;
Ok(Self { segments })
}
#[must_use]
pub fn segments(&self) -> &[SubjectSegment] {
&self.segments
}
#[must_use]
pub fn as_str(&self) -> String {
self.segments
.iter()
.map(SubjectSegment::as_str)
.collect::<Vec<_>>()
.join(".")
}
#[must_use]
pub fn is_concrete(&self) -> bool {
true
}
}
impl fmt::Display for Subject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PatternSegment {
Concrete(SubjectSegment),
WildcardOne,
WildcardMulti,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SubjectPattern {
segments: Vec<PatternSegment>,
}
impl SubjectPattern {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, SubjectError> {
if s.is_empty() {
return Err(SubjectError::EmptySubject);
}
if s.starts_with('.') {
return Err(SubjectError::LeadingSeparator);
}
if s.ends_with('.') {
return Err(SubjectError::TrailingSeparator);
}
if s.contains("..") {
return Err(SubjectError::ConsecutiveSeparators);
}
let parts: Vec<&str> = s.split('.').collect();
let mut segments = Vec::with_capacity(parts.len());
for (i, part) in parts.iter().enumerate() {
let seg = match *part {
"+" => PatternSegment::WildcardOne,
"#" => {
if i != parts.len() - 1 {
return Err(SubjectError::WildcardMultiNotLast);
}
PatternSegment::WildcardMulti
}
other => PatternSegment::Concrete(SubjectSegment::new(other)?),
};
segments.push(seg);
}
Ok(Self { segments })
}
#[must_use]
pub fn matches(&self, subject: &Subject) -> bool {
let concrete = subject.segments();
let pattern = &self.segments;
let mut ci = 0;
let mut pi = 0;
while pi < pattern.len() && ci < concrete.len() {
match &pattern[pi] {
PatternSegment::Concrete(seg) => {
if seg != &concrete[ci] {
return false;
}
ci += 1;
pi += 1;
}
PatternSegment::WildcardOne => {
ci += 1;
pi += 1;
}
PatternSegment::WildcardMulti => {
return pi == pattern.len() - 1;
}
}
}
if pi < pattern.len() && matches!(pattern[pi], PatternSegment::WildcardMulti) {
return true;
}
pi == pattern.len() && ci == concrete.len()
}
#[must_use]
pub fn as_str(&self) -> String {
self.segments
.iter()
.map(|s| match s {
PatternSegment::Concrete(seg) => seg.as_str().to_owned(),
PatternSegment::WildcardOne => "+".to_owned(),
PatternSegment::WildcardMulti => "#".to_owned(),
})
.collect::<Vec<_>>()
.join(".")
}
#[must_use]
pub fn segments(&self) -> &[PatternSegment] {
&self.segments
}
}
impl fmt::Display for SubjectPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SubjectError {
#[error("empty segment")]
EmptySegment,
#[error("invalid charset in segment: {segment}")]
InvalidCharset {
segment: String,
},
#[error("separator inside segment")]
SeparatorInSegment,
#[error("trailing separator")]
TrailingSeparator,
#[error("leading separator")]
LeadingSeparator,
#[error("consecutive separators")]
ConsecutiveSeparators,
#[error("wildcard in concrete subject")]
WildcardInConcreteSubject,
#[error("wildcard '#' must be the last segment")]
WildcardMultiNotLast,
#[error("empty subject")]
EmptySubject,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Operation {
Query,
Command,
Event,
Stream,
Response,
Telemetry,
Error,
}
impl fmt::Display for Operation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Query => "query",
Self::Command => "command",
Self::Event => "event",
Self::Stream => "stream",
Self::Response => "response",
Self::Telemetry => "telemetry",
Self::Error => "error",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FormatTag {
Json,
Bincode,
Postcard,
Custom(String),
}
impl fmt::Display for FormatTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Json => f.write_str("json"),
Self::Bincode => f.write_str("bincode"),
Self::Postcard => f.write_str("postcard"),
Self::Custom(name) => write!(f, "custom:{name}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Payload {
Json(serde_json::Value),
Bytes(Vec<u8>, FormatTag),
Unit,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Principal {
pub id: String,
pub scopes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct Metadata {
pub correlation_id: Option<CorrelationId>,
pub causation_id: Option<CausationId>,
pub trace_id: Option<TraceId>,
pub auth_principal: Option<Principal>,
pub format: Option<FormatTag>,
pub extensions: BTreeMap<String, serde_json::Value>,
}
impl Metadata {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_correlation(mut self, id: CorrelationId) -> Self {
self.correlation_id = Some(id);
self
}
#[must_use]
pub fn with_causation(mut self, id: CausationId) -> Self {
self.causation_id = Some(id);
self
}
#[must_use]
pub fn with_trace(mut self, id: TraceId) -> Self {
self.trace_id = Some(id);
self
}
#[must_use]
pub fn with_extension<K: Into<String>, V: Into<serde_json::Value>>(
mut self,
key: K,
value: V,
) -> Self {
self.extensions.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TimestampKind {
Monotonic,
WallClock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Timestamp {
pub kind: TimestampKind,
pub nanos_since_epoch: i64,
}
impl Timestamp {
#[must_use]
pub fn new(kind: TimestampKind, nanos_since_epoch: i64) -> Self {
Self {
kind,
nanos_since_epoch,
}
}
#[must_use]
pub fn now_monotonic() -> Self {
Self::new(TimestampKind::Monotonic, 0)
}
#[must_use]
pub fn now_wall_clock() -> Self {
Self::new(TimestampKind::WallClock, 0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Deadline {
pub kind: TimestampKind,
pub nanos_since_epoch: i64,
}
impl Deadline {
#[must_use]
pub fn new(kind: TimestampKind, nanos_since_epoch: i64) -> Self {
Self {
kind,
nanos_since_epoch,
}
}
#[must_use]
pub fn is_expired(&self, now: &Timestamp) -> bool {
self.kind == now.kind && self.nanos_since_epoch <= now.nanos_since_epoch
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FieldPath {
segments: Vec<String>,
}
impl FieldPath {
#[must_use]
pub fn from_segments(segments: Vec<String>) -> Self {
Self { segments }
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, SubjectError> {
if s.is_empty() {
return Err(SubjectError::EmptySegment);
}
let segments: Vec<String> = s.split('.').map(str::to_owned).collect();
if segments.iter().any(String::is_empty) {
return Err(SubjectError::EmptySegment);
}
Ok(Self { segments })
}
#[must_use]
pub fn segments(&self) -> &[String] {
&self.segments
}
#[must_use]
pub fn as_str(&self) -> String {
self.segments.join(".")
}
}
impl fmt::Display for FieldPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Comparison {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
In,
Contains,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Predicate {
Field {
path: FieldPath,
op: Comparison,
value: serde_json::Value,
},
And(Vec<Predicate>),
Or(Vec<Predicate>),
Not(Box<Predicate>),
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub enum Selection {
#[default]
All,
Filter(Predicate),
}
impl Selection {
#[must_use]
pub fn all() -> Self {
Self::All
}
#[must_use]
pub fn filter(p: Predicate) -> Self {
Self::Filter(p)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Projection {
pub include: Vec<FieldPath>,
pub exclude: Vec<FieldPath>,
}
impl Projection {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn include(mut self, path: FieldPath) -> Self {
self.include.push(path);
self
}
#[must_use]
pub fn exclude(mut self, path: FieldPath) -> Self {
self.exclude.push(path);
self
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.include.is_empty() || self.exclude.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Resource {
pub subject: Subject,
}
impl Resource {
#[must_use]
pub fn from_subject(subject: Subject) -> Self {
Self { subject }
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, SubjectError> {
Ok(Self {
subject: Subject::from_str(s)?,
})
}
}
impl fmt::Display for Resource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.subject)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CacheTier {
L1,
L2,
L3,
L4,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum InvalidationHint {
OnSubjectChange(SubjectPattern),
AfterTtl(std::time::Duration),
Explicit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CachePolicy {
pub enabled: bool,
pub ttl: Option<std::time::Duration>,
pub tier_hint: Option<CacheTier>,
pub bypass: bool,
pub invalidation_hint: Option<InvalidationHint>,
}
impl CachePolicy {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn disabled() -> Self {
Self {
enabled: false,
ttl: None,
tier_hint: None,
bypass: false,
invalidation_hint: None,
}
}
#[must_use]
pub fn with_ttl(mut self, ttl: std::time::Duration) -> Self {
self.ttl = Some(ttl);
self
}
#[must_use]
pub fn with_tier(mut self, tier: CacheTier) -> Self {
self.tier_hint = Some(tier);
self
}
#[must_use]
pub fn with_bypass(mut self, bypass: bool) -> Self {
self.bypass = bypass;
self
}
}
impl Default for CachePolicy {
fn default() -> Self {
Self {
enabled: true,
ttl: None,
tier_hint: None,
bypass: false,
invalidation_hint: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Cancellation {
cancelled: bool,
}
impl Cancellation {
#[must_use]
pub fn new() -> Self {
Self { cancelled: false }
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.cancelled
}
pub fn cancel(&mut self) {
self.cancelled = true;
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct Context {
pub deadline: Option<Deadline>,
pub principal: Option<Principal>,
pub trace_id: Option<TraceId>,
pub cache_policy: CachePolicy,
pub cancellation: Cancellation,
}
impl Context {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_deadline(mut self, d: Deadline) -> Self {
self.deadline = Some(d);
self
}
#[must_use]
pub fn with_principal(mut self, p: Principal) -> Self {
self.principal = Some(p);
self
}
#[must_use]
pub fn with_trace(mut self, t: TraceId) -> Self {
self.trace_id = Some(t);
self
}
#[must_use]
pub fn with_cache_policy(mut self, p: CachePolicy) -> Self {
self.cache_policy = p;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ResponseValue {
Json(serde_json::Value),
Bytes(Vec<u8>, FormatTag),
Unit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Response {
pub result: Result<ResponseValue, Error>,
pub correlation_id: CorrelationId,
}
impl Response {
#[must_use]
pub fn ok(value: ResponseValue, correlation_id: CorrelationId) -> Self {
Self {
result: Ok(value),
correlation_id,
}
}
#[must_use]
pub fn err(error: Error, correlation_id: CorrelationId) -> Self {
Self {
result: Err(error),
correlation_id,
}
}
#[must_use]
pub fn is_ok(&self) -> bool {
self.result.is_ok()
}
#[must_use]
pub fn is_err(&self) -> bool {
self.result.is_err()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
TransportError,
ResolverError,
CacheMiss,
ValidationError,
Timeout,
InternalError,
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::TransportError => "transport_error",
Self::ResolverError => "resolver_error",
Self::CacheMiss => "cache_miss",
Self::ValidationError => "validation_error",
Self::Timeout => "timeout",
Self::InternalError => "internal_error",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Error {
pub code: ErrorCode,
pub message: String,
pub subject: Option<Subject>,
pub correlation_id: Option<CorrelationId>,
pub causation_id: Option<CausationId>,
pub details: Option<serde_json::Value>,
}
impl Error {
#[must_use]
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
subject: None,
correlation_id: None,
causation_id: None,
details: None,
}
}
#[must_use]
pub fn transport_error(msg: impl Into<String>) -> Self {
Self::new(ErrorCode::TransportError, msg)
}
#[must_use]
pub fn resolver_error(msg: impl Into<String>) -> Self {
Self::new(ErrorCode::ResolverError, msg)
}
#[must_use]
pub fn cache_miss(msg: impl Into<String>) -> Self {
Self::new(ErrorCode::CacheMiss, msg)
}
#[must_use]
pub fn validation_error(msg: impl Into<String>) -> Self {
Self::new(ErrorCode::ValidationError, msg)
}
#[must_use]
pub fn timeout(msg: impl Into<String>) -> Self {
Self::new(ErrorCode::Timeout, msg)
}
#[must_use]
pub fn internal_error(msg: impl Into<String>) -> Self {
Self::new(ErrorCode::InternalError, msg)
}
#[must_use]
pub fn with_subject(mut self, s: Subject) -> Self {
self.subject = Some(s);
self
}
#[must_use]
pub fn with_correlation(mut self, id: CorrelationId) -> Self {
self.correlation_id = Some(id);
self
}
#[must_use]
pub fn with_causation(mut self, id: CausationId) -> Self {
self.causation_id = Some(id);
self
}
#[must_use]
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for Error {}
impl From<SubjectError> for Error {
fn from(e: SubjectError) -> Self {
Self::validation_error(e.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Query {
pub resource: Resource,
pub selection: Selection,
pub arguments: Option<serde_json::Value>,
pub projection: Option<Projection>,
pub context: Context,
pub cache_policy: Option<CachePolicy>,
pub deadline: Option<Deadline>,
}
impl Query {
#[must_use]
pub fn new(resource: Resource) -> Self {
Self {
resource,
selection: Selection::All,
arguments: None,
projection: None,
context: Context::default(),
cache_policy: None,
deadline: None,
}
}
#[must_use]
pub fn with_selection(mut self, s: Selection) -> Self {
self.selection = s;
self
}
#[must_use]
pub fn with_projection(mut self, p: Projection) -> Self {
self.projection = Some(p);
self
}
#[must_use]
pub fn with_arguments(mut self, args: serde_json::Value) -> Self {
self.arguments = Some(args);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub id: MessageId,
pub timestamp: Timestamp,
pub subject: Subject,
pub operation: Operation,
pub payload: Payload,
pub metadata: Metadata,
}
impl Message {
pub fn new(subject_str: &str, operation: Operation) -> Result<Self, SubjectError> {
Ok(Self {
id: MessageId::new(),
timestamp: Timestamp::now_monotonic(),
subject: Subject::from_str(subject_str)?,
operation,
payload: Payload::Unit,
metadata: Metadata::default(),
})
}
}
pub trait Resolver: Send + Sync {
fn resolve<'a>(
&'a self,
query: &'a Query,
ctx: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>>;
}
pub trait ResolverBoxed: Send + Sync {
#[allow(async_fn_in_trait)]
fn resolve(
&self,
query: &Query,
ctx: &Context,
) -> impl Future<Output = Result<Response, Error>> + Send;
}
impl<T> Resolver for T
where
T: ResolverBoxed + ?Sized,
{
fn resolve<'a>(
&'a self,
query: &'a Query,
ctx: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>> {
Box::pin(<T as ResolverBoxed>::resolve(self, query, ctx))
}
}
pub trait MessageHandler: Send + Sync {
fn handle<'a>(
&'a self,
msg: &'a Message,
) -> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send + 'a>>;
}
#[allow(async_fn_in_trait)]
pub trait QueryExecutor {
fn execute(
&self,
query: &Query,
ctx: &Context,
) -> impl Future<Output = Result<Response, Error>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_id_is_unique() {
let a = MessageId::new();
let b = MessageId::new();
assert_ne!(a, b, "two MessageId::new() must differ");
}
#[test]
fn correlation_id_derives_from_request() {
let req = MessageId::new();
let corr = CorrelationId::for_request(&req);
assert_eq!(corr.0, req.0, "correlation must equal request id");
}
#[test]
fn subject_parses_simple() {
let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
assert_eq!(s.segments().len(), 4);
assert_eq!(s.as_str(), "vehicle.sensors.imu.gyro");
assert!(s.is_concrete());
}
#[test]
fn subject_rejects_empty() {
assert!(matches!(
Subject::from_str(""),
Err(SubjectError::EmptySubject)
));
}
#[test]
fn subject_rejects_trailing_separator() {
assert!(matches!(
Subject::from_str("vehicle."),
Err(SubjectError::TrailingSeparator)
));
}
#[test]
fn subject_rejects_leading_separator() {
assert!(matches!(
Subject::from_str(".vehicle"),
Err(SubjectError::LeadingSeparator)
));
}
#[test]
fn subject_rejects_consecutive_separators() {
assert!(matches!(
Subject::from_str("vehicle..sensors"),
Err(SubjectError::ConsecutiveSeparators)
));
}
#[test]
fn subject_rejects_wildcard_in_concrete() {
assert!(matches!(
Subject::from_str("vehicle.+.sensors"),
Err(SubjectError::WildcardInConcreteSubject)
));
}
#[test]
fn subject_segment_rejects_invalid_charset() {
assert!(matches!(
SubjectSegment::new("bad/segment"),
Err(SubjectError::InvalidCharset { .. })
));
}
#[test]
fn pattern_matches_exact() {
let p = SubjectPattern::from_str("vehicle.sensors.imu.gyro").unwrap();
let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
assert!(p.matches(&s));
}
#[test]
fn pattern_matches_wildcard_one() {
let p = SubjectPattern::from_str("vehicle.+.imu.gyro").unwrap();
let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
assert!(p.matches(&s), "+ must match one segment");
}
#[test]
fn pattern_does_not_match_wildcard_one_when_length_differs() {
let p = SubjectPattern::from_str("vehicle.+.gyro").unwrap();
let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
assert!(!p.matches(&s), "+ matches exactly one segment");
}
#[test]
fn pattern_matches_wildcard_multi_trailing() {
let p = SubjectPattern::from_str("vehicle.#").unwrap();
let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
assert!(p.matches(&s), "# must match trailing segments");
}
#[test]
fn pattern_wildcard_multi_matches_zero() {
let p = SubjectPattern::from_str("vehicle.sensors.#").unwrap();
let s = Subject::from_str("vehicle.sensors").unwrap();
assert!(p.matches(&s), "# must match zero trailing segments");
}
#[test]
fn pattern_rejects_wildcard_multi_not_last() {
assert!(matches!(
SubjectPattern::from_str("vehicle.#.sensors"),
Err(SubjectError::WildcardMultiNotLast)
));
}
#[test]
fn operation_serialises_snake_case() {
let json = serde_json::to_string(&Operation::Telemetry).unwrap();
assert_eq!(json, "\"telemetry\"");
let back: Operation = serde_json::from_str("\"telemetry\"").unwrap();
assert_eq!(back, Operation::Telemetry);
}
#[test]
fn projection_include_and_exclude_mutually_exclusive() {
let mut p = Projection::new().include(FieldPath::from_str("position.x").unwrap());
p = p.exclude(FieldPath::from_str("velocity.vx").unwrap());
assert!(!p.is_valid(), "include + exclude must be invalid");
}
#[test]
fn projection_empty_is_valid() {
let p = Projection::new();
assert!(p.is_valid(), "empty projection is valid (returns all)");
}
#[test]
fn selection_default_is_all() {
assert_eq!(Selection::default(), Selection::All);
}
#[test]
fn cache_policy_disabled_is_not_enabled() {
let p = CachePolicy::disabled();
assert!(!p.enabled);
}
#[test]
fn cache_policy_default_is_enabled() {
assert!(CachePolicy::default().enabled);
}
#[test]
fn response_ok_is_ok() {
let r = Response::ok(ResponseValue::Unit, CorrelationId::new());
assert!(r.is_ok());
assert!(!r.is_err());
}
#[test]
fn response_err_is_err() {
let r = Response::err(Error::timeout("deadline"), CorrelationId::new());
assert!(r.is_err());
assert!(!r.is_ok());
}
#[test]
fn subject_error_converts_to_core_error() {
let e: Error = SubjectError::EmptySubject.into();
assert_eq!(e.code, ErrorCode::ValidationError);
}
#[test]
fn message_new_constructs_valid() {
let m = Message::new("vehicle.sensors.imu.gyro", Operation::Telemetry).unwrap();
assert_eq!(m.operation, Operation::Telemetry);
assert_eq!(m.payload, Payload::Unit);
assert_eq!(m.subject.as_str(), "vehicle.sensors.imu.gyro");
}
#[test]
fn message_new_rejects_invalid_subject() {
let r = Message::new("", Operation::Telemetry);
assert!(r.is_err());
}
#[test]
fn message_round_trips_json() {
let m = Message::new("vehicle.sensors.imu.gyro", Operation::Telemetry).unwrap();
let json = serde_json::to_string(&m).unwrap();
let back: Message = serde_json::from_str(&json).unwrap();
assert_eq!(m, back);
}
#[test]
fn query_round_trips_json() {
let q = Query::new(Resource::from_str("vehicle.sensors.imu.gyro").unwrap());
let json = serde_json::to_string(&q).unwrap();
let back: Query = serde_json::from_str(&json).unwrap();
assert_eq!(q, back);
}
#[test]
fn subject_round_trip_preserves_string() {
for s in [
"vehicle",
"vehicle.sensors",
"vehicle.sensors.imu.gyro",
"a.b.c.d.e.f.g",
] {
let subject = Subject::from_str(s).unwrap();
assert_eq!(subject.as_str(), s, "round-trip must preserve {s}");
}
}
#[test]
fn pattern_matches_self_concrete() {
let p = SubjectPattern::from_str("vehicle.sensors.imu.gyro").unwrap();
assert!(p.matches(&Subject::from_str("vehicle.sensors.imu.gyro").unwrap()));
assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu.accel").unwrap()));
assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu").unwrap()));
assert!(!p.matches(&Subject::from_str("vehicle.sensors.imu.gyro.extra").unwrap()));
}
#[test]
fn cache_key_stability_via_hash() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let s = Subject::from_str("vehicle.sensors.imu.gyro").unwrap();
let mut h1 = DefaultHasher::new();
s.hash(&mut h1);
let mut h2 = DefaultHasher::new();
s.hash(&mut h2);
assert_eq!(h1.finish(), h2.finish(), "hash must be stable");
}
}