use std::any::{TypeId, type_name};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::panic::AssertUnwindSafe;
use futures::{StreamExt, stream::BoxStream};
use crate::structural_key::{ScopePath, StructuralKey};
pub struct Subscription<Msg: 'static> {
pub(super) id: SubscriptionId,
pub(super) spawn: Box<dyn FnOnce() -> BoxStream<'static, Msg> + Send>,
}
impl<Msg: 'static> Subscription<Msg> {
#[must_use]
pub fn new<Source>(source: Source) -> Self
where
Source: SubscriptionSource<Output = Msg> + 'static,
{
let id = SubscriptionId::from_source::<Source>(source.key());
Self {
id,
spawn: Box::new(move || source.stream().boxed()),
}
}
#[must_use]
pub fn map<F, NewMsg>(self, f: F) -> Subscription<NewMsg>
where
F: Fn(Msg) -> NewMsg + Send + 'static,
Msg: 'static,
NewMsg: 'static,
{
let spawn = self.spawn;
Subscription {
id: self.id,
spawn: Box::new(move || {
let stream = spawn();
stream.map(f).boxed()
}),
}
}
#[must_use = "scoped returns a modified subscription and does not mutate in place"]
pub fn scoped<Scope>(mut self, scope: Scope) -> Self
where
Scope: Eq + Hash + Send + Sync + 'static,
{
self.id.scope = AssertUnwindSafe(self.id.scope.appended(scope));
self
}
pub(crate) const fn id(&self) -> &SubscriptionId {
&self.id
}
}
impl<A: SubscriptionSource<Output = Msg> + 'static, Msg> From<A> for Subscription<Msg> {
fn from(value: A) -> Self {
Self::new(value)
}
}
pub trait SubscriptionSource: Send {
type Output;
type Key: Eq + Hash + Send + Sync + 'static;
fn stream(&self) -> BoxStream<'static, Self::Output>;
fn key(&self) -> Self::Key;
}
pub struct SubscriptionId {
pub(super) source_type_id: TypeId,
source_type_name: &'static str,
key: AssertUnwindSafe<StructuralKey>,
scope: AssertUnwindSafe<ScopePath>,
}
impl Clone for SubscriptionId {
fn clone(&self) -> Self {
Self {
source_type_id: self.source_type_id,
source_type_name: self.source_type_name,
key: AssertUnwindSafe(self.key.0.clone()),
scope: AssertUnwindSafe(self.scope.0.clone()),
}
}
}
impl SubscriptionId {
fn from_source<Source>(key: Source::Key) -> Self
where
Source: SubscriptionSource + 'static,
{
Self {
source_type_id: TypeId::of::<Source>(),
source_type_name: type_name::<Source>(),
key: AssertUnwindSafe(StructuralKey::new(key)),
scope: AssertUnwindSafe(ScopePath::empty()),
}
}
}
impl fmt::Debug for SubscriptionId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SubscriptionId")
.field("source", &self.source_type_name)
.field("key", &self.key.0.type_name())
.finish_non_exhaustive()
}
}
impl PartialEq for SubscriptionId {
fn eq(&self, other: &Self) -> bool {
self.source_type_id == other.source_type_id
&& self.key.0.value_eq(&other.key.0)
&& self.scope.0 == other.scope.0
}
}
impl Eq for SubscriptionId {}
impl Hash for SubscriptionId {
fn hash<H: Hasher>(&self, state: &mut H) {
self.source_type_id.hash(state);
self.key.0.hash_value(state);
self.scope.0.hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::hash_map::DefaultHasher;
use std::panic::{RefUnwindSafe, UnwindSafe};
use futures::{StreamExt, stream};
struct SourceA(u8);
struct SourceB(u8);
impl SubscriptionSource for SourceA {
type Output = ();
type Key = u8;
fn stream(&self) -> BoxStream<'static, Self::Output> {
stream::empty().boxed()
}
fn key(&self) -> Self::Key {
self.0
}
}
impl SubscriptionSource for SourceB {
type Output = ();
type Key = u8;
fn stream(&self) -> BoxStream<'static, Self::Output> {
stream::empty().boxed()
}
fn key(&self) -> Self::Key {
self.0
}
}
#[derive(Eq, PartialEq)]
struct Collision(u8);
impl Hash for Collision {
fn hash<H: Hasher>(&self, state: &mut H) {
0_u8.hash(state);
}
}
struct CollisionSource(Collision);
impl SubscriptionSource for CollisionSource {
type Output = ();
type Key = Collision;
fn stream(&self) -> BoxStream<'static, Self::Output> {
stream::empty().boxed()
}
fn key(&self) -> Self::Key {
Collision(self.0.0)
}
}
fn hash(id: &SubscriptionId) -> u64 {
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}
#[test]
fn subscription_ids_are_structural_and_source_namespaced() {
let first = Subscription::new(SourceA(1)).id;
let equal = Subscription::new(SourceA(1)).id;
let different_key = Subscription::new(SourceA(2)).id;
let different_source = Subscription::new(SourceB(1)).id;
assert_eq!(first, equal);
assert_ne!(first, different_key);
assert_ne!(first, different_source);
assert_eq!(hash(&first), hash(&equal));
assert_eq!(first, first.clone());
}
#[test]
fn hash_collisions_do_not_make_subscription_ids_equal() {
let first = Subscription::new(CollisionSource(Collision(1))).id;
let second = Subscription::new(CollisionSource(Collision(2))).id;
assert_eq!(hash(&first), hash(&second));
assert_ne!(first, second);
}
#[test]
fn debug_only_reports_type_names() {
let first = format!("{:?}", Subscription::new(SourceA(1)).id);
let second = format!("{:?}", Subscription::new(SourceA(2)).id);
assert_eq!(first, second);
assert!(first.contains("SourceA"));
assert!(!first.contains('1'));
}
#[test]
fn subscription_id_preserves_marker_traits() {
fn assert_traits<T: Send + Sync + UnwindSafe + RefUnwindSafe>() {}
assert_traits::<SubscriptionId>();
}
#[test]
fn unscoped_tuple_local_key_does_not_alias_a_scoped_identity() {
struct TupleKeyedSource;
impl SubscriptionSource for TupleKeyedSource {
type Output = ();
type Key = (&'static str, u8);
fn stream(&self) -> BoxStream<'static, Self::Output> {
stream::empty().boxed()
}
fn key(&self) -> Self::Key {
("pane-1", 1)
}
}
let tupled = Subscription::new(TupleKeyedSource).id;
let scoped = Subscription::new(SourceA(1)).scoped("pane-1").id;
assert_ne!(tupled, scoped);
}
#[test]
fn scoped_makes_independent_child_instances_distinct() {
let first = Subscription::new(SourceA(1)).scoped("pane-1").id;
let second = Subscription::new(SourceA(1)).scoped("pane-2").id;
assert_ne!(first, second);
}
#[test]
fn scoped_with_equal_scope_and_local_key_is_equal() {
let first = Subscription::new(SourceA(1)).scoped("pane-1").id;
let second = Subscription::new(SourceA(1)).scoped("pane-1").id;
assert_eq!(first, second);
assert_eq!(hash(&first), hash(&second));
}
#[test]
fn scoped_differs_from_unscoped() {
let unscoped = Subscription::new(SourceA(1)).id;
let scoped = Subscription::new(SourceA(1)).scoped("pane-1").id;
assert_ne!(unscoped, scoped);
}
#[test]
fn scope_segment_type_differences_affect_equality() {
let as_str = Subscription::new(SourceA(1)).scoped("1").id;
let as_u32 = Subscription::new(SourceA(1)).scoped(1_u32).id;
assert_ne!(as_str, as_u32);
}
#[test]
fn reversing_two_unequal_scope_segments_changes_identity() {
let forward = Subscription::new(SourceA(1))
.scoped("inner")
.scoped("outer")
.id;
let backward = Subscription::new(SourceA(1))
.scoped("outer")
.scoped("inner")
.id;
assert_ne!(forward, backward);
}
#[test]
fn reversing_two_equal_scope_segments_preserves_identity() {
let forward = Subscription::new(SourceA(1))
.scoped("pane")
.scoped("pane")
.id;
let backward = Subscription::new(SourceA(1))
.scoped("pane")
.scoped("pane")
.id;
assert_eq!(forward, backward);
}
#[test]
fn map_and_scoped_placement_are_equivalent() {
let before = Subscription::new(SourceA(1))
.map(|()| 1)
.scoped("pane-1")
.id;
let after = Subscription::new(SourceA(1))
.scoped("pane-1")
.map(|()| 1)
.id;
assert_eq!(before, after);
}
#[test]
fn scope_hash_collisions_do_not_make_scoped_ids_equal() {
let first = Subscription::new(SourceA(1)).scoped(Collision(1)).id;
let second = Subscription::new(SourceA(1)).scoped(Collision(2)).id;
assert_eq!(hash(&first), hash(&second));
assert_ne!(first, second);
}
}