Skip to main content

axon/buffer/
kind.rs

1//! [`BufferKind`] — content-kind tags for multimodal buffers.
2//!
3//! Unlike the closed catalogues in v1.4.0 (trust proofs,
4//! backpressure policies), the `BufferKind` registry is **open**.
5//! Adopters register domain-specific kinds at startup:
6//!
7//! ```
8//! # use axon::buffer::BufferKind;
9//! let my_kind = BufferKind::new("siemens_dicom");
10//! assert_eq!(my_kind.slug(), "siemens_dicom");
11//! ```
12//!
13//! The registry seeds with a conservative list of common kinds so
14//! flows that just need "pcm16 at 16kHz" don't have to register
15//! anything:
16//!
17//! | Slug       | Typical source                              |
18//! |------------|---------------------------------------------|
19//! | `raw`      | Untagged bytes (default for new buffers)    |
20//! | `pcm16`    | 16-bit signed PCM audio                     |
21//! | `mulaw8`   | 8-bit μ-law telephony audio                 |
22//! | `wav`      | WAV container (header + PCM)                |
23//! | `mp3`      | MPEG-1/2 Audio Layer III                    |
24//! | `opus`     | Opus (WebRTC / Discord / Zoom)              |
25//! | `jpeg`     | Baseline JPEG image                         |
26//! | `png`      | PNG image                                   |
27//! | `webp`     | WebP image                                  |
28//! | `mp4`      | MPEG-4 container (video)                    |
29//! | `webm`     | WebM container                              |
30//! | `pdf`      | Portable Document Format                    |
31//! | `json`     | UTF-8 encoded JSON                          |
32//! | `csv`      | UTF-8 encoded CSV                           |
33
34use std::sync::{Arc, RwLock};
35
36/// Interned content-kind tag. Two kinds are equal when their slug
37/// string matches (case-sensitive). Construction via
38/// [`BufferKind::new`] is cheap — the registry de-duplicates
39/// identical slugs to a single `Arc<str>`.
40#[derive(Debug, Clone)]
41pub struct BufferKind {
42    slug: Arc<str>,
43}
44
45impl BufferKind {
46    /// Construct (or reuse) a kind from a slug. Registers the kind
47    /// in the global [`BufferKindRegistry`] so observability tooling
48    /// can enumerate every kind currently in use.
49    pub fn new(slug: impl Into<String>) -> Self {
50        let slug = slug.into();
51        let arc = BufferKindRegistry::global().intern(&slug);
52        BufferKind { slug: arc }
53    }
54
55    /// Slug lookup — stable, case-sensitive string.
56    pub fn slug(&self) -> &str {
57        &self.slug
58    }
59
60    // ── Seeded kinds ────────────────────────────────────────────
61
62    pub fn raw() -> Self {
63        Self::new("raw")
64    }
65    pub fn pcm16() -> Self {
66        Self::new("pcm16")
67    }
68    pub fn mulaw8() -> Self {
69        Self::new("mulaw8")
70    }
71    pub fn wav() -> Self {
72        Self::new("wav")
73    }
74    pub fn mp3() -> Self {
75        Self::new("mp3")
76    }
77    pub fn opus() -> Self {
78        Self::new("opus")
79    }
80    pub fn jpeg() -> Self {
81        Self::new("jpeg")
82    }
83    pub fn png() -> Self {
84        Self::new("png")
85    }
86    pub fn webp() -> Self {
87        Self::new("webp")
88    }
89    pub fn mp4() -> Self {
90        Self::new("mp4")
91    }
92    pub fn webm() -> Self {
93        Self::new("webm")
94    }
95    pub fn pdf() -> Self {
96        Self::new("pdf")
97    }
98    pub fn json() -> Self {
99        Self::new("json")
100    }
101    pub fn csv() -> Self {
102        Self::new("csv")
103    }
104}
105
106impl PartialEq for BufferKind {
107    fn eq(&self, other: &Self) -> bool {
108        // Compare Arc identity first (interned kinds match fast),
109        // then fall back to string compare when the registry
110        // returned a stale reference (very rare).
111        Arc::ptr_eq(&self.slug, &other.slug) || self.slug == other.slug
112    }
113}
114
115impl Eq for BufferKind {}
116
117impl PartialOrd for BufferKind {
118    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
119        Some(self.cmp(other))
120    }
121}
122
123impl Ord for BufferKind {
124    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
125        self.slug.as_ref().cmp(other.slug.as_ref())
126    }
127}
128
129impl std::hash::Hash for BufferKind {
130    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
131        self.slug.hash(state);
132    }
133}
134
135impl std::fmt::Display for BufferKind {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        f.write_str(&self.slug)
138    }
139}
140
141// ── Global registry ──────────────────────────────────────────────────
142
143/// Interning registry. Adopters rarely use this directly — the
144/// [`BufferKind::new`] constructor goes through the global instance.
145/// Tests can swap the global via [`BufferKindRegistry::set_global`].
146pub struct BufferKindRegistry {
147    inner: RwLock<BufferKindRegistryInner>,
148}
149
150struct BufferKindRegistryInner {
151    slugs: std::collections::HashMap<String, Arc<str>>,
152}
153
154impl BufferKindRegistry {
155    pub fn new() -> Self {
156        let mut inner = BufferKindRegistryInner {
157            slugs: std::collections::HashMap::new(),
158        };
159        for seeded in SEEDED_KINDS {
160            let arc: Arc<str> = Arc::from(*seeded);
161            inner.slugs.insert((*seeded).to_string(), arc);
162        }
163        BufferKindRegistry {
164            inner: RwLock::new(inner),
165        }
166    }
167
168    /// Returns the process-wide singleton. Built lazily on first
169    /// access with the seeded kinds already registered.
170    pub fn global() -> &'static BufferKindRegistry {
171        use std::sync::OnceLock;
172        static GLOBAL: OnceLock<BufferKindRegistry> = OnceLock::new();
173        GLOBAL.get_or_init(BufferKindRegistry::new)
174    }
175
176    /// Intern a slug, returning the canonical `Arc<str>`.
177    pub fn intern(&self, slug: &str) -> Arc<str> {
178        // Fast path — read lock, hit cache.
179        {
180            let guard = self.inner.read().expect("registry poisoned");
181            if let Some(existing) = guard.slugs.get(slug) {
182                return Arc::clone(existing);
183            }
184        }
185        // Slow path — promote to write lock.
186        let mut guard = self.inner.write().expect("registry poisoned");
187        Arc::clone(
188            guard
189                .slugs
190                .entry(slug.to_string())
191                .or_insert_with(|| Arc::from(slug)),
192        )
193    }
194
195    /// Enumerate every currently registered slug (sorted, stable
196    /// order for tests / tracing).
197    pub fn known_slugs(&self) -> Vec<String> {
198        let guard = self.inner.read().expect("registry poisoned");
199        let mut v: Vec<String> = guard.slugs.keys().cloned().collect();
200        v.sort();
201        v
202    }
203}
204
205impl Default for BufferKindRegistry {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211const SEEDED_KINDS: &[&str] = &[
212    "raw", "pcm16", "mulaw8", "wav", "mp3", "opus", "jpeg", "png", "webp",
213    "mp4", "webm", "pdf", "json", "csv",
214];
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn seeded_kinds_are_equal_across_constructors() {
222        assert_eq!(BufferKind::raw(), BufferKind::new("raw"));
223        assert_eq!(BufferKind::pcm16(), BufferKind::new("pcm16"));
224    }
225
226    #[test]
227    fn intern_returns_same_arc_for_same_slug() {
228        let a = BufferKind::new("custom_kind_a");
229        let b = BufferKind::new("custom_kind_a");
230        assert_eq!(a, b);
231        // Interned → both clones point at the same Arc.
232        assert!(Arc::ptr_eq(&a.slug, &b.slug));
233    }
234
235    #[test]
236    fn different_slugs_are_not_equal() {
237        assert_ne!(BufferKind::new("a"), BufferKind::new("b"));
238    }
239
240    #[test]
241    fn known_slugs_includes_seeded() {
242        let slugs = BufferKindRegistry::global().known_slugs();
243        for seeded in SEEDED_KINDS {
244            assert!(
245                slugs.contains(&seeded.to_string()),
246                "seeded kind {seeded} missing from registry"
247            );
248        }
249    }
250
251    #[test]
252    fn display_format_matches_slug() {
253        let k = BufferKind::new("opus");
254        assert_eq!(format!("{k}"), "opus");
255    }
256}