Skip to main content

cbor_edn/
transform.rs

1use super::{DelimiterPolicy, Transformable};
2
3/// A builder-style transformation of CBOR or EDN data.
4///
5/// A transformation encodes changes that can be applied to a [`Transformable`] such as a
6/// [`StandaloneItem`][crate::StandaloneItem] or a [`Sequence`][crate::Sequence].
7///
8/// While methods on those and the [`application`][crate::application] module provide fine
9/// granularity and arbitrary operations, many use cases do not require the finest control -- and
10/// applying changes manually can lead to suboptimal results if not done in the right sequence.
11///
12/// Note that not all transformations have an effect for all input and output formats (any of
13/// CBOR-to-EDN, EDN-to-CBOR, EDN-to-EDN and CBOR-to-CBOR). For example, operations that influence
14/// indentation or comments have no practical effect when the transformation's output is encoded to
15/// CBOR, and operations that decode application-oriented literals have no effect when the input is
16/// decoded from CBOR.
17#[derive(Debug, Copy, Clone)]
18pub struct Transformation {
19    /// Policy applied to input
20    early_delimiter_policy: Option<DelimiterPolicy>,
21    /// Policy applied after application-oriented literals have been created
22    late_delimiter_policy: Option<DelimiterPolicy>,
23    tag_to_aol: bool,
24    from_999: bool,
25    aol_to_item: bool,
26    to_999: bool,
27    bignum_tag_to_edn_integer: bool,
28    known_structure: Option<KnownStructure>,
29    bytestring_heuristics: bool,
30}
31
32impl Transformation {
33    /// Creates a transformation builder that applies no changes.
34    pub const fn new() -> Self {
35        Transformation {
36            early_delimiter_policy: None,
37            late_delimiter_policy: None,
38            tag_to_aol: false,
39            from_999: false,
40            aol_to_item: false,
41            to_999: false,
42            bignum_tag_to_edn_integer: false,
43            known_structure: None,
44            bytestring_heuristics: false,
45        }
46    }
47
48    pub fn apply_to<'a>(&self, item: &mut impl Transformable<'a>) {
49        // This should get us to notice whenever there's a new parameter to process
50        let &Transformation {
51            early_delimiter_policy,
52            late_delimiter_policy,
53            tag_to_aol,
54            from_999,
55            aol_to_item,
56            to_999,
57            bignum_tag_to_edn_integer,
58            known_structure,
59            bytestring_heuristics,
60        } = self;
61
62        if let Some(indent) = early_delimiter_policy {
63            item.set_delimiters(indent);
64        }
65
66        // Running this early because later to-AoL steps should produce something suitable
67        // themselves.
68        if bytestring_heuristics {
69            crate::string::apply_bytestring_heuristincs(item);
70        }
71
72        if from_999 {
73            item.visit_tag(&mut crate::application::tag999_to_aol);
74        }
75
76        #[allow(clippy::single_match)] // reason: expecting to add more
77        match known_structure {
78            // 601 is UCCS, but that's just because it's toplevel; we have it as a CBOR item, so
79            // locally it is unprotected, but more generally, it is a CCS.
80            Some(KnownStructure::UnwrappedTag(601)) => {
81                for item in item.toplevel_items() {
82                    // FIXME: How do we best propagate a "that's not a map" error?
83                    // For what it's worth, do we even expect that there is more than one or no
84                    // CCS item, and if so, what does that mean for errors?
85                    let _ = item.visit_map_elements(&mut crate::application::comment_ccs);
86                }
87            }
88            Some(KnownStructure::CoseHeaderMap) => {
89                for item in item.toplevel_items() {
90                    // FIXME: like above
91                    let _ = item.visit_map_elements(&mut crate::application::comment_cose_header);
92                }
93            }
94            Some(KnownStructure::UnwrappedTag(_)) | None => {}
95        }
96
97        if tag_to_aol {
98            item.visit_tag(&mut crate::application::dt_tag_to_aol);
99            item.visit_tag(&mut crate::application::ip_tag_to_aol);
100            item.visit_tag(&mut crate::application::comment_lang_tag);
101        }
102
103        if bignum_tag_to_edn_integer {
104            item.visit_tag(&mut crate::application::bignum_tag_to_edn_integer);
105        }
106
107        if aol_to_item {
108            item.visit_application_literals(&mut crate::application::dt_aol_to_item);
109            item.visit_application_literals(&mut crate::application::ip_aol_to_item);
110        }
111
112        if to_999 {
113            item.visit_application_literals(&mut crate::application::any_aol_to_tag999);
114        }
115
116        if let Some(indent) = late_delimiter_policy {
117            item.set_delimiters(indent);
118        }
119    }
120
121    // FIXME: how much do we have to do manually so this can be used also with items?
122}
123
124/// # Groups of transformations transformations
125impl Transformation {
126    /// Applies any transformation that making the EDN well-readable for humans.
127    ///
128    /// Currently, this applies indentation and processes all known tags to produce corresponding
129    /// application-oriented literals (or, in case of tags 2 and 3, to EDN numbers).
130    pub const fn pretty(self) -> Self {
131        self.indent()
132            .tag_to_aol()
133            .bignum_tag_to_edn_integer()
134            .bytestring_heuristics()
135    }
136
137    /// Applies the minimal transformations that are necessary for expressing general EDN as CBOR.
138    ///
139    /// Currently, this is an alias for [`Self::aol_to_item()`] (and the author lacks the
140    /// imagination on whether this would ever need other transformations; the one that comes to
141    /// mind is converting big integers from EDN to CBOR, but that currently happens automatically
142    /// at serialization time).
143    pub const fn for_cbor_serialization(self) -> Self {
144        self.aol_to_item()
145    }
146}
147
148/// # Single transformations
149impl Transformation {
150    /// Removes all comments and spaces from the EDN.
151    pub const fn minify(self) -> Self {
152        Transformation {
153            early_delimiter_policy: Some(DelimiterPolicy::DiscardAll),
154            ..self
155        }
156    }
157
158    /// Applies some default indentation to the EDN.
159    ///
160    /// Output will have a final newline if the general structure is multiline.
161    pub const fn indent(self) -> Self {
162        Transformation {
163            late_delimiter_policy: Some(DelimiterPolicy::indented()),
164            ..self
165        }
166    }
167
168    /// Applies some default indentation to the EDN.
169    ///
170    /// Output will have a final newline unconditionally
171    pub const fn indent_with_final_newline(self) -> Self {
172        Transformation {
173            late_delimiter_policy: Some(DelimiterPolicy::indented_with_final_newline()),
174            ..self
175        }
176    }
177
178    /// Recognizes Tag 999 and turns it into application-oriented literals.
179    ///
180    /// While this is mostly useful to obtain pretty EDN from an application that, knowing its
181    /// domain CBOR, prepared tags for pretty-printing, it can also interact with other options
182    /// that recognize application-oriented literals.
183    pub const fn from_999(self) -> Self {
184        Transformation {
185            from_999: true,
186            ..self
187        }
188    }
189
190    /// Produces Tag 999 for all application-oriented literals that are not converted to items in
191    /// other steps.
192    ///
193    /// This is applied last, after any other transformation option that process
194    /// application-oriented literals.
195    pub const fn to_999(self) -> Self {
196        Transformation {
197            to_999: true,
198            ..self
199        }
200    }
201
202    /// Recognizes tags to produce application-oriented literals or annotations inside the tag.
203    pub const fn tag_to_aol(self) -> Self {
204        Transformation {
205            tag_to_aol: true,
206            ..self
207        }
208    }
209
210    pub const fn bignum_tag_to_edn_integer(self) -> Self {
211        Transformation {
212            bignum_tag_to_edn_integer: true,
213            ..self
214        }
215    }
216
217    /// Turns recognized application-oriented literals into their CBOR items.
218    pub const fn aol_to_item(self) -> Self {
219        Transformation {
220            aol_to_item: true,
221            ..self
222        }
223    }
224
225    /// Annotates (by turning into an AOL, adding comments or processing nested items) the
226    /// top-level structure .
227    pub fn annotate_unwrapped_tag(self, tag_number: u64) -> Self {
228        Transformation {
229            known_structure: Some(KnownStructure::UnwrappedTag(tag_number)),
230            ..self
231        }
232    }
233
234    pub fn annotate_cose_header_map(self) -> Self {
235        Transformation {
236            known_structure: Some(KnownStructure::CoseHeaderMap),
237            ..self
238        }
239    }
240
241    /// Decides `'abc'` and `h'616263'` based on its content
242    pub const fn bytestring_heuristics(self) -> Self {
243        Transformation {
244            bytestring_heuristics: true,
245            ..self
246        }
247    }
248}
249
250impl Default for Transformation {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256/// A structure known of an item that can be used for better annotation.
257#[derive(Copy, Clone, Debug)]
258enum KnownStructure {
259    /// The structure follows the content of a tag given by number.
260    UnwrappedTag(u64),
261    // Not yet:
262    //
263    // ContentFormat(u16)
264    // MediaType(...)
265    CoseHeaderMap,
266}