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::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()
}),
}
}
}
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>,
}
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()),
}
}
}
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)),
}
}
}
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)
}
}
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);
}
}
#[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>();
}
}