use crate::graph::Graph;
use crate::predicate::BoxedPredicate;
use core::any::Any;
use hashbrown::{HashMap, HashSet};
use polaris_system::plugin::{IntoScheduleIds, ScheduleId};
use polaris_system::resource::LocalResource;
use polaris_system::system::{BoxedSystem, ErasedSystem, IntoSystem};
use std::any::TypeId;
use std::fmt;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NodeId(Arc<str>);
impl LocalResource for NodeId {}
impl NodeId {
#[must_use]
pub fn new() -> Self {
Self(nanoid::nanoid!(8).into())
}
#[must_use]
pub fn from_string(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for NodeId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "node_{}", self.0)
}
}
impl IntoIterator for NodeId {
type Item = NodeId;
type IntoIter = std::iter::Once<NodeId>;
fn into_iter(self) -> Self::IntoIter {
std::iter::once(self)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Node {
System(SystemNode),
Decision(DecisionNode),
Switch(SwitchNode),
Parallel(ParallelNode),
Loop(LoopNode),
Scope(ScopeNode),
}
impl Node {
#[must_use]
pub fn id(&self) -> NodeId {
match self {
Node::System(n) => n.id.clone(),
Node::Decision(n) => n.id.clone(),
Node::Switch(n) => n.id.clone(),
Node::Parallel(n) => n.id.clone(),
Node::Loop(n) => n.id.clone(),
Node::Scope(n) => n.id.clone(),
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Node::System(n) => n.name(),
Node::Decision(n) => n.name,
Node::Switch(n) => n.name,
Node::Parallel(n) => n.name,
Node::Loop(n) => n.name,
Node::Scope(n) => n.name,
}
}
}
#[derive(Debug, Clone)]
pub enum RetryPolicy {
Fixed {
max_retries: usize,
delay: Duration,
},
Exponential {
max_retries: usize,
initial_delay: Duration,
max_delay: Option<Duration>,
},
}
impl RetryPolicy {
#[must_use]
pub fn fixed(max_retries: usize, delay: Duration) -> Self {
RetryPolicy::Fixed { max_retries, delay }
}
#[must_use]
pub fn exponential(max_retries: usize, initial_delay: Duration) -> Self {
RetryPolicy::Exponential {
max_retries,
initial_delay,
max_delay: None,
}
}
#[must_use]
pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
if let RetryPolicy::Exponential {
max_delay: ref mut md,
..
} = self
{
*md = Some(max_delay);
}
self
}
#[must_use]
pub fn max_retries(&self) -> usize {
match self {
RetryPolicy::Fixed { max_retries, .. }
| RetryPolicy::Exponential { max_retries, .. } => *max_retries,
}
}
#[must_use]
pub fn delay_for_attempt(&self, attempt: usize) -> Duration {
match self {
RetryPolicy::Fixed { delay, .. } => *delay,
RetryPolicy::Exponential {
initial_delay,
max_delay,
..
} => {
let multiplier = 1u32.checked_shl(attempt as u32);
let delay = if let Some(m) = multiplier {
initial_delay.saturating_mul(m)
} else {
max_delay.unwrap_or(Duration::MAX)
};
if let Some(cap) = max_delay {
delay.min(*cap)
} else {
delay
}
}
}
}
}
pub struct SystemNode {
pub id: NodeId,
pub system: BoxedSystem,
pub timeout: Option<Duration>,
pub retry_policy: Option<RetryPolicy>,
pub schedules: Vec<ScheduleId>,
}
impl SystemNode {
#[must_use]
pub fn new<S: ErasedSystem>(system: S) -> Self {
Self {
id: NodeId::new(),
system: Box::new(system),
timeout: None,
retry_policy: None,
schedules: Vec::new(),
}
}
#[must_use]
pub fn new_boxed(system: BoxedSystem) -> Self {
Self {
id: NodeId::new(),
system,
timeout: None,
retry_policy: None,
schedules: Vec::new(),
}
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[must_use]
pub fn with_schedules(mut self, schedules: Vec<ScheduleId>) -> Self {
self.schedules = schedules;
self
}
#[must_use]
pub fn name(&self) -> &'static str {
self.system.name()
}
#[must_use]
pub fn output_type_id(&self) -> TypeId {
self.system.output_type_id()
}
#[must_use]
pub fn output_type_name(&self) -> &'static str {
self.system.output_type_name()
}
}
impl fmt::Debug for SystemNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SystemNode")
.field("id", &self.id)
.field("name", &self.name())
.field("output_type", &self.output_type_name())
.field("schedules", &self.schedules)
.finish()
}
}
pub struct DecisionNode {
pub id: NodeId,
pub name: &'static str,
pub predicate: Option<BoxedPredicate>,
pub true_branch: Option<NodeId>,
pub false_branch: Option<NodeId>,
}
impl DecisionNode {
#[must_use]
pub fn new(name: &'static str) -> Self {
Self {
id: NodeId::new(),
name,
predicate: None,
true_branch: None,
false_branch: None,
}
}
#[must_use]
pub fn with_predicate(name: &'static str, predicate: BoxedPredicate) -> Self {
Self {
id: NodeId::new(),
name,
predicate: Some(predicate),
true_branch: None,
false_branch: None,
}
}
}
impl fmt::Debug for DecisionNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DecisionNode")
.field("id", &self.id)
.field("name", &self.name)
.field("has_predicate", &self.predicate.is_some())
.field("true_branch", &self.true_branch)
.field("false_branch", &self.false_branch)
.finish()
}
}
pub struct SwitchNode {
pub id: NodeId,
pub name: &'static str,
pub discriminator: Option<crate::predicate::BoxedDiscriminator>,
pub cases: Vec<(&'static str, NodeId)>,
pub default: Option<NodeId>,
}
impl SwitchNode {
#[must_use]
pub fn new(name: &'static str) -> Self {
Self {
id: NodeId::new(),
name,
discriminator: None,
cases: Vec::new(),
default: None,
}
}
#[must_use]
pub fn with_discriminator(
name: &'static str,
discriminator: crate::predicate::BoxedDiscriminator,
) -> Self {
Self {
id: NodeId::new(),
name,
discriminator: Some(discriminator),
cases: Vec::new(),
default: None,
}
}
}
impl fmt::Debug for SwitchNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SwitchNode")
.field("id", &self.id)
.field("name", &self.name)
.field("has_discriminator", &self.discriminator.is_some())
.field("cases", &self.cases)
.field("default", &self.default)
.finish()
}
}
#[derive(Debug)]
pub struct ParallelNode {
pub id: NodeId,
pub name: &'static str,
pub branches: Vec<NodeId>,
}
impl ParallelNode {
#[must_use]
pub fn new(name: &'static str) -> Self {
Self {
id: NodeId::new(),
name,
branches: Vec::new(),
}
}
}
pub struct LoopNode {
pub id: NodeId,
pub name: &'static str,
pub termination: Option<BoxedPredicate>,
pub max_iterations: Option<usize>,
pub body_entry: Option<NodeId>,
}
impl LoopNode {
#[must_use]
pub fn new(name: &'static str) -> Self {
Self {
id: NodeId::new(),
name,
termination: None,
max_iterations: None,
body_entry: None,
}
}
#[must_use]
pub fn with_termination(name: &'static str, termination: BoxedPredicate) -> Self {
Self {
id: NodeId::new(),
name,
termination: Some(termination),
max_iterations: None,
body_entry: None,
}
}
#[must_use]
pub fn with_max_iterations(name: &'static str, max_iterations: usize) -> Self {
Self {
id: NodeId::new(),
name,
termination: None,
max_iterations: Some(max_iterations),
body_entry: None,
}
}
}
impl fmt::Debug for LoopNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LoopNode")
.field("id", &self.id)
.field("name", &self.name)
.field("has_termination", &self.termination.is_some())
.field("max_iterations", &self.max_iterations)
.field("body_entry", &self.body_entry)
.finish()
}
}
pub(crate) type CloneFn = fn(&dyn Any) -> Option<Box<dyn Any + Send + Sync>>;
#[derive(Clone)]
pub(crate) enum CrossingAction {
Share,
Forward(CloneFn),
Fork(CloneFn),
ForwardFresh,
}
impl PartialEq for CrossingAction {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(Self::Share, Self::Share)
| (Self::Forward(_), Self::Forward(_))
| (Self::Fork(_), Self::Fork(_))
| (Self::ForwardFresh, Self::ForwardFresh)
)
}
}
impl Eq for CrossingAction {}
impl fmt::Debug for CrossingAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Share => f.write_str("Share"),
Self::Forward(_) => f.write_str("Forward"),
Self::Fork(_) => f.write_str("Fork"),
Self::ForwardFresh => f.write_str("ForwardFresh"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResourceCrossing {
pub(crate) type_id: TypeId,
pub(crate) type_name: &'static str,
pub(crate) action: CrossingAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContextMode {
Shared,
Inherit,
Isolated,
}
impl fmt::Display for ContextMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContextMode::Shared => f.write_str("Shared"),
ContextMode::Inherit => f.write_str("Inherit"),
ContextMode::Isolated => f.write_str("Isolated"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextPolicy {
pub(crate) mode: ContextMode,
pub(crate) crossings: HashMap<TypeId, ResourceCrossing>,
pub(crate) excludes: HashSet<TypeId>,
pub(crate) share_rest: bool,
pub(crate) cached_parent_filter: Arc<polaris_system::param::ParentFilter>,
}
impl ContextPolicy {
#[must_use]
#[expect(
clippy::new_without_default,
reason = "ContextPolicy intentionally omits Default — `new()` and `shared()` make the boundary choice explicit"
)]
pub fn new() -> Self {
Self {
mode: ContextMode::Isolated,
crossings: HashMap::new(),
excludes: HashSet::new(),
share_rest: false,
cached_parent_filter: Arc::new(polaris_system::param::ParentFilter::allow_only([])),
}
}
#[must_use]
pub fn shared() -> Self {
Self {
mode: ContextMode::Shared,
crossings: HashMap::new(),
excludes: HashSet::new(),
share_rest: false,
cached_parent_filter: Arc::new(polaris_system::param::ParentFilter::allow_only([])),
}
}
#[must_use]
pub fn share<T: LocalResource>(mut self) -> Self {
self.assert_not_shared::<T>("share");
self.set_crossing::<T>(CrossingAction::Share);
self.refresh_parent_filter();
self
}
#[must_use]
pub fn forward<T: LocalResource + Clone>(mut self) -> Self {
self.assert_not_shared::<T>("forward");
self.set_crossing::<T>(CrossingAction::Forward(|any| {
Some(Box::new(any.downcast_ref::<T>()?.clone()))
}));
self.refresh_parent_filter();
self
}
#[must_use]
pub fn fork<T>(mut self) -> Self
where
T: polaris_system::resource::ForkStrategy,
{
self.assert_not_shared::<T>("fork");
self.set_crossing::<T>(CrossingAction::Fork(|any| {
Some(Box::new(any.downcast_ref::<T>()?.fork()))
}));
self.refresh_parent_filter();
self
}
#[must_use]
pub fn forward_fresh<T: LocalResource>(mut self) -> Self {
self.assert_not_shared::<T>("forward_fresh");
self.set_crossing::<T>(CrossingAction::ForwardFresh);
self.refresh_parent_filter();
self
}
#[must_use]
pub fn exclude<T: LocalResource>(mut self) -> Self {
self.assert_not_shared::<T>("exclude");
self.crossings.remove(&TypeId::of::<T>());
self.excludes.insert(TypeId::of::<T>());
self.refresh_parent_filter();
self
}
#[must_use]
pub fn share_rest(mut self) -> Self {
assert!(
!matches!(self.mode, ContextMode::Shared),
"share_rest() is not valid on ContextPolicy::shared() — \
use ContextPolicy::new() to compose per-resource verbs",
);
self.share_rest = true;
self.mode = ContextMode::Inherit;
self.refresh_parent_filter();
self
}
fn assert_not_shared<T: ?Sized>(&self, verb: &'static str) {
assert!(
!matches!(self.mode, ContextMode::Shared),
"{verb}::<{}>() is not valid on ContextPolicy::shared() — \
use ContextPolicy::new() to compose per-resource verbs",
core::any::type_name::<T>(),
);
}
fn set_crossing<T: 'static>(&mut self, action: CrossingAction) {
let type_id = TypeId::of::<T>();
self.excludes.remove(&type_id);
self.crossings.insert(
type_id,
ResourceCrossing {
type_id,
type_name: core::any::type_name::<T>(),
action,
},
);
}
fn refresh_parent_filter(&mut self) {
use polaris_system::param::ParentFilter;
self.cached_parent_filter = Arc::new(if self.share_rest {
ParentFilter::allow_all_except(self.excludes.iter().copied())
} else {
ParentFilter::allow_only(
self.crossings
.values()
.filter(|c| matches!(c.action, CrossingAction::Share))
.map(|c| c.type_id),
)
});
}
#[must_use]
pub fn mode(&self) -> ContextMode {
self.mode
}
#[must_use]
pub fn is_shared(&self) -> bool {
matches!(self.mode, ContextMode::Shared)
}
pub(crate) fn crossings(&self) -> impl Iterator<Item = &ResourceCrossing> {
self.crossings.values()
}
#[cfg(test)]
pub(crate) fn crossing_for(&self, type_id: TypeId) -> Option<&ResourceCrossing> {
self.crossings.get(&type_id)
}
#[cfg(test)]
pub(crate) fn parent_filter(&self) -> &polaris_system::param::ParentFilter {
&self.cached_parent_filter
}
pub(crate) fn parent_filter_arc(&self) -> Arc<polaris_system::param::ParentFilter> {
Arc::clone(&self.cached_parent_filter)
}
}
#[derive(Debug)]
pub struct ScopeNode {
pub id: NodeId,
pub name: &'static str,
pub(crate) graph: Graph,
pub(crate) context_policy: ContextPolicy,
}
impl ScopeNode {
#[must_use]
pub fn new(name: &'static str, graph: Graph, context_policy: ContextPolicy) -> Self {
Self {
id: NodeId::new(),
name,
graph,
context_policy,
}
}
#[must_use]
pub fn graph(&self) -> &Graph {
&self.graph
}
#[must_use]
pub fn context_policy(&self) -> &ContextPolicy {
&self.context_policy
}
}
pub trait IntoSystemNode<Marker> {
fn into_system_node(self) -> (BoxedSystem, Vec<ScheduleId>);
}
pub struct NodeMarker<M>(PhantomData<M>);
pub struct ScheduledNodeMarker<M>(PhantomData<M>);
impl<S, M> IntoSystemNode<NodeMarker<M>> for S
where
S: IntoSystem<M>,
S::System: 'static,
{
fn into_system_node(self) -> (BoxedSystem, Vec<ScheduleId>) {
(Box::new(self.into_system()), Vec::new())
}
}
impl<Sch, S, M> IntoSystemNode<ScheduledNodeMarker<M>> for (Sch, S)
where
Sch: IntoScheduleIds,
S: IntoSystem<M>,
S::System: 'static,
{
fn into_system_node(self) -> (BoxedSystem, Vec<ScheduleId>) {
(Box::new(self.1.into_system()), Sch::schedule_ids())
}
}
#[cfg(test)]
mod tests {
use super::*;
use polaris_system::plugin::Schedule;
use polaris_system::system::IntoSystem;
async fn test_system() -> String {
"hello".to_string()
}
async fn sys_fn() -> i32 {
42
}
#[test]
fn node_id_uniqueness() {
let id1 = NodeId::new();
let id2 = NodeId::new();
assert_ne!(id1, id2);
}
#[test]
fn system_node_creation() {
let system = test_system.into_system();
let node = SystemNode::new(system);
assert!(!node.id.as_str().is_empty());
assert!(node.name().contains("test_system"));
}
#[test]
fn node_enum_accessors() {
let system = Node::System(SystemNode::new(sys_fn.into_system()));
assert!(!system.id().as_str().is_empty());
assert!(system.name().contains("sys_fn"));
let decision = Node::Decision(DecisionNode::new("dec"));
assert!(!decision.id().as_str().is_empty());
assert_eq!(decision.name(), "dec");
}
#[test]
fn system_node_preserves_type_info() {
let system = sys_fn.into_system();
let node = SystemNode::new(system);
assert_eq!(node.output_type_id(), TypeId::of::<i32>());
assert!(node.output_type_name().contains("i32"));
}
#[test]
fn retry_policy_fixed_delay() {
let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
assert_eq!(policy.max_retries(), 3);
assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(100));
assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(100));
}
#[test]
fn retry_policy_exponential_delay() {
let policy = RetryPolicy::exponential(4, Duration::from_millis(100));
assert_eq!(policy.max_retries(), 4);
assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(200));
assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(400));
assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(800));
}
#[test]
fn retry_policy_exponential_with_max_delay() {
let policy = RetryPolicy::exponential(4, Duration::from_millis(100))
.with_max_delay(Duration::from_millis(300));
assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(200));
assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(300));
assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(300));
}
#[test]
fn retry_policy_with_max_delay_no_effect_on_fixed() {
let policy = RetryPolicy::fixed(2, Duration::from_millis(100))
.with_max_delay(Duration::from_millis(50));
assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
}
struct MarkerA;
impl Schedule for MarkerA {}
struct MarkerB;
impl Schedule for MarkerB {}
#[test]
fn into_system_node_bare() {
let (_, schedules) = sys_fn.into_system_node();
assert!(schedules.is_empty());
}
#[test]
fn into_system_node_single_schedule() {
let (_, schedules) = (MarkerA, sys_fn).into_system_node();
assert_eq!(schedules.len(), 1);
assert_eq!(schedules[0], ScheduleId::of::<MarkerA>());
}
#[test]
fn into_system_node_multi_schedules() {
let (_, schedules) = ((MarkerA, MarkerB), sys_fn).into_system_node();
assert_eq!(schedules.len(), 2);
assert_eq!(schedules[0], ScheduleId::of::<MarkerA>());
assert_eq!(schedules[1], ScheduleId::of::<MarkerB>());
}
#[test]
fn system_node_with_schedules() {
let node = SystemNode::new(sys_fn.into_system()).with_schedules(vec![
ScheduleId::of::<MarkerA>(),
ScheduleId::of::<MarkerB>(),
]);
assert_eq!(node.schedules.len(), 2);
assert_eq!(node.schedules[0], ScheduleId::of::<MarkerA>());
assert_eq!(node.schedules[1], ScheduleId::of::<MarkerB>());
}
use polaris_system::param::ParentFilter;
use polaris_system::resource::{ForkStrategy, LocalResource};
#[derive(Clone, Default)]
struct TestRes;
impl LocalResource for TestRes {}
#[derive(Default)]
struct ForkRes;
impl LocalResource for ForkRes {}
impl ForkStrategy for ForkRes {
fn fork(&self) -> Self {
ForkRes
}
}
#[test]
fn context_policy_shared_no_boundary() {
let policy = ContextPolicy::shared();
assert!(policy.is_shared());
assert_eq!(policy.mode(), ContextMode::Shared);
assert!(policy.crossings().next().is_none());
}
#[test]
fn context_policy_new_is_isolated_by_default() {
let policy = ContextPolicy::new();
assert!(!policy.is_shared());
assert_eq!(policy.mode(), ContextMode::Isolated);
assert_eq!(*policy.parent_filter(), ParentFilter::allow_only([]));
}
#[test]
fn share_rest_yields_child_with_allow_all() {
let policy = ContextPolicy::new().share_rest();
assert_eq!(policy.mode(), ContextMode::Inherit);
assert_eq!(*policy.parent_filter(), ParentFilter::allow_all_except([]));
}
#[test]
fn share_specific_type_yields_child_with_allow_only() {
let policy = ContextPolicy::new().share::<TestRes>();
assert_eq!(
*policy.parent_filter(),
ParentFilter::allow_only([TypeId::of::<TestRes>()])
);
}
#[test]
fn share_then_share_rest_keeps_share_and_yields_allow_all() {
let policy = ContextPolicy::new().share::<TestRes>().share_rest();
assert_eq!(policy.mode(), ContextMode::Inherit);
assert_eq!(*policy.parent_filter(), ParentFilter::allow_all_except([]));
let crossing = policy
.crossing_for(TypeId::of::<TestRes>())
.expect("share crossing should be retained");
assert!(matches!(crossing.action, CrossingAction::Share));
}
#[test]
fn forward_records_clone_strategy() {
let policy = ContextPolicy::new().forward::<TestRes>();
let crossing = policy.crossing_for(TypeId::of::<TestRes>()).unwrap();
assert!(matches!(crossing.action, CrossingAction::Forward(_)));
}
#[test]
fn fork_records_fork_strategy() {
let policy = ContextPolicy::new().fork::<ForkRes>();
let crossing = policy.crossing_for(TypeId::of::<ForkRes>()).unwrap();
assert!(matches!(crossing.action, CrossingAction::Fork(_)));
}
#[test]
fn forward_fresh_records_factory_strategy() {
let policy = ContextPolicy::new().forward_fresh::<TestRes>();
let crossing = policy.crossing_for(TypeId::of::<TestRes>()).unwrap();
assert!(matches!(crossing.action, CrossingAction::ForwardFresh));
}
#[test]
fn exclude_in_share_rest_filters_parent() {
let policy = ContextPolicy::new().share_rest().exclude::<TestRes>();
let filter = policy.parent_filter();
assert!(!filter.allows(TypeId::of::<TestRes>()));
struct Other;
assert!(filter.allows(TypeId::of::<Other>()));
}
#[test]
fn exclude_clears_prior_positive_verb() {
let policy = ContextPolicy::new().share::<TestRes>().exclude::<TestRes>();
assert!(policy.crossing_for(TypeId::of::<TestRes>()).is_none());
let policy_with_rest = ContextPolicy::new()
.share::<TestRes>()
.exclude::<TestRes>()
.share_rest();
let filter = policy_with_rest.parent_filter();
assert!(!filter.allows(TypeId::of::<TestRes>()));
}
#[test]
fn positive_verb_clears_prior_exclude() {
let policy = ContextPolicy::new().exclude::<TestRes>().share::<TestRes>();
let crossing = policy.crossing_for(TypeId::of::<TestRes>()).unwrap();
assert!(matches!(crossing.action, CrossingAction::Share));
assert_eq!(
*policy.parent_filter(),
ParentFilter::allow_only([TypeId::of::<TestRes>()])
);
}
#[test]
fn forward_clears_prior_exclude() {
let policy = ContextPolicy::new()
.exclude::<TestRes>()
.forward::<TestRes>();
let crossing = policy.crossing_for(TypeId::of::<TestRes>()).unwrap();
assert!(matches!(crossing.action, CrossingAction::Forward(_)));
assert!(!policy.excludes.contains(&TypeId::of::<TestRes>()));
assert_eq!(*policy.parent_filter(), ParentFilter::allow_only([]));
}
#[test]
fn later_verb_overrides_earlier_for_same_type() {
let policy = ContextPolicy::new().forward::<TestRes>().share::<TestRes>();
let crossing = policy.crossing_for(TypeId::of::<TestRes>()).unwrap();
assert!(matches!(crossing.action, CrossingAction::Share));
}
#[test]
fn context_mode_display_renders_each_variant() {
assert_eq!(ContextMode::Shared.to_string(), "Shared");
assert_eq!(ContextMode::Inherit.to_string(), "Inherit");
assert_eq!(ContextMode::Isolated.to_string(), "Isolated");
}
#[test]
fn equality_is_verb_shape_not_closure_identity() {
assert_eq!(
ContextPolicy::new().forward::<TestRes>(),
ContextPolicy::new().forward::<TestRes>(),
);
assert_eq!(
ContextPolicy::new().fork::<ForkRes>(),
ContextPolicy::new().fork::<ForkRes>(),
);
assert_eq!(
ContextPolicy::new().share::<TestRes>(),
ContextPolicy::new().share::<TestRes>(),
);
assert_ne!(
ContextPolicy::new().forward::<TestRes>(),
ContextPolicy::new().share::<TestRes>(),
);
#[derive(Clone, Default)]
struct OtherRes;
impl LocalResource for OtherRes {}
assert_ne!(
ContextPolicy::new().forward::<TestRes>(),
ContextPolicy::new().forward::<OtherRes>(),
);
}
#[test]
#[should_panic(expected = "is not valid on ContextPolicy::shared()")]
fn shared_plus_share_panics() {
let _ = ContextPolicy::shared().share::<TestRes>();
}
#[test]
#[should_panic(expected = "is not valid on ContextPolicy::shared()")]
fn shared_plus_forward_panics() {
let _ = ContextPolicy::shared().forward::<TestRes>();
}
#[test]
#[should_panic(expected = "is not valid on ContextPolicy::shared()")]
fn shared_plus_forward_fresh_panics() {
let _ = ContextPolicy::shared().forward_fresh::<TestRes>();
}
#[test]
#[should_panic(expected = "is not valid on ContextPolicy::shared()")]
fn shared_plus_fork_panics() {
let _ = ContextPolicy::shared().fork::<ForkRes>();
}
#[test]
#[should_panic(expected = "is not valid on ContextPolicy::shared()")]
fn shared_plus_exclude_panics() {
let _ = ContextPolicy::shared().exclude::<TestRes>();
}
#[test]
#[should_panic(expected = "is not valid on ContextPolicy::shared()")]
fn shared_plus_share_rest_panics() {
let _ = ContextPolicy::shared().share_rest();
}
#[test]
fn scope_node_accessors() {
let inner = Graph::new();
let scope = ScopeNode {
id: NodeId::new(),
name: "test_scope",
graph: inner,
context_policy: ContextPolicy::shared(),
};
let node = Node::Scope(scope);
assert_eq!(node.name(), "test_scope");
assert!(!node.id().as_str().is_empty());
}
}