Skip to main content

qubit_event_bus/core/
topic.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Type-safe event topics.
11
12use std::any::{
13    TypeId,
14    type_name,
15};
16use std::fmt::{
17    self,
18    Display,
19    Formatter,
20};
21use std::hash::{
22    Hash,
23    Hasher,
24};
25use std::marker::PhantomData;
26
27use crate::{
28    EventBusError,
29    EventBusResult,
30    TopicKey,
31};
32
33/// Type-safe event topic.
34///
35/// `T` is the payload type associated with the topic. Two topics are equal only
36/// when both the topic name and payload type match.
37#[derive(Debug)]
38pub struct Topic<T: 'static> {
39    name: String,
40    payload_type_id: TypeId,
41    payload_type_name: &'static str,
42    marker: PhantomData<fn() -> T>,
43}
44
45impl<T: 'static> Topic<T> {
46    /// Creates a topic after validating its name.
47    ///
48    /// # Parameters
49    /// - `name`: Non-blank topic name.
50    ///
51    /// # Returns
52    /// A type-safe topic bound to `T`.
53    ///
54    /// # Errors
55    /// Returns [`EventBusError::InvalidArgument`] when `name` is blank.
56    pub fn try_new(name: impl Into<String>) -> EventBusResult<Self> {
57        let name = name.into();
58        if name.trim().is_empty() {
59            return Err(EventBusError::invalid_argument(
60                "name",
61                "topic name must not be blank",
62            ));
63        }
64        Ok(Self {
65            name,
66            payload_type_id: TypeId::of::<T>(),
67            payload_type_name: type_name::<T>(),
68            marker: PhantomData,
69        })
70    }
71
72    /// Returns the topic name.
73    ///
74    /// # Returns
75    /// The immutable topic name.
76    pub fn name(&self) -> &str {
77        &self.name
78    }
79
80    /// Returns the payload [`TypeId`].
81    ///
82    /// # Returns
83    /// Type identifier for payload `T`.
84    pub fn payload_type_id(&self) -> TypeId {
85        self.payload_type_id
86    }
87
88    /// Returns the Rust payload type name.
89    ///
90    /// # Returns
91    /// Fully qualified payload type name.
92    pub fn payload_type_name(&self) -> &'static str {
93        self.payload_type_name
94    }
95
96    /// Returns a type-erased key for internal maps.
97    ///
98    /// # Returns
99    /// A key containing the topic name and payload type.
100    pub fn key(&self) -> TopicKey {
101        TopicKey::new(self.name.clone(), self.payload_type_id)
102    }
103}
104
105impl<T: 'static> Clone for Topic<T> {
106    /// Clones the topic metadata without requiring `T: Clone`.
107    fn clone(&self) -> Self {
108        Self {
109            name: self.name.clone(),
110            payload_type_id: self.payload_type_id,
111            payload_type_name: self.payload_type_name,
112            marker: PhantomData,
113        }
114    }
115}
116
117impl<T: 'static> PartialEq for Topic<T> {
118    /// Compares topic name and payload type.
119    fn eq(&self, other: &Self) -> bool {
120        self.name == other.name && self.payload_type_id == other.payload_type_id
121    }
122}
123
124impl<T: 'static> Eq for Topic<T> {}
125
126impl<T: 'static> Hash for Topic<T> {
127    /// Hashes topic name and payload type.
128    fn hash<H: Hasher>(&self, state: &mut H) {
129        self.name.hash(state);
130        self.payload_type_id.hash(state);
131    }
132}
133
134impl<T: 'static> Display for Topic<T> {
135    /// Formats the topic as `<payload_type>.<topic_name>`.
136    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
137        write!(formatter, "{}.{}", self.payload_type_name, self.name)
138    }
139}