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}