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}