dcbor/
tags_store.rs

1import_stdlib!();
2
3use crate::{CBOR, Result, Tag, TagValue};
4
5/// A function type for summarizing CBOR values as human-readable strings.
6///
7/// `CBORSummarizer` is used to create custom formatters for tagged CBOR values.
8/// It's a type alias for a thread-safe function pointer that takes a CBOR value
9/// and returns a string representation or an error.
10///
11/// ## Purpose
12///
13/// In CBOR, tags provide semantic meaning to data structures. A
14/// `CBORSummarizer` helps interpret and display these tagged values in a
15/// human-readable format. This is particularly useful for debugging, logging,
16/// or displaying CBOR data to users.
17///
18/// ## Thread Safety
19///
20/// The `CBORSummarizer` type is wrapped in an `Arc` (Atomic Reference Count)
21/// and requires `Send + Sync` traits, making it safe to share between threads.
22///
23/// ## Examples
24///
25/// ```
26/// use std::sync::Arc;
27///
28/// use dcbor::prelude::*;
29///
30/// // Create a custom summarizer for a date tag
31/// let date_summarizer: CBORSummarizer = Arc::new(|cbor, _flat| {
32///     // Extract timestamp from tagged CBOR
33///     let timestamp: f64 = cbor.clone().try_into()?;
34///
35///     // Format timestamp as ISO date (simplified example)
36///     Ok(format!("Date: {:.1} seconds since epoch", timestamp))
37/// });
38///
39/// // Create a tags store
40/// let mut tags = TagsStore::default();
41///
42/// // Register a tag for date (tag 1 is the standard CBOR tag for dates)
43/// tags.insert(Tag::new(1, "date".to_string()));
44///
45/// // Register our summarizer for tag 1
46/// tags.set_summarizer(1, date_summarizer);
47/// ```
48///
49/// When this summarizer is used (for example in diagnostic output), it would
50/// convert a tagged CBOR timestamp into a more readable date format.
51pub type CBORSummarizer =
52    Arc<dyn (Fn(CBOR, bool) -> Result<String>) + Send + Sync>;
53
54/// A trait for types that can map between CBOR tags and their human-readable
55/// names.
56///
57/// The `TagsStoreTrait` provides a standardized interface for resolving CBOR
58/// tags to human-readable names and vice versa. This is useful for debugging
59/// and displaying CBOR data in a more understandable format.
60///
61/// ## Functionality
62///
63/// This trait defines methods for:
64/// - Converting between tag numbers and human-readable names
65/// - Looking up tags by name or value
66/// - Retrieving summarizers for specific tags
67///
68/// ## Implementation
69///
70/// Implementers of this trait should maintain a bidirectional mapping between
71/// tag values (numbers) and their corresponding names. The primary
72/// implementation is [`TagsStore`], but other implementations can be created
73/// for specific needs.
74///
75/// ## Examples
76///
77/// ```
78/// use dcbor::prelude::*;
79///
80/// // Create a store that implements TagsStoreTrait
81/// let mut tags = TagsStore::default();
82///
83/// // Register a tag with a human-readable name
84/// tags.insert(Tag::new(1, "date".to_string()));
85///
86/// // Look up tag by number
87/// let tag_name = tags.name_for_value(1);
88/// assert_eq!(tag_name, "date");
89///
90/// // Look up tag by name
91/// let tag = tags.tag_for_name("date").unwrap();
92/// assert_eq!(tag.value(), 1);
93/// ```
94///
95/// The trait also includes a helper method to handle the common case of
96/// optional tag stores.
97pub trait TagsStoreTrait {
98    fn assigned_name_for_tag(&self, tag: &Tag) -> Option<String>;
99    fn name_for_tag(&self, tag: &Tag) -> String;
100    fn tag_for_value(&self, value: TagValue) -> Option<Tag>;
101    fn tag_for_name(&self, name: &str) -> Option<Tag>;
102    fn name_for_value(&self, value: TagValue) -> String;
103    fn summarizer(&self, tag: TagValue) -> Option<&CBORSummarizer>;
104
105    fn name_for_tag_opt<T>(tag: &Tag, tags: Option<&T>) -> String
106    where
107        T: TagsStoreTrait,
108        Self: Sized,
109    {
110        match tags {
111            None => tag.value().to_string(),
112            Some(tags) => tags.name_for_tag(tag),
113        }
114    }
115}
116
117#[derive(Clone, Default)]
118pub enum TagsStoreOpt<'a> {
119    None,
120    #[default]
121    Global,
122    Custom(&'a dyn TagsStoreTrait),
123}
124
125/// A registry that maintains mappings between CBOR tags, their human-readable
126/// names, and optional summarizers.
127///
128/// The `TagsStore` is the primary implementation of the [`TagsStoreTrait`],
129/// providing a bidirectional mapping between CBOR tag values (numbers) and
130/// their human-readable names. It also supports registering custom summarizers
131/// for specific tags.
132///
133/// ## Use Cases
134///
135/// The `TagsStore` serves several important purposes:
136///
137/// 1. **Readability**: Converting numeric tags to meaningful names in
138///    diagnostic output
139/// 2. **Consistency**: Ensuring consistent tag usage throughout an application
140/// 3. **Documentation**: Providing a central registry of all tags used in a
141///    system
142/// 4. **Customization**: Supporting custom summarization of tagged values
143///
144/// ## Features
145///
146/// - Bidirectional lookup between tag values and names
147/// - Prevention of duplicate registrations with conflicting names
148/// - Optional registration of custom summarizers for specific tags
149/// - Default implementation for an empty registry
150///
151/// ## Examples
152///
153/// ### Basic usage
154///
155/// ```
156/// use dcbor::prelude::*;
157///
158/// // Create a new TagsStore with a set of predefined tags
159/// let mut tags = TagsStore::new([
160///     Tag::new(1, "date".to_string()),
161///     Tag::new(2, "positive_bignum".to_string()),
162///     Tag::new(3, "negative_bignum".to_string()),
163/// ]);
164///
165/// // Look up a tag by its value
166/// let date_tag = tags.tag_for_value(1).unwrap();
167/// assert_eq!(date_tag.name().unwrap(), "date");
168///
169/// // Look up a tag name by value
170/// let name = tags.name_for_value(2);
171/// assert_eq!(name, "positive_bignum");
172///
173/// // Look up a tag by its name
174/// let neg_tag = tags.tag_for_name("negative_bignum").unwrap();
175/// assert_eq!(neg_tag.value(), 3);
176/// ```
177///
178/// ### Adding summarizers
179///
180/// ```
181/// use std::sync::Arc;
182///
183/// use dcbor::prelude::*;
184///
185/// // Create an empty tags store
186/// let mut tags = TagsStore::default();
187///
188/// // Register a tag
189/// tags.insert(Tag::new(1, "date".to_string()));
190///
191/// // Add a summarizer for the date tag
192/// tags.set_summarizer(
193///     1,
194///     Arc::new(|cbor, _flat| {
195///         // Try to convert CBOR to f64 for timestamp formatting
196///         let timestamp: f64 = cbor.clone().try_into().unwrap_or(0.0);
197///         Ok(format!("Timestamp: {}", timestamp))
198///     }),
199/// );
200///
201/// // Later, this summarizer can be retrieved and used
202/// let date_summarizer = tags.summarizer(1);
203/// assert!(date_summarizer.is_some());
204/// ```
205///
206/// ## Implementation Notes
207///
208/// The `TagsStore` prevents registering the same tag value with different
209/// names, which helps maintain consistency in CBOR tag usage. Attempting to
210/// register a tag value that already exists with a different name will panic.
211#[derive(Clone)]
212pub struct TagsStore {
213    tags_by_value: HashMap<TagValue, Tag>,
214    tags_by_name: HashMap<String, Tag>,
215    summarizers: HashMap<TagValue, CBORSummarizer>,
216}
217
218impl TagsStore {
219    pub fn new<T>(tags: T) -> Self
220    where
221        T: IntoIterator<Item = Tag>,
222    {
223        let mut tags_by_value = HashMap::new();
224        let mut tags_by_name = HashMap::new();
225        for tag in tags {
226            Self::_insert(tag, &mut tags_by_value, &mut tags_by_name);
227        }
228        Self {
229            tags_by_value,
230            tags_by_name,
231            summarizers: HashMap::new(),
232        }
233    }
234
235    pub fn insert(&mut self, tag: Tag) {
236        Self::_insert(tag, &mut self.tags_by_value, &mut self.tags_by_name);
237    }
238
239    pub fn insert_all(&mut self, tags: Vec<Tag>) {
240        for tag in tags {
241            Self::_insert(tag, &mut self.tags_by_value, &mut self.tags_by_name);
242        }
243    }
244
245    pub fn set_summarizer(
246        &mut self,
247        tag: TagValue,
248        summarizer: CBORSummarizer,
249    ) {
250        self.summarizers.insert(tag, summarizer);
251    }
252
253    fn _insert(
254        tag: Tag,
255        tags_by_value: &mut HashMap<TagValue, Tag>,
256        tags_by_name: &mut HashMap<String, Tag>,
257    ) {
258        let name = tag.name().unwrap();
259        assert!(!name.is_empty());
260        let result = tags_by_value.insert(tag.value(), tag.clone());
261        if let Some(old_value) = result {
262            // if the names don't match, we have a problem
263            let old_name = old_value.name().unwrap();
264            if old_name != name {
265                panic!(
266                    "Attempt to register tag: {} '{}' with different name: '{}'",
267                    tag.value(),
268                    old_name,
269                    name
270                );
271            }
272        }
273        tags_by_name.insert(name, tag);
274    }
275}
276
277impl TagsStoreTrait for TagsStore {
278    fn assigned_name_for_tag(&self, tag: &Tag) -> Option<String> {
279        self.tag_for_value(tag.value())
280            .map(|tag| tag.name().unwrap())
281    }
282
283    fn name_for_tag(&self, tag: &Tag) -> String {
284        self.assigned_name_for_tag(tag)
285            .unwrap_or_else(|| tag.value().to_string())
286    }
287
288    fn tag_for_name(&self, name: &str) -> Option<Tag> {
289        self.tags_by_name.get(name).cloned()
290    }
291
292    fn tag_for_value(&self, value: TagValue) -> Option<Tag> {
293        self.tags_by_value.get(&value).cloned()
294    }
295
296    fn name_for_value(&self, value: TagValue) -> String {
297        self.tag_for_value(value)
298            .and_then(|tag| tag.name())
299            .unwrap_or_else(|| value.to_string())
300    }
301
302    fn summarizer(&self, tag: TagValue) -> Option<&CBORSummarizer> {
303        self.summarizers.get(&tag)
304    }
305}
306
307impl Default for TagsStore {
308    fn default() -> Self { Self::new([]) }
309}