serde_ucl 0.3.0

UCL (Universal Configuration Language) with serde: reads and writes UCL as libucl does
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//! Output formats: JSON, compact JSON, the UCL config format and YAML (spec §10).
//!
//! An [`Emitter`] writes a [`UclValue`] in one [`Format`], byte for byte as libucl writes it,
//! quirks included. Besides the value itself, the config and YAML formats depend on facts
//! remembered from parsing (spec §10.1): whether a string was single-quoted or a heredoc, and how
//! each key was written. The parser records them ([`crate::parse::Parser::output_facts`]);
//! [`Parser::emitter`](crate::parse::Parser::emitter) gives an emitter that uses the facts of the
//! last parse. Without facts, strings use the JSON form and keys are quoted by
//! [`key_needs_quoting`].
//!
//! The config format can also write the comments saved under `SAVE_COMMENTS` (spec §10.10), but
//! only when asked with [`Emitter::with_comments`]; by default no comments are written. JSON and
//! YAML never contain comments.
//!
//! ```
//! use serde_ucl::emit::Format;
//! use serde_ucl::parse::Parser;
//!
//! let mut parser = Parser::new();
//! let value = parser.parse(b"name = 'web'\nports = [80, 443]\ntimeout = 1.5").unwrap();
//! let config = parser.emitter(Format::Config).emit(&value);
//! assert_eq!(config, "name = 'web';\nports [\n    80,\n    443,\n]\ntimeout = 1.500000;\n");
//! let json = parser.emitter(Format::JsonCompact).emit(&value);
//! assert_eq!(json, r#"{"name":"web","ports":[80,443],"timeout":1.500000}"#);
//! ```
//!
//! The default formats do not always read back as the same value; spec §10.8 lists where they
//! differ. Floats, for instance, keep at most six decimals. The serde functions of
//! [`crate::ser`] use the same layouts with forms that read back exactly, except that their JSON
//! output, which is valid JSON, writes a time as its seconds, which read back as a float.

mod config;
mod json;
mod number;
mod text;

pub use text::key_needs_quoting;

use crate::parse::facts::{self, NodeId};
use crate::parse::tree::Children;
use crate::parse::{
    AttachedComments, Comment, CommentPlacement, OutputFacts, PathSegment, ValueFacts,
};
use crate::value::UclValue;

/// An output format of spec §10.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Format {
    /// JSON with one member or element per line, indented four spaces per level (§10.4).
    Json,
    /// JSON without whitespace (§10.4).
    JsonCompact,
    /// The UCL config format, which libucl reads back (§10.5).
    Config,
    /// libucl's YAML-like format (§10.6).
    Yaml,
}

/// How an [`Emitter`] writes scalars, keys and multi-value entries.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
    /// As libucl writes them (spec §10.2–§10.7), quirks included.
    Libucl,
    /// In forms that read back as exactly the value written (spec §10.8), for serde
    /// serialization ([`crate::ser`]). Output facts and saved comments are not used. JSON and
    /// compact JSON are valid JSON (RFC 8259): a time is written as its number of seconds, and a
    /// NaN or infinite float or time has no form there (WORKLIST.md C4, decision 2).
    RoundTrip,
}

/// Writes values in one [`Format`], with optional output facts and saved comments.
#[derive(Debug, Clone, Copy)]
pub struct Emitter<'a> {
    format: Format,
    facts: Option<&'a OutputFacts>,
    comments: Option<(&'a [Comment], &'a [AttachedComments])>,
    mode: Mode,
}

impl<'a> Emitter<'a> {
    /// An emitter for `format`, without output facts or comments.
    pub fn new(format: Format) -> Self {
        Self {
            format,
            facts: None,
            comments: None,
            mode: Mode::Libucl,
        }
    }

    /// The emitter in round-trip mode: every value is written in a form that libucl and
    /// [`crate::parse`] read back as exactly that value (spec §10.8), in the layouts of the
    /// emitter's format; in JSON and compact JSON a time is written as its seconds and reads back
    /// as a float. Use [`Emitter::try_emit`]; see [`crate::ser`] for the forms and for the
    /// values that have none.
    pub(crate) fn round_trip(mut self) -> Self {
        self.mode = Mode::RoundTrip;
        self
    }

    /// The format this emitter writes.
    pub fn format(&self) -> Format {
        self.format
    }

    /// Uses `facts`, recorded when the value was parsed, for the forms of strings and keys
    /// (spec §10.1). The paths in `facts` must be those of the value given to
    /// [`Emitter::emit`].
    pub fn with_facts(mut self, facts: &'a OutputFacts) -> Self {
        self.facts = Some(facts);
        self
    }

    /// Writes the saved comments `attached` to each value, as libucl's config format does when
    /// the application passes them in (spec §10.10). `comments` and `attached` are
    /// [`Parser::comments`](crate::parse::Parser::comments) and
    /// [`Parser::attached_comments`](crate::parse::Parser::attached_comments) of the parse that
    /// produced the value. This has an effect in the config format only.
    pub fn with_comments(
        mut self,
        comments: &'a [Comment],
        attached: &'a [AttachedComments],
    ) -> Self {
        self.comments = Some((comments, attached));
        self
    }

    /// The text of `value` in the emitter's format.
    pub fn emit(&self, value: &UclValue) -> String {
        self.run(value).out
    }

    /// The text of `value` in round-trip mode, or a description of the first value that has no
    /// form that reads back exactly.
    pub(crate) fn try_emit(&self, value: &UclValue) -> Result<String, String> {
        let writer = self.run(value);
        match writer.error {
            Some(error) => Err(error),
            None => Ok(writer.out),
        }
    }

    fn run(&self, value: &UclValue) -> Writer<'a> {
        let exact = self.mode == Mode::RoundTrip;
        let comments = match (self.format, self.comments) {
            (Format::Config, Some((comments, attached))) if !exact => {
                CommentTree::new(comments, attached)
            }
            _ => CommentTree::default(),
        };
        let facts = self.facts.filter(|f| !f.is_empty() && !exact);
        let track = facts.is_some() || !comments.is_empty();
        let mut writer = Writer {
            out: String::new(),
            track,
            cursor: vec![(
                facts.map(|_| facts::ROOT),
                (!comments.is_empty()).then_some(0),
            )],
            facts,
            comments,
            mode: self.mode,
            json: exact && matches!(self.format, Format::Json | Format::JsonCompact),
            error: None,
        };
        if exact && !matches!(value, UclValue::Object(_) | UclValue::Array(_)) {
            writer.fail(format!(
                "a root {}: a UCL document is an object or an array (spec §1.1)",
                value.type_name()
            ));
        }
        if exact && crate::value::nesting(value) > crate::parse::MAX_NESTING {
            writer.fail(format!(
                "a value nested more than {} containers deep, the root included (spec §11.2)",
                crate::parse::MAX_NESTING
            ));
            return writer;
        }
        match self.format {
            Format::Json => writer.json_root(value, json::Style::Json),
            Format::JsonCompact => writer.json_root(value, json::Style::Compact),
            Format::Yaml => writer.json_root(value, json::Style::Yaml),
            Format::Config => writer.config_root(value),
        }
        writer
    }
}

/// `value` as pretty JSON (spec §10.4), without output facts: keys as they are in `value`
/// ([`to_config`] says what that leaves out).
pub fn to_json(value: &UclValue) -> String {
    Emitter::new(Format::Json).emit(value)
}

/// `value` as JSON without whitespace (spec §10.4), without output facts, as [`to_json`].
pub fn to_json_compact(value: &UclValue) -> String {
    Emitter::new(Format::JsonCompact).emit(value)
}

/// `value` in the UCL config format (spec §10.5), without output facts: every string in the JSON
/// form, keys as they are in `value` and quoted by [`key_needs_quoting`].
///
/// A [`UclValue`] does not record how its document was written, so this output ignores it:
/// single-quoted strings and heredocs are written in the JSON form, keys lose the spelling and
/// quoting they were written with (spec §10.1), and copies made by `.inherit` get the key of
/// their entry. libucl writes those as they were parsed, and so does
/// [`Parser::emitter`](crate::parse::Parser::emitter), which uses the output facts of the
/// parser's last parse:
///
/// ```
/// use serde_ucl::emit::{self, Format};
/// use serde_ucl::parse::Parser;
///
/// let mut parser = Parser::new();
/// let value = parser.parse(b"name = 'web'").unwrap();
/// assert_eq!(emit::to_config(&value), "name = \"web\";\n");
/// assert_eq!(parser.emitter(Format::Config).emit(&value), "name = 'web';\n");
/// ```
pub fn to_config(value: &UclValue) -> String {
    Emitter::new(Format::Config).emit(value)
}

/// `value` in libucl's YAML format (spec §10.6), without output facts, as [`to_config`]:
/// strings in the JSON form and keys quoted by [`key_needs_quoting`].
pub fn to_yaml(value: &UclValue) -> String {
    Emitter::new(Format::Yaml).emit(value)
}

/// Saved comments by value, as a tree of the values' paths that the writer walks down with the
/// value it writes, so that finding a value's comments takes one step at any depth.
#[derive(Debug, Default)]
struct CommentTree<'a> {
    /// `nodes[0]` is the root's path, when there is any node.
    nodes: Vec<CommentNode<'a>>,
}

#[derive(Debug, Default)]
struct CommentNode<'a> {
    comments: Option<(CommentPlacement, Vec<&'a str>)>,
    children: Children,
}

impl<'a> CommentTree<'a> {
    fn new(comments: &'a [Comment], attached: &'a [AttachedComments]) -> Self {
        let mut tree = Self::default();
        for group in attached {
            let texts = group
                .comments
                .iter()
                .filter_map(|&i| comments.get(i).map(|c| c.text.as_str()))
                .collect();
            let mut node = tree.root();
            for segment in &group.path {
                node = tree.child_or_insert(node, segment);
            }
            tree.nodes[node].comments = Some((group.placement, texts));
        }
        tree
    }

    fn is_empty(&self) -> bool {
        self.nodes.is_empty()
    }

    fn root(&mut self) -> usize {
        if self.nodes.is_empty() {
            self.nodes.push(CommentNode::default());
        }
        0
    }

    fn child_or_insert(&mut self, node: usize, segment: &PathSegment) -> usize {
        if let Some(child) = self.nodes[node].children.get(segment) {
            return child;
        }
        let child = self.nodes.len();
        self.nodes.push(CommentNode::default());
        self.nodes[node].children.insert(segment, child);
        child
    }
}

/// The state of one [`Emitter::emit`]: the output, and where the value being written is in the
/// output facts and the saved comments, when there are any.
struct Writer<'a> {
    out: String,
    facts: Option<&'a OutputFacts>,
    comments: CommentTree<'a>,
    /// There are facts or comments to look up.
    track: bool,
    /// For the value being written and each container it is in, innermost last: its node in the
    /// output facts and in the comment tree, if it has one.
    cursor: Vec<(Option<NodeId>, Option<usize>)>,
    mode: Mode,
    /// Round-trip mode in JSON or compact JSON: numbers are JSON numbers.
    json: bool,
    /// In round-trip mode, the first value that has no exact form.
    error: Option<String>,
}

impl<'a> Writer<'a> {
    /// Records that a value has no exact form (round-trip mode). The first failure is kept.
    fn fail(&mut self, error: String) {
        if self.error.is_none() {
            self.error = Some(error);
        }
    }

    fn indent(&mut self, depth: usize) {
        for _ in 0..depth {
            self.out.push_str("    ");
        }
    }

    /// The nodes of the value being written.
    fn here(&self) -> (Option<NodeId>, Option<usize>) {
        *self.cursor.last().expect("the root is entered")
    }

    /// Enters value `index` of the entry `key`.
    fn enter_key(&mut self, key: &str, index: usize) {
        if self.track {
            let (fact, comment) = self.here();
            let fact = fact.and_then(|n| self.facts?.child_key(n, key, index));
            let comment = comment.and_then(|n| self.comments.nodes[n].children.key(key, index));
            self.cursor.push((fact, comment));
        }
    }

    /// Enters element `index` of an array.
    fn enter_index(&mut self, index: usize) {
        if self.track {
            let (fact, comment) = self.here();
            let fact = fact.and_then(|n| self.facts?.child_element(n, index));
            let comment = comment.and_then(|n| self.comments.nodes[n].children.element(index));
            self.cursor.push((fact, comment));
        }
    }

    fn leave(&mut self) {
        if self.track {
            self.cursor.pop();
        }
    }

    /// The facts of the value being written.
    fn facts(&self) -> Option<&ValueFacts> {
        let (node, _) = self.here();
        self.facts?.facts_of(node?)
    }

    /// Writes the key of the entry value being written, whose entry is `entry_key`: its own
    /// spelling, bare or in the JSON form (spec §10.1). The empty key is written as nothing.
    fn write_key(&mut self, entry_key: &str) {
        if self.mode == Mode::RoundTrip {
            self.exact_key(entry_key, true);
            return;
        }
        let (spelling, quoted) = match self.facts() {
            Some(facts) => {
                let spelling = facts.key_spelling.as_deref().unwrap_or(entry_key);
                let quoted = facts
                    .key_quoted
                    .unwrap_or_else(|| key_needs_quoting(spelling));
                (spelling.to_owned(), quoted)
            }
            None => (entry_key.to_owned(), key_needs_quoting(entry_key)),
        };
        text::write_key(&mut self.out, &spelling, quoted);
    }

    /// The saved comments of the value being written that are attached with `placement`.
    fn comments(&self, placement: CommentPlacement) -> Vec<&'a str> {
        let (_, node) = self.here();
        match node.and_then(|n| self.comments.nodes[n].comments.as_ref()) {
            Some((p, texts)) if *p == placement => texts.clone(),
            _ => Vec::new(),
        }
    }

    /// Round-trip mode: a key that reads back exactly (spec §10.8). Bare where `bare_allowed` and
    /// spec §3.1 allow it, otherwise double-quoted with the escapes of §6.1. The empty key has no
    /// form, since parsing rejects it (§3.2).
    fn exact_key(&mut self, key: &str, bare_allowed: bool) {
        if key.is_empty() {
            self.fail("the empty key, which parsing rejects (spec §3.2, §10.8)".to_owned());
        } else if bare_allowed && text::is_bare_key(key) {
            self.out.push_str(key);
        } else {
            text::write_escaped_string(&mut self.out, key);
        }
    }

    /// Round-trip mode: a scalar in a form that reads back exactly (spec §10.8). Strings of the
    /// config format may use single quotes (`config`); the other formats use double quotes. In
    /// JSON, a float and a time are JSON numbers, the time as its seconds, which read back as a
    /// float.
    fn exact_scalar(&mut self, value: &UclValue, config: bool) {
        let result = match value {
            UclValue::Integer(i) => {
                use std::fmt::Write;
                let _ = write!(self.out, "{i}");
                Ok(())
            }
            UclValue::Float(f) if self.json => {
                number::write_json_number(&mut self.out, *f, "float")
            }
            UclValue::Time(t) if self.json => number::write_json_number(&mut self.out, *t, "time"),
            UclValue::Float(f) => number::write_exact_float(&mut self.out, *f),
            UclValue::Time(t) => number::write_exact_time(&mut self.out, *t),
            UclValue::String(s) if config => text::write_exact_config_string(&mut self.out, s),
            UclValue::String(s) => text::write_exact_double_quoted(&mut self.out, s),
            UclValue::Boolean(b) => {
                self.out.push_str(if *b { "true" } else { "false" });
                Ok(())
            }
            UclValue::Null => {
                self.out.push_str("null");
                Ok(())
            }
            UclValue::Object(_) | UclValue::Array(_) => {
                unreachable!("containers are written by the format")
            }
        };
        if let Err(error) = result {
            self.fail(error);
        }
    }

    /// A scalar in the form every format shares (spec §10.2), strings in the JSON form.
    fn scalar(&mut self, value: &UclValue) {
        if self.mode == Mode::RoundTrip {
            self.exact_scalar(value, false);
            return;
        }
        match value {
            UclValue::Integer(i) => {
                use std::fmt::Write;
                let _ = write!(self.out, "{i}");
            }
            UclValue::Float(f) | UclValue::Time(f) => number::write_float(&mut self.out, *f),
            UclValue::String(s) => text::write_json_string(&mut self.out, s),
            UclValue::Boolean(b) => self.out.push_str(if *b { "true" } else { "false" }),
            UclValue::Null => self.out.push_str("null"),
            UclValue::Object(_) | UclValue::Array(_) => {
                unreachable!("containers are written by the format")
            }
        }
    }
}

#[cfg(test)]
mod tests;