bc_envelope/format/
format_context.rs

1#[cfg(any(feature = "expression", feature = "known_value"))]
2use std::sync::Arc;
3use std::sync::{Mutex, Once};
4
5#[cfg(feature = "known_value")]
6use bc_components::tags::TAG_KNOWN_VALUE;
7use bc_components::tags::*;
8#[cfg(feature = "known_value")]
9use known_values::{KNOWN_VALUES, KnownValue, KnownValuesStore};
10
11#[cfg(feature = "expression")]
12use super::notation::EnvelopeFormatOpts;
13#[cfg(feature = "expression")]
14use crate::Envelope;
15#[cfg(feature = "expression")]
16use crate::extension::expressions::{
17    FunctionsStore, GLOBAL_FUNCTIONS, GLOBAL_PARAMETERS, ParametersStore,
18};
19#[cfg(any(feature = "expression", feature = "known_value"))]
20use crate::string_utils::StringUtils;
21
22#[derive(Clone)]
23pub enum FormatContextOpt<'a> {
24    None,
25    Global,
26    Custom(&'a FormatContext),
27}
28
29impl<'a> Default for FormatContextOpt<'a> {
30    fn default() -> Self { FormatContextOpt::Global }
31}
32
33impl<'a> From<FormatContextOpt<'a>> for dcbor::TagsStoreOpt<'a> {
34    fn from(opt: FormatContextOpt<'a>) -> Self {
35        match opt {
36            FormatContextOpt::None => dcbor::TagsStoreOpt::None,
37            FormatContextOpt::Global => dcbor::TagsStoreOpt::Global,
38            FormatContextOpt::Custom(context) => {
39                dcbor::TagsStoreOpt::Custom(context.tags())
40            }
41        }
42    }
43}
44
45/// Context object for formatting Gordian Envelopes with annotations.
46///
47/// The `FormatContext` provides information about CBOR tags, known values,
48/// functions, and parameters that are used to annotate the output of envelope
49/// formatting functions. This context enables human-readable output when
50/// converting envelopes to string representations like diagnostic notation.
51///
52/// This type is central to the diagnostic capabilities of Gordian Envelope,
53/// translating numeric CBOR tags into meaningful names and providing
54/// context-specific formatting for special values.
55///
56/// # Format Context Content
57///
58/// A `FormatContext` contains:
59/// - CBOR tag registry (always present)
60/// - Known Values store (when `known_value` feature is enabled)
61/// - Functions store (when `expression` feature is enabled)
62/// - Parameters store (when `expression` feature is enabled)
63/// - A flag indicating whether the format should be flat or structured
64///
65/// # Global Context
66///
67/// A global format context is available through the `with_format_context!` and
68/// `with_format_context_mut!` macros. This global context is initialized with
69/// standard tags and registries.
70///
71/// # Example
72///
73/// Using the global format context to produce annotated CBOR diagnostic
74/// notation:
75///
76/// ```
77/// # use bc_envelope::prelude::*;
78/// # use indoc::indoc;
79/// # let e = Envelope::new("Hello.");
80/// # bc_envelope::register_tags();
81/// assert_eq!(
82///     e.diagnostic_annotated(),
83///     indoc! {r#"
84/// 200(   / envelope /
85///     201("Hello.")   / leaf /
86/// )
87/// "#}
88///     .trim()
89/// );
90/// ```
91///
92/// The annotations (comments after the `/` characters) provide human-readable
93/// context for the CBOR tags and structure.
94#[derive(Clone)]
95pub struct FormatContext {
96    tags: TagsStore,
97    #[cfg(feature = "known_value")]
98    known_values: KnownValuesStore,
99    #[cfg(feature = "expression")]
100    functions: FunctionsStore,
101    #[cfg(feature = "expression")]
102    parameters: ParametersStore,
103}
104
105impl FormatContext {
106    /// Creates a new format context with the specified components.
107    ///
108    /// This constructor allows full customization of the format context by
109    /// providing optional components. Any component not provided will be
110    /// initialized with its default.
111    ///
112    /// # Parameters
113    ///
114    /// * `flat` - If true, formatting will be flattened without indentation and
115    ///   structure
116    /// * `tags` - Optional CBOR tag registry for mapping tag numbers to names
117    /// * `known_values` - Optional known values registry (requires
118    ///   `known_value` feature)
119    /// * `functions` - Optional functions registry (requires `expression`
120    ///   feature)
121    /// * `parameters` - Optional parameters registry (requires `expression`
122    ///   feature)
123    ///
124    /// # Returns
125    ///
126    /// A new `FormatContext` instance initialized with the provided components.
127    pub fn new(
128        tags: Option<&TagsStore>,
129        #[cfg(feature = "known_value")] known_values: Option<&KnownValuesStore>,
130        #[cfg(feature = "expression")] functions: Option<&FunctionsStore>,
131        #[cfg(feature = "expression")] parameters: Option<&ParametersStore>,
132    ) -> Self {
133        Self {
134            tags: tags.cloned().unwrap_or_default(),
135            #[cfg(feature = "known_value")]
136            known_values: known_values.cloned().unwrap_or_default(),
137            #[cfg(feature = "expression")]
138            functions: functions.cloned().unwrap_or_default(),
139            #[cfg(feature = "expression")]
140            parameters: parameters.cloned().unwrap_or_default(),
141        }
142    }
143
144    /// Returns a reference to the CBOR tags registry.
145    ///
146    /// The tags registry maps CBOR tag numbers to human-readable names
147    /// and provides summarizers for tag-specific formatting.
148    pub fn tags(&self) -> &TagsStore { &self.tags }
149
150    /// Returns a mutable reference to the CBOR tags registry.
151    ///
152    /// This allows modifying the tags registry to add or change tag mappings.
153    pub fn tags_mut(&mut self) -> &mut TagsStore { &mut self.tags }
154
155    /// Returns a reference to the known values registry.
156    ///
157    /// The known values registry maps symbolic values (like "true", "false",
158    /// etc.) to their canonical string representations.
159    ///
160    /// This method is only available when the `known_value` feature is enabled.
161    #[cfg(feature = "known_value")]
162    pub fn known_values(&self) -> &KnownValuesStore { &self.known_values }
163
164    /// Returns a reference to the functions registry.
165    ///
166    /// The functions registry maps function identifiers to their human-readable
167    /// names for use in expression formatting.
168    ///
169    /// This method is only available when the `expression` feature is enabled.
170    #[cfg(feature = "expression")]
171    pub fn functions(&self) -> &FunctionsStore { &self.functions }
172
173    /// Returns a reference to the parameters registry.
174    ///
175    /// The parameters registry maps parameter identifiers to their
176    /// human-readable names for use in expression formatting.
177    ///
178    /// This method is only available when the `expression` feature is enabled.
179    #[cfg(feature = "expression")]
180    pub fn parameters(&self) -> &ParametersStore { &self.parameters }
181}
182
183/// Implementation of `TagsStoreTrait` for `FormatContext`, delegating to the
184/// internal `TagsStore`.
185///
186/// This implementation allows a `FormatContext` to be used anywhere a
187/// `TagsStoreTrait` is required, providing the tag resolution functionality
188/// directly.
189impl TagsStoreTrait for FormatContext {
190    /// Returns the assigned name for a tag if one exists.
191    fn assigned_name_for_tag(&self, tag: &Tag) -> Option<String> {
192        self.tags.assigned_name_for_tag(tag)
193    }
194
195    /// Returns a name for a tag, either the assigned name or a generic
196    /// representation.
197    fn name_for_tag(&self, tag: &Tag) -> String { self.tags.name_for_tag(tag) }
198
199    /// Looks up a tag by its name.
200    fn tag_for_name(&self, name: &str) -> Option<Tag> {
201        self.tags.tag_for_name(name)
202    }
203
204    /// Looks up a tag by its numeric value.
205    fn tag_for_value(&self, value: TagValue) -> Option<Tag> {
206        self.tags.tag_for_value(value)
207    }
208
209    /// Returns a CBOR summarizer for a tag value if one exists.
210    fn summarizer(&self, tag: TagValue) -> Option<&CBORSummarizer> {
211        self.tags.summarizer(tag)
212    }
213
214    /// Returns a name for a tag value, either the assigned name or a generic
215    /// representation.
216    fn name_for_value(&self, value: TagValue) -> String {
217        self.tags.name_for_value(value)
218    }
219}
220
221/// Default implementation for `FormatContext`, creating an instance with
222/// default components.
223impl Default for FormatContext {
224    /// Creates a default `FormatContext` with:
225    /// - Flat formatting disabled (structured formatting)
226    /// - Default tag registry
227    /// - Default known values store (when `known_value` feature is enabled)
228    /// - Default functions store (when `expression` feature is enabled)
229    /// - Default parameters store (when `expression` feature is enabled)
230    fn default() -> Self {
231        Self::new(
232            None,
233            #[cfg(feature = "known_value")]
234            None,
235            #[cfg(feature = "expression")]
236            None,
237            #[cfg(feature = "expression")]
238            None,
239        )
240    }
241}
242
243/// A container for lazily initializing the global format context.
244///
245/// This type ensures the global format context is only initialized once,
246/// even in multithreaded contexts, and provides thread-safe access to the
247/// context.
248pub struct LazyFormatContext {
249    /// Initialization flag to ensure one-time initialization
250    init: Once,
251    /// Thread-safe storage for the format context
252    data: Mutex<Option<FormatContext>>,
253}
254
255impl LazyFormatContext {
256    /// Gets a thread-safe reference to the format context, initializing it if
257    /// necessary.
258    ///
259    /// On first access, this method initializes the format context with
260    /// standard registrations for tags, known values, functions, and
261    /// parameters.
262    ///
263    /// # Returns
264    ///
265    /// A mutex guard containing a reference to the global format context.
266    pub fn get(&self) -> std::sync::MutexGuard<'_, Option<FormatContext>> {
267        self.init.call_once(|| {
268            bc_components::register_tags();
269            let tags_binding = dcbor::GLOBAL_TAGS.get();
270            let tags = tags_binding.as_ref().unwrap();
271
272            #[cfg(feature = "known_value")]
273            let known_values_binding = KNOWN_VALUES.get();
274            #[cfg(feature = "known_value")]
275            let known_values = known_values_binding.as_ref().unwrap();
276
277            #[cfg(feature = "expression")]
278            let functions_binding = GLOBAL_FUNCTIONS.get();
279            #[cfg(feature = "expression")]
280            let functions = functions_binding.as_ref().unwrap();
281            #[cfg(feature = "expression")]
282            let parameters_binding = GLOBAL_PARAMETERS.get();
283            #[cfg(feature = "expression")]
284            let parameters = parameters_binding.as_ref().unwrap();
285
286            let context = FormatContext::new(
287                Some(tags),
288                #[cfg(feature = "known_value")]
289                Some(known_values),
290                #[cfg(feature = "expression")]
291                Some(functions),
292                #[cfg(feature = "expression")]
293                Some(parameters),
294            );
295            *self.data.lock().unwrap() = Some(context);
296        });
297        self.data.lock().unwrap()
298    }
299}
300
301/// Global singleton instance of `FormatContext` for application-wide use.
302///
303/// Access this using the `with_format_context!` macro.
304pub static GLOBAL_FORMAT_CONTEXT: LazyFormatContext =
305    LazyFormatContext { init: Once::new(), data: Mutex::new(None) };
306
307/// A macro to access the global format context for read-only operations.
308///
309/// This macro provides a convenient way to use the global format context
310/// without dealing with mutex locking and unlocking directly.
311#[macro_export]
312macro_rules! with_format_context {
313    ($action:expr) => {{
314        let binding = $crate::GLOBAL_FORMAT_CONTEXT.get();
315        let context = &*binding.as_ref().unwrap();
316        #[allow(clippy::redundant_closure_call)]
317        $action(context)
318    }};
319}
320
321/// A macro to access the global format context for read-write operations.
322///
323/// This macro provides a convenient way to use and modify the global format
324/// context without dealing with mutex locking and unlocking directly.
325#[macro_export]
326macro_rules! with_format_context_mut {
327    ($action:expr) => {{
328        let mut binding = $crate::GLOBAL_FORMAT_CONTEXT.get();
329        let context = binding.as_mut().unwrap();
330        #[allow(clippy::redundant_closure_call)]
331        $action(context)
332    }};
333}
334
335/// Registers standard tags and summarizers in a format context.
336///
337/// This function populates a format context with standard tag definitions
338/// and summarizers for various envelope-related types, enabling proper
339/// annotation of formatted output.
340///
341/// # Parameters
342///
343/// * `context` - The format context to register tags in
344pub fn register_tags_in(context: &mut FormatContext) {
345    // Register standard component tags
346    bc_components::register_tags_in(context.tags_mut());
347
348    #[cfg(feature = "known_value")]
349    {
350        // Known value summarizer - formats known values with single quotes
351        let known_values = context.known_values().clone();
352        context.tags_mut().set_summarizer(
353            TAG_KNOWN_VALUE,
354            Arc::new(move |untagged_cbor: CBOR, _flat: bool| {
355                Ok(known_values
356                    .name(KnownValue::from_untagged_cbor(untagged_cbor)?)
357                    .flanked_by("'", "'"))
358            }),
359        );
360    }
361
362    // Register expression-related summarizers when the expression feature is
363    // enabled
364    #[cfg(feature = "expression")]
365    {
366        use crate::extension::expressions::{
367            Function, FunctionsStore, Parameter, ParametersStore,
368        };
369
370        // Function summarizer - formats functions with special delimiter
371        // characters
372        let functions = context.functions().clone();
373        context.tags_mut().set_summarizer(
374            TAG_FUNCTION,
375            Arc::new(move |untagged_cbor: CBOR, _flat: bool| {
376                let f = Function::from_untagged_cbor(untagged_cbor)?;
377                Ok(FunctionsStore::name_for_function(&f, Some(&functions))
378                    .flanked_by("«", "»"))
379            }),
380        );
381
382        // Parameter summarizer - formats parameters with special delimiter
383        // characters
384        let parameters = context.parameters().clone();
385        context.tags_mut().set_summarizer(
386            TAG_PARAMETER,
387            Arc::new(move |untagged_cbor: CBOR, _flat: bool| {
388                let p = Parameter::from_untagged_cbor(untagged_cbor)?;
389                Ok(ParametersStore::name_for_parameter(&p, Some(&parameters))
390                    .flanked_by("❰", "❱"))
391            }),
392        );
393
394        // Request summarizer - formats requests with request() notation
395        let cloned_context = context.clone();
396        context.tags_mut().set_summarizer(
397            TAG_REQUEST,
398            Arc::new(move |untagged_cbor: CBOR, flat: bool| {
399                Ok(Envelope::new(untagged_cbor)
400                    .format_opt(
401                        &EnvelopeFormatOpts::default()
402                            .flat(flat)
403                            .context(FormatContextOpt::Custom(&cloned_context)),
404                    )
405                    .flanked_by("request(", ")"))
406            }),
407        );
408
409        // Response summarizer - formats responses with response() notation
410        let cloned_context = context.clone();
411        context.tags_mut().set_summarizer(
412            TAG_RESPONSE,
413            Arc::new(move |untagged_cbor: CBOR, flat: bool| {
414                Ok(Envelope::new(untagged_cbor)
415                    .format_opt(
416                        &EnvelopeFormatOpts::default()
417                            .flat(flat)
418                            .context(FormatContextOpt::Custom(&cloned_context)),
419                    )
420                    .flanked_by("response(", ")"))
421            }),
422        );
423
424        // Event summarizer - formats events with event() notation
425        let cloned_context = context.clone();
426        context.tags_mut().set_summarizer(
427            TAG_EVENT,
428            Arc::new(move |untagged_cbor: CBOR, flat: bool| {
429                Ok(Envelope::new(untagged_cbor)
430                    .format_opt(
431                        &EnvelopeFormatOpts::default()
432                            .flat(flat)
433                            .context(FormatContextOpt::Custom(&cloned_context)),
434                    )
435                    .flanked_by("event(", ")"))
436            }),
437        );
438    }
439}
440
441/// Registers standard tags in the global format context.
442///
443/// This function uses the global format context and registers standard tags
444/// using the `register_tags_in` function. It's a convenience wrapper for
445/// registering tags in the global context.
446pub fn register_tags() {
447    with_format_context_mut!(|context: &mut FormatContext| {
448        register_tags_in(context);
449    });
450}