use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Default, Clone)]
pub struct Extensions {
map: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
}
impl Extensions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
self.map.insert(TypeId::of::<T>(), Arc::new(value));
}
#[must_use]
pub fn get<T: 'static>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|v| v.downcast_ref::<T>())
}
#[must_use]
pub fn contains<T: 'static>(&self) -> bool {
self.map.contains_key(&TypeId::of::<T>())
}
pub fn remove<T: Send + Sync + 'static>(&mut self) -> Option<Arc<T>> {
self.map
.remove(&TypeId::of::<T>())
.and_then(|v| Arc::downcast::<T>(v).ok())
}
}
impl std::fmt::Debug for Extensions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Extensions")
.field("count", &self.map.len())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestId(String);
impl RequestId {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl From<String> for RequestId {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for RequestId {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl AsRef<str> for RequestId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for RequestId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<RequestId> for String {
fn from(value: RequestId) -> Self {
value.into_inner()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceId(String);
impl TraceId {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl From<String> for TraceId {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for TraceId {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl AsRef<str> for TraceId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for TraceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<TraceId> for String {
fn from(value: TraceId) -> Self {
value.into_inner()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Principal(String);
impl Principal {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl From<String> for Principal {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for Principal {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl AsRef<str> for Principal {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for Principal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<Principal> for String {
fn from(value: Principal) -> Self {
value.into_inner()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tenant(String);
impl Tenant {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl From<String> for Tenant {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for Tenant {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl AsRef<str> for Tenant {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for Tenant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<Tenant> for String {
fn from(value: Tenant) -> Self {
value.into_inner()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scopes(Vec<String>);
impl Scopes {
#[must_use]
pub fn from_values<I, S>(iter: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self(iter.into_iter().map(Into::into).collect())
}
#[must_use]
pub fn as_slice(&self) -> &[String] {
&self.0
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.0.iter().map(String::as_str)
}
#[must_use]
pub fn into_vec(self) -> Vec<String> {
self.0
}
#[must_use]
pub fn contains(&self, scope: &str) -> bool {
self.0.iter().any(|candidate| candidate == scope)
}
}
impl<S> std::iter::FromIterator<S> for Scopes
where
S: Into<String>,
{
fn from_iter<T: IntoIterator<Item = S>>(iter: T) -> Self {
Self::from_values(iter)
}
}
impl From<Vec<String>> for Scopes {
fn from(value: Vec<String>) -> Self {
Self(value)
}
}
impl<const N: usize> From<[String; N]> for Scopes {
fn from(value: [String; N]) -> Self {
Self(value.into_iter().collect())
}
}
impl<const N: usize> From<[&str; N]> for Scopes {
fn from(value: [&str; N]) -> Self {
Self::from_values(value)
}
}
impl IntoIterator for Scopes {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl AsRef<[String]> for Scopes {
fn as_ref(&self) -> &[String] {
self.as_slice()
}
}
#[derive(Clone, Default)]
struct RequestContextData {
deadline: Option<Instant>,
extensions: Extensions,
transport: Option<Arc<dyn Any + Send + Sync>>,
}
impl RequestContextData {
fn new() -> Self {
Self::default()
}
fn with_deadline(deadline: Instant) -> Self {
Self {
deadline: Some(deadline),
extensions: Extensions::new(),
transport: None,
}
}
fn deadline(&self) -> Option<Instant> {
self.deadline
}
fn time_remaining(&self) -> Option<Duration> {
self.deadline
.and_then(|d| d.checked_duration_since(Instant::now()))
}
fn get<T: 'static>(&self) -> Option<&T> {
self.extensions.get::<T>()
}
fn insert_value<T: Send + Sync + 'static>(&mut self, value: T) {
self.extensions.insert(value);
}
fn extensions_mut(&mut self) -> &mut Extensions {
&mut self.extensions
}
fn extensions(&self) -> &Extensions {
&self.extensions
}
fn set_transport<T: Send + Sync + 'static>(&mut self, transport: T) {
self.transport = Some(Arc::new(transport));
}
fn has_transport(&self) -> bool {
self.transport.is_some()
}
fn transport_as<T: 'static>(&self) -> Option<&T> {
self.transport.as_ref().and_then(|t| t.downcast_ref::<T>())
}
fn with_shorter_deadline(&mut self, deadline: Instant) {
self.deadline = match self.deadline {
Some(existing) if existing < deadline => Some(existing),
Some(_) | None => Some(deadline),
};
}
}
#[derive(Clone, Default)]
struct RuntimeContextState {
cancellation: Arc<Mutex<SharedCancellationState>>,
}
impl RuntimeContextState {
fn cancel(&self, deadline: Option<Instant>) {
self.cancel_with_reason(deadline, DoneReason::Cancelled);
}
fn cancel_with_reason(&self, deadline: Option<Instant>, reason: DoneReason) {
self.set_done_reason(deadline, reason);
}
fn set_done_reason(&self, deadline: Option<Instant>, reason: DoneReason) {
let mut guard = match self.cancellation.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if guard.reason.is_some() {
return;
}
if let Some(d) = deadline
&& Instant::now() >= d
{
guard.reason = Some(DoneReason::DeadlineExceeded);
return;
}
guard.reason = Some(reason);
}
fn is_done(&self, deadline: Option<Instant>) -> bool {
self.done_reason(deadline).is_some()
}
fn done_reason(&self, deadline: Option<Instant>) -> Option<DoneReason> {
let mut guard = match self.cancellation.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if let Some(reason) = guard.reason {
return Some(reason);
}
match deadline {
Some(d) if Instant::now() >= d => {
guard.reason = Some(DoneReason::DeadlineExceeded);
guard.reason
}
Some(_) | None => None,
}
}
fn is_cancelled(&self) -> bool {
self.explicit_done_reason().is_some()
}
fn explicit_done_reason(&self) -> Option<DoneReason> {
let guard = match self.cancellation.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.reason
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct SharedCancellationState {
reason: Option<DoneReason>,
}
#[derive(Clone)]
pub struct CancellationHandle {
runtime: RuntimeContextState,
deadline: Option<Instant>,
}
impl CancellationHandle {
pub fn cancel(&self) {
self.runtime.cancel(self.deadline);
}
pub fn cancel_with_reason(&self, reason: DoneReason) {
self.runtime.cancel_with_reason(self.deadline, reason);
}
#[must_use]
pub fn done_reason(&self) -> Option<DoneReason> {
self.runtime.done_reason(self.deadline)
}
#[must_use]
pub fn is_done(&self) -> bool {
self.runtime.is_done(self.deadline)
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
matches!(
self.runtime.explicit_done_reason(),
Some(DoneReason::Cancelled | DoneReason::Shutdown)
)
}
#[must_use]
pub fn deadline_exceeded(&self) -> bool {
matches!(self.done_reason(), Some(DoneReason::DeadlineExceeded))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DoneReason {
Cancelled,
DeadlineExceeded,
Shutdown,
}
#[derive(Clone)]
pub struct Context {
request: RequestContextData,
runtime: RuntimeContextState,
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
#[must_use]
pub fn new() -> Self {
Self {
request: RequestContextData::new(),
runtime: RuntimeContextState::default(),
}
}
#[must_use]
pub fn with_deadline(deadline: Instant) -> Self {
Self {
request: RequestContextData::with_deadline(deadline),
runtime: RuntimeContextState::default(),
}
}
#[must_use]
pub fn with_timeout(timeout: Duration) -> Self {
Self::with_deadline(Instant::now() + timeout)
}
#[must_use]
pub fn deadline(&self) -> Option<Instant> {
self.request.deadline()
}
#[must_use]
pub fn time_remaining(&self) -> Option<Duration> {
self.request.time_remaining()
}
#[must_use]
pub fn is_done(&self) -> bool {
self.runtime.is_done(self.request.deadline())
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
matches!(
self.runtime.explicit_done_reason(),
Some(DoneReason::Cancelled | DoneReason::Shutdown)
)
}
#[must_use]
pub fn done_reason(&self) -> Option<DoneReason> {
self.runtime.done_reason(self.request.deadline())
}
#[must_use]
pub fn cancellation_handle(&self) -> CancellationHandle {
CancellationHandle {
runtime: self.runtime.clone(),
deadline: self.request.deadline(),
}
}
#[must_use]
pub fn deadline_exceeded(&self) -> bool {
matches!(self.done_reason(), Some(DoneReason::DeadlineExceeded))
}
#[must_use]
pub fn get<T: 'static>(&self) -> Option<&T> {
self.request.get::<T>()
}
#[must_use]
pub fn with_value<T: Send + Sync + 'static>(mut self, value: T) -> Self {
self.request.insert_value(value);
self
}
#[must_use]
pub fn with_request_id(self, request_id: impl Into<RequestId>) -> Self {
self.with_value(request_id.into())
}
#[must_use]
pub fn request_id(&self) -> Option<&str> {
self.get::<RequestId>().map(RequestId::as_str)
}
#[must_use]
pub fn request_id_value(&self) -> Option<&RequestId> {
self.get::<RequestId>()
}
#[must_use]
pub fn with_trace_id(self, trace_id: impl Into<TraceId>) -> Self {
self.with_value(trace_id.into())
}
#[must_use]
pub fn trace_id(&self) -> Option<&str> {
self.get::<TraceId>().map(TraceId::as_str)
}
#[must_use]
pub fn trace_id_value(&self) -> Option<&TraceId> {
self.get::<TraceId>()
}
#[must_use]
pub fn with_principal(self, principal: impl Into<Principal>) -> Self {
self.with_value(principal.into())
}
#[must_use]
pub fn principal(&self) -> Option<&str> {
self.get::<Principal>().map(Principal::as_str)
}
#[must_use]
pub fn principal_value(&self) -> Option<&Principal> {
self.get::<Principal>()
}
#[must_use]
pub fn with_tenant(self, tenant: impl Into<Tenant>) -> Self {
self.with_value(tenant.into())
}
#[must_use]
pub fn tenant(&self) -> Option<&str> {
self.get::<Tenant>().map(Tenant::as_str)
}
#[must_use]
pub fn tenant_value(&self) -> Option<&Tenant> {
self.get::<Tenant>()
}
#[must_use]
pub fn with_scopes<I, S>(self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.with_value(Scopes::from_values(scopes))
}
#[must_use]
pub fn scopes(&self) -> Option<&[String]> {
self.get::<Scopes>().map(Scopes::as_slice)
}
#[must_use]
pub fn scopes_value(&self) -> Option<&Scopes> {
self.get::<Scopes>()
}
#[must_use]
pub fn has_scope(&self, scope: &str) -> bool {
self.scopes_value()
.is_some_and(|scopes| scopes.contains(scope))
}
pub fn extensions_mut(&mut self) -> &mut Extensions {
self.request.extensions_mut()
}
#[must_use]
pub fn extensions(&self) -> &Extensions {
self.request.extensions()
}
#[must_use]
pub fn with_transport<T: Send + Sync + 'static>(mut self, transport: T) -> Self {
self.request.set_transport(transport);
self
}
#[must_use]
pub fn has_transport(&self) -> bool {
self.request.has_transport()
}
pub fn cancel(&mut self) {
self.runtime.cancel(self.request.deadline());
}
pub fn cancel_with_reason(&mut self, reason: DoneReason) {
self.runtime
.cancel_with_reason(self.request.deadline(), reason);
}
#[must_use]
pub fn with_shorter_deadline(mut self, deadline: Instant) -> Self {
self.request.with_shorter_deadline(deadline);
self
}
#[must_use]
pub fn with_shorter_timeout(self, timeout: Duration) -> Self {
self.with_shorter_deadline(Instant::now() + timeout)
}
#[must_use]
pub fn child_with_deadline(&self, deadline: Instant) -> Self {
self.clone().with_shorter_deadline(deadline)
}
#[must_use]
pub fn child_with_timeout(&self, timeout: Duration) -> Self {
self.child_with_deadline(Instant::now() + timeout)
}
}
impl std::fmt::Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context")
.field("deadline", &self.request.deadline())
.field("extensions", &self.request.extensions())
.field("has_transport", &self.request.has_transport())
.field("cancelled", &self.runtime.is_cancelled())
.field("done_reason", &self.done_reason())
.finish()
}
}
pub trait TransportAs {
fn transport_as<T: 'static>(&self) -> Option<&T>;
}
impl TransportAs for Context {
fn transport_as<T: 'static>(&self) -> Option<&T> {
self.request.transport_as::<T>()
}
}
#[derive(Default)]
pub struct ContextBuilder {
request: RequestContextData,
}
impl ContextBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_deadline(mut self, deadline: Instant) -> Self {
self.request.deadline = Some(deadline);
self
}
#[must_use]
pub fn with_timeout(self, timeout: Duration) -> Self {
self.with_deadline(Instant::now() + timeout)
}
#[must_use]
pub fn with_value<T: Send + Sync + 'static>(mut self, value: T) -> Self {
self.request.insert_value(value);
self
}
#[must_use]
pub fn with_request_id(self, request_id: impl Into<RequestId>) -> Self {
self.with_value(request_id.into())
}
#[must_use]
pub fn with_trace_id(self, trace_id: impl Into<TraceId>) -> Self {
self.with_value(trace_id.into())
}
#[must_use]
pub fn with_principal(self, principal: impl Into<Principal>) -> Self {
self.with_value(principal.into())
}
#[must_use]
pub fn with_tenant(self, tenant: impl Into<Tenant>) -> Self {
self.with_value(tenant.into())
}
#[must_use]
pub fn with_scopes<I, S>(self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.with_value(Scopes::from_values(scopes))
}
#[must_use]
pub fn with_transport<T: Send + Sync + 'static>(mut self, transport: T) -> Self {
self.request.set_transport(transport);
self
}
#[must_use]
pub fn build(self) -> Context {
Context {
request: self.request,
runtime: RuntimeContextState::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extensions_insert_and_get() {
let mut ext = Extensions::new();
ext.insert(42u32);
ext.insert("hello".to_string());
assert_eq!(ext.get::<u32>(), Some(&42));
assert_eq!(ext.get::<String>(), Some(&"hello".to_string()));
assert!(ext.get::<i64>().is_none());
}
#[test]
fn extensions_contains() {
let mut ext = Extensions::new();
ext.insert(42u32);
assert!(ext.contains::<u32>());
assert!(!ext.contains::<String>());
}
#[test]
fn context_deadline() {
let deadline = Instant::now() + Duration::from_secs(60);
let ctx = Context::with_deadline(deadline);
assert_eq!(ctx.deadline(), Some(deadline));
assert!(!ctx.is_done());
}
#[test]
fn context_timeout() {
let ctx = Context::with_timeout(Duration::from_secs(60));
assert!(ctx.deadline().is_some());
assert!(ctx.time_remaining().is_some());
assert!(!ctx.is_done());
}
#[test]
fn context_expired() {
let ctx = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
assert!(ctx.is_done());
assert_eq!(ctx.done_reason(), Some(DoneReason::DeadlineExceeded));
}
#[test]
fn context_cancelled() {
let mut ctx = Context::new();
assert!(!ctx.is_done());
ctx.cancel();
assert!(ctx.is_done());
assert_eq!(ctx.done_reason(), Some(DoneReason::Cancelled));
}
#[test]
fn context_with_value() {
let ctx = Context::new()
.with_value(42u32)
.with_value("test".to_string());
assert_eq!(ctx.get::<u32>(), Some(&42));
assert_eq!(ctx.get::<String>(), Some(&"test".to_string()));
}
#[test]
fn context_transport() {
struct TestTransport {
id: u32,
}
let ctx = Context::new().with_transport(TestTransport { id: 123 });
assert!(ctx.has_transport());
let transport = ctx.transport_as::<TestTransport>();
assert!(transport.is_some());
assert_eq!(transport.map(|t| t.id), Some(123));
}
#[test]
fn context_shorter_deadline() {
let far = Instant::now() + Duration::from_secs(60);
let near = Instant::now() + Duration::from_secs(10);
let ctx = Context::with_deadline(far);
let child = ctx.clone().with_shorter_deadline(near);
assert_eq!(ctx.deadline(), Some(far));
assert_eq!(child.deadline(), Some(near));
let extended = Context::with_deadline(near).with_shorter_deadline(far);
assert_eq!(extended.deadline(), Some(near));
}
#[test]
fn context_builder() {
let ctx = ContextBuilder::new()
.with_timeout(Duration::from_secs(30))
.with_value(100i32)
.build();
assert!(ctx.deadline().is_some());
assert_eq!(ctx.get::<i32>(), Some(&100));
}
#[test]
fn context_cancel_does_not_drop_request_facts() {
#[derive(Debug, PartialEq, Eq)]
struct RequestInfo {
request_id: &'static str,
}
#[derive(Debug, PartialEq, Eq)]
struct TransportData {
path: &'static str,
}
let deadline = Instant::now() + Duration::from_secs(60);
let mut ctx = Context::with_deadline(deadline)
.with_value(RequestInfo {
request_id: "req-001",
})
.with_transport(TransportData {
path: "/tasks/create",
});
ctx.cancel();
assert!(ctx.is_done());
assert_eq!(ctx.deadline(), Some(deadline));
assert_eq!(
ctx.get::<RequestInfo>(),
Some(&RequestInfo {
request_id: "req-001"
})
);
assert_eq!(
ctx.transport_as::<TransportData>(),
Some(&TransportData {
path: "/tasks/create"
})
);
}
#[test]
fn context_clone_runtime_state_is_shared() {
#[derive(Debug, PartialEq, Eq)]
struct RequestInfo {
request_id: &'static str,
}
let ctx = Context::new().with_value(RequestInfo {
request_id: "req-xyz",
});
let mut cloned = ctx.clone();
cloned.cancel();
assert!(ctx.is_done());
assert!(cloned.is_done());
assert_eq!(ctx.done_reason(), Some(DoneReason::Cancelled));
assert_eq!(cloned.done_reason(), Some(DoneReason::Cancelled));
assert_eq!(
ctx.get::<RequestInfo>(),
Some(&RequestInfo {
request_id: "req-xyz"
})
);
assert_eq!(
cloned.get::<RequestInfo>(),
Some(&RequestInfo {
request_id: "req-xyz"
})
);
}
#[test]
fn context_cancel_with_shutdown_reason() {
let mut ctx = Context::new();
ctx.cancel_with_reason(DoneReason::Shutdown);
assert!(ctx.is_done());
assert_eq!(ctx.done_reason(), Some(DoneReason::Shutdown));
}
#[test]
fn context_deadline_reason_wins_if_cancelled_after_expiry() {
let mut ctx = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
ctx.cancel();
assert_eq!(ctx.done_reason(), Some(DoneReason::DeadlineExceeded));
}
#[test]
fn context_first_terminal_reason_wins() {
let mut ctx = Context::new();
ctx.cancel_with_reason(DoneReason::Shutdown);
ctx.cancel();
assert_eq!(ctx.done_reason(), Some(DoneReason::Shutdown));
}
#[test]
fn context_deadline_reason_persists_across_clones_once_observed() {
let ctx = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
let reason = ctx.done_reason();
let clone = ctx.clone();
assert_eq!(reason, Some(DoneReason::DeadlineExceeded));
assert_eq!(clone.done_reason(), Some(DoneReason::DeadlineExceeded));
}
#[test]
fn context_child_with_deadline_uses_earliest_deadline() {
let far = Instant::now() + Duration::from_secs(60);
let near = Instant::now() + Duration::from_secs(10);
let parent = Context::with_deadline(far);
let child = parent.child_with_deadline(near);
let child_attempt_extend = parent.child_with_deadline(far + Duration::from_secs(60));
assert_eq!(parent.deadline(), Some(far));
assert_eq!(child.deadline(), Some(near));
assert_eq!(child_attempt_extend.deadline(), Some(far));
}
#[test]
fn context_child_with_timeout_shares_cancellation_state() {
let mut parent = Context::with_timeout(Duration::from_secs(60));
let child = parent.child_with_timeout(Duration::from_secs(5));
parent.cancel_with_reason(DoneReason::Shutdown);
assert!(child.is_done());
assert_eq!(child.done_reason(), Some(DoneReason::Shutdown));
}
#[test]
fn context_deadline_exceeded_query_distinguishes_done_reasons() {
let mut cancelled = Context::new();
cancelled.cancel();
assert!(!cancelled.deadline_exceeded());
let expired = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
assert!(expired.deadline_exceeded());
}
#[test]
fn context_is_cancelled_true_for_explicit_cancel_and_shutdown() {
let mut cancelled = Context::new();
cancelled.cancel();
assert!(cancelled.is_cancelled());
let mut shutdown = Context::new();
shutdown.cancel_with_reason(DoneReason::Shutdown);
assert!(shutdown.is_cancelled());
}
#[test]
fn context_is_cancelled_false_for_deadline_only() {
let expired = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
assert!(expired.is_done());
assert_eq!(expired.done_reason(), Some(DoneReason::DeadlineExceeded));
assert!(!expired.is_cancelled());
}
#[test]
fn cancellation_handle_propagates_done_reason_across_context_clones() {
let ctx = Context::new();
let cloned = ctx.clone();
let handle = ctx.cancellation_handle();
handle.cancel_with_reason(DoneReason::Shutdown);
assert_eq!(ctx.done_reason(), Some(DoneReason::Shutdown));
assert_eq!(cloned.done_reason(), Some(DoneReason::Shutdown));
assert_eq!(handle.done_reason(), Some(DoneReason::Shutdown));
}
#[test]
fn cancellation_handle_respects_deadline_expiry_when_cancelling() {
let ctx = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
let handle = ctx.cancellation_handle();
handle.cancel();
assert_eq!(ctx.done_reason(), Some(DoneReason::DeadlineExceeded));
}
#[test]
fn cancellation_handle_state_queries_distinguish_cancel_vs_deadline() {
let mut cancelled = Context::new();
cancelled.cancel_with_reason(DoneReason::Shutdown);
let cancelled_handle = cancelled.cancellation_handle();
assert!(cancelled_handle.is_done());
assert!(cancelled_handle.is_cancelled());
assert!(!cancelled_handle.deadline_exceeded());
let expired = Context::with_timeout(Duration::from_nanos(1));
std::thread::sleep(Duration::from_millis(1));
let expired_handle = expired.cancellation_handle();
assert!(expired_handle.is_done());
assert!(!expired_handle.is_cancelled());
assert!(expired_handle.deadline_exceeded());
}
#[test]
fn context_typed_metadata_accessors() {
let ctx = Context::new()
.with_request_id("req-123")
.with_trace_id("trace-abc")
.with_principal("user-42")
.with_tenant("tenant-7")
.with_scopes(["tasks:read", "tasks:write"]);
assert_eq!(ctx.request_id(), Some("req-123"));
assert_eq!(ctx.trace_id(), Some("trace-abc"));
assert_eq!(ctx.principal(), Some("user-42"));
assert_eq!(ctx.tenant(), Some("tenant-7"));
assert_eq!(
ctx.request_id_value().map(RequestId::as_str),
Some("req-123")
);
assert_eq!(ctx.trace_id_value().map(TraceId::as_str), Some("trace-abc"));
assert_eq!(
ctx.principal_value().map(Principal::as_str),
Some("user-42")
);
assert_eq!(ctx.tenant_value().map(Tenant::as_str), Some("tenant-7"));
assert_eq!(
ctx.scopes().map(<[String]>::to_vec),
Some(vec!["tasks:read".to_string(), "tasks:write".to_string()])
);
}
#[test]
fn context_builder_typed_metadata_accessors() {
let ctx = ContextBuilder::new()
.with_request_id("req-b-123")
.with_trace_id("trace-b-abc")
.with_principal("svc-batch")
.with_tenant("tenant-b")
.with_scopes(["jobs:run"])
.build();
assert_eq!(ctx.request_id(), Some("req-b-123"));
assert_eq!(ctx.trace_id(), Some("trace-b-abc"));
assert_eq!(ctx.principal(), Some("svc-batch"));
assert_eq!(ctx.tenant(), Some("tenant-b"));
assert_eq!(
ctx.scopes().map(<[String]>::to_vec),
Some(vec!["jobs:run".to_string()])
);
}
#[test]
fn context_accepts_typed_metadata_wrappers_directly() {
let ctx = Context::new()
.with_request_id(RequestId::new("req-w-1"))
.with_trace_id(TraceId::new("trace-w-1"))
.with_principal(Principal::new("user-w-1"))
.with_tenant(Tenant::new("tenant-w-1"))
.with_scopes(Scopes::from_values(["tasks:read", "tasks:write"]));
assert_eq!(ctx.request_id(), Some("req-w-1"));
assert_eq!(ctx.trace_id(), Some("trace-w-1"));
assert_eq!(ctx.principal(), Some("user-w-1"));
assert_eq!(ctx.tenant(), Some("tenant-w-1"));
assert!(ctx.has_scope("tasks:read"));
assert!(ctx.has_scope("tasks:write"));
}
#[test]
fn context_builder_accepts_typed_metadata_wrappers_directly() {
let scopes = Scopes::from_values(["jobs:run", "jobs:retry"]);
let ctx = ContextBuilder::new()
.with_request_id(RequestId::new("req-bw-1"))
.with_trace_id(TraceId::new("trace-bw-1"))
.with_principal(Principal::new("svc-bw-1"))
.with_tenant(Tenant::new("tenant-bw-1"))
.with_scopes(scopes.clone())
.build();
assert_eq!(ctx.request_id(), Some("req-bw-1"));
assert_eq!(ctx.trace_id(), Some("trace-bw-1"));
assert_eq!(ctx.principal(), Some("svc-bw-1"));
assert_eq!(ctx.tenant(), Some("tenant-bw-1"));
assert!(ctx.has_scope("jobs:run"));
assert!(ctx.has_scope("jobs:retry"));
}
#[test]
fn typed_metadata_wrappers_support_from_as_ref_and_display() {
let request_id = RequestId::from("req-1");
let trace_id = TraceId::from(String::from("trace-1"));
let principal = Principal::from("user-1");
let tenant = Tenant::from(String::from("tenant-1"));
assert_eq!(request_id.as_ref(), "req-1");
assert_eq!(trace_id.as_ref(), "trace-1");
assert_eq!(principal.as_ref(), "user-1");
assert_eq!(tenant.as_ref(), "tenant-1");
assert_eq!(request_id.to_string(), "req-1");
assert_eq!(trace_id.to_string(), "trace-1");
assert_eq!(principal.to_string(), "user-1");
assert_eq!(tenant.to_string(), "tenant-1");
}
#[test]
fn typed_metadata_wrappers_support_into_inner() {
assert_eq!(RequestId::new("req-x").into_inner(), "req-x".to_string());
assert_eq!(TraceId::new("trace-x").into_inner(), "trace-x".to_string());
assert_eq!(Principal::new("user-x").into_inner(), "user-x".to_string());
assert_eq!(Tenant::new("tenant-x").into_inner(), "tenant-x".to_string());
}
#[test]
fn typed_metadata_wrappers_support_into_string() {
let request_id: String = RequestId::new("req-y").into();
let trace_id: String = TraceId::new("trace-y").into();
let principal: String = Principal::new("user-y").into();
let tenant: String = Tenant::new("tenant-y").into();
assert_eq!(request_id, "req-y");
assert_eq!(trace_id, "trace-y");
assert_eq!(principal, "user-y");
assert_eq!(tenant, "tenant-y");
}
#[test]
fn scopes_support_from_iterator() {
let scopes: Scopes = ["tasks:read", "tasks:write"].into_iter().collect();
assert_eq!(
scopes.as_slice(),
["tasks:read".to_string(), "tasks:write".to_string()]
);
assert!(scopes.contains("tasks:read"));
assert!(!scopes.contains("tasks:delete"));
}
#[test]
fn scopes_iter_and_into_vec_are_zero_copy_helpers() {
let scopes = Scopes::from_values(["tasks:read", "tasks:write"]);
let iterated = scopes.iter().collect::<Vec<_>>();
assert_eq!(iterated, vec!["tasks:read", "tasks:write"]);
let owned = scopes.into_vec();
assert_eq!(
owned,
vec!["tasks:read".to_string(), "tasks:write".to_string()]
);
}
#[test]
fn scopes_support_from_conversions() {
let from_vec = Scopes::from(vec!["tasks:read".to_string(), "tasks:write".to_string()]);
assert_eq!(from_vec.as_slice(), ["tasks:read", "tasks:write"]);
let from_str_array = Scopes::from(["jobs:run", "jobs:retry"]);
assert_eq!(from_str_array.as_slice(), ["jobs:run", "jobs:retry"]);
let from_string_array =
Scopes::from([String::from("admin:read"), String::from("admin:write")]);
assert_eq!(from_string_array.as_slice(), ["admin:read", "admin:write"]);
}
#[test]
fn context_has_scope_uses_typed_scopes_metadata() {
let ctx = Context::new().with_scopes(["tasks:read", "tasks:write"]);
assert!(ctx.has_scope("tasks:read"));
assert!(!ctx.has_scope("tasks:delete"));
let iterated = ctx
.scopes_value()
.map(|value| value.iter().collect::<Vec<_>>())
.unwrap_or_default();
assert_eq!(iterated, vec!["tasks:read", "tasks:write"]);
let empty = Context::new();
assert!(!empty.has_scope("tasks:read"));
assert!(empty.scopes_value().is_none());
assert!(empty.request_id_value().is_none());
assert!(empty.trace_id_value().is_none());
assert!(empty.principal_value().is_none());
assert!(empty.tenant_value().is_none());
}
}