Skip to main content

oneyaml_core/
settings.rs

1//! The single settings value type and its presets.
2
3use crate::{Emit, Layout, Limits};
4
5/// The schema that resolves plain scalars to types.
6#[non_exhaustive]
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
8pub enum Schema {
9    /// The YAML 1.2 Core schema (the default).
10    #[default]
11    Core,
12    /// The YAML 1.2 JSON schema: strict JSON types.
13    Json,
14    /// The YAML 1.2 Failsafe schema: every scalar is a string.
15    Failsafe,
16}
17
18/// What to do with a duplicate mapping key.
19///
20/// `Document` and the serde deserializer read it alike.
21#[non_exhaustive]
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
23pub enum DuplicateKeys {
24    /// An error, as YAML 1.2 requires (the default).
25    #[default]
26    Error,
27    /// The last value wins, and a warning with a "first defined here" label
28    /// is **always** recorded. Part of [`Compat::serde_yaml()`].
29    LastWins,
30}
31
32/// All input and output settings.
33///
34/// A value type with private fields and chainable `const fn` setters, so a
35/// configuration can be a `const`:
36///
37/// ```
38/// use oneyaml_core::{Limits, Settings};
39/// const CFG: Settings = Settings::new().merge_keys(true).max_depth(512);
40/// const UNTRUSTED: Settings = Settings::new().limits(Limits::untrusted());
41/// # assert!(CFG.get_merge_keys());
42/// ```
43///
44/// Settings are always passed explicitly; there are no globals and no
45/// environment variables. Every departure from YAML 1.2 is off by default.
46///
47/// # Settings, by the layer that consumes them
48///
49/// * **`merge_keys`** — YAML 1.1 merge keys (`<<`). *Resolver / `Value` /
50///   serde* (later unit).
51/// * Other YAML 1.1 behaviours: **`yaml11_booleans`**, **`legacy_octal`**,
52///   **`sexagesimal`**, **`digit_separators`**, **`yaml11_int_forms`**.
53///   *Resolver*.
54/// * **`schema`** — Core (default), JSON, or Failsafe. *Resolver* (later
55///   unit).
56/// * **`duplicate_keys`** — *tree builder and serde* (later unit).
57/// * **`honor_version_directive`** — `%YAML 1.1` switches that document to
58///   the [`Compat::yaml_1_1()`] behaviours. *Resolver* (later unit); the
59///   parser reports the directive either way.
60/// * **`lenient_inline_colons`** — *parser* (consumed now).
61/// * **`resolve_tagged_variant_content`** — *serde deserializer* (typed
62///   enum reads of `!Name value`, also through `from_value_with` and
63///   `NodeRef::deserialize`) and *serializer* ([`Layout::SerdeYaml`]).
64/// * **`limits`** ([`Limits`]) — `max_depth`, `max_scalar_len`,
65///   `max_document_size`, and `max_documents` are enforced by the *parser*
66///   now; `alias_budget` and `alias_byte_budget` by `Value`/serde reads.
67/// * **`emit`** ([`Emit`]) — *serializer*. The serializer also enforces
68///   `max_depth` on `Value`s and `max_typed_depth` on Rust types, and
69///   refuses the JSON and Failsafe schemas (its output is written for
70///   Core).
71///
72/// # Examples
73///
74/// Every setter has a getter of the same name with `get_`:
75///
76/// ```
77/// use oneyaml_core::{DuplicateKeys, Emit, Limits, Schema, Settings};
78///
79/// // A YAML 1.1 reader for one legacy file, spelled out by hand.
80/// let s = Settings::new()
81///     .merge_keys(true)
82///     .yaml11_booleans(true)
83///     .legacy_octal(true)
84///     .sexagesimal(true)
85///     .digit_separators(true)
86///     .yaml11_int_forms(true)
87///     .schema(Schema::Core)
88///     .duplicate_keys(DuplicateKeys::LastWins)
89///     .honor_version_directive(true)
90///     .lenient_inline_colons(false)
91///     .resolve_tagged_variant_content(false)
92///     .limits(Limits::untrusted())
93///     .max_depth(64)
94///     .emit(Emit::new())
95///     .indent(4);
96/// assert!(s.get_merge_keys() && s.get_yaml11_booleans() && s.get_legacy_octal());
97/// assert!(s.get_sexagesimal() && s.get_digit_separators() && s.get_yaml11_int_forms());
98/// assert_eq!(s.get_schema(), Schema::Core);
99/// assert_eq!(s.get_duplicate_keys(), DuplicateKeys::LastWins);
100/// assert!(s.get_honor_version_directive());
101/// assert!(!s.get_lenient_inline_colons() && !s.get_resolve_tagged_variant_content());
102/// // `max_depth` and `indent` are shortcuts into `limits` and `emit`.
103/// assert_eq!(s.get_limits().get_max_depth(), 64);
104/// assert_eq!(s.get_emit().get_indent(), 4);
105/// // The same 1.1 behaviours as a preset (limits stay the defaults).
106/// let preset = oneyaml_core::Compat::yaml_1_1();
107/// assert!(preset.get_yaml11_booleans() && preset.get_limits() == &Limits::default());
108/// ```
109// One value type; Clone, Debug, PartialEq, Eq, Hash.
110#[derive(Clone, Debug, PartialEq, Eq, Hash)]
111pub struct Settings {
112    merge_keys: bool,
113    yaml11_booleans: bool,
114    legacy_octal: bool,
115    sexagesimal: bool,
116    digit_separators: bool,
117    yaml11_int_forms: bool,
118    schema: Schema,
119    duplicate_keys: DuplicateKeys,
120    honor_version_directive: bool,
121    lenient_inline_colons: bool,
122    resolve_tagged_variant_content: bool,
123    limits: Limits,
124    emit: Emit,
125}
126
127impl Settings {
128    /// YAML 1.2, Core schema, default limits, default output. Same as
129    /// [`Settings::default()`], usable in `const`.
130    #[must_use]
131    pub const fn new() -> Self {
132        Self {
133            merge_keys: false,
134            yaml11_booleans: false,
135            legacy_octal: false,
136            sexagesimal: false,
137            digit_separators: false,
138            yaml11_int_forms: false,
139            schema: Schema::Core,
140            duplicate_keys: DuplicateKeys::Error,
141            honor_version_directive: false,
142            lenient_inline_colons: false,
143            resolve_tagged_variant_content: false,
144            limits: Limits::new(),
145            emit: Emit::new(),
146        }
147    }
148
149    /// **YAML 1.1 merge keys (`<<`)**, e.g. for docker-compose files and
150    /// GitLab CI. Off by default. Merges through aliases count toward
151    /// [`Limits::alias_budget`].
152    ///
153    /// YAML 1.2 has no merge keys: without this setting `<<` is an
154    /// ordinary string key, as the spec says. [`Compat::yaml_1_1()`] turns
155    /// it on with every other YAML 1.1 behaviour; [`Compat::serde_yaml()`]
156    /// leaves it off, as `serde_yaml`'s deserializer does. A `Document`
157    /// keeps `<<` entries as written and edits them like any other entry.
158    #[must_use]
159    pub const fn merge_keys(mut self, on: bool) -> Self {
160        self.merge_keys = on;
161        self
162    }
163
164    /// YAML 1.1 booleans (`yes`/`no`/`on`/`off`/`y`/`n`, …). Off by default.
165    #[must_use]
166    pub const fn yaml11_booleans(mut self, on: bool) -> Self {
167        self.yaml11_booleans = on;
168        self
169    }
170
171    /// YAML 1.1 legacy octal integers (`0777`). Off by default.
172    #[must_use]
173    pub const fn legacy_octal(mut self, on: bool) -> Self {
174        self.legacy_octal = on;
175        self
176    }
177
178    /// YAML 1.1 sexagesimal (base-60) numbers (`12:30`). Off by default.
179    #[must_use]
180    pub const fn sexagesimal(mut self, on: bool) -> Self {
181        self.sexagesimal = on;
182        self
183    }
184
185    /// YAML 1.1 digit separators (`1_000`). Off by default.
186    #[must_use]
187    pub const fn digit_separators(mut self, on: bool) -> Self {
188        self.digit_separators = on;
189        self
190    }
191
192    /// YAML 1.1 integer forms that YAML 1.2 dropped: binary (`0b1010`,
193    /// optionally signed) and a sign on hexadecimal (`-0x1F`, `+0x1f`).
194    /// Off by default. `_` separators in these forms still need
195    /// [`Settings::digit_separators`]; 1.1 octal, signed or not (`017`,
196    /// `-017`), is [`Settings::legacy_octal`]. A signed `0o` (`-0o17`) is
197    /// neither YAML 1.1 nor 1.2 and stays a string under every setting.
198    #[must_use]
199    pub const fn yaml11_int_forms(mut self, on: bool) -> Self {
200        self.yaml11_int_forms = on;
201        self
202    }
203
204    /// The schema (default [`Schema::Core`]).
205    #[must_use]
206    pub const fn schema(mut self, schema: Schema) -> Self {
207        self.schema = schema;
208        self
209    }
210
211    /// Duplicate-key handling (default [`DuplicateKeys::Error`]).
212    #[must_use]
213    pub const fn duplicate_keys(mut self, policy: DuplicateKeys) -> Self {
214        self.duplicate_keys = policy;
215        self
216    }
217
218    /// Let `%YAML 1.1` switch its document to the YAML 1.1 behaviours of
219    /// [`Compat::yaml_1_1()`]; `%YAML 1.0` gets the same preset, 1.1 being
220    /// the closest supported version. Off by default: such documents are
221    /// read as YAML 1.2 with a warning.
222    #[must_use]
223    pub const fn honor_version_directive(mut self, on: bool) -> Self {
224        self.honor_version_directive = on;
225        self
226    }
227
228    /// Leniency, not YAML: read a same-line `: ` inside an unquoted block
229    /// value as text instead of an error, e.g. `title: The farm: a banana
230    /// story` becomes the string `The farm: a banana story`. Off by
231    /// default; when it applies, a warning is **always** recorded. Flow
232    /// context is unaffected, and no valid input changes meaning. Not part
233    /// of any [`Compat`] preset.
234    #[must_use]
235    pub const fn lenient_inline_colons(mut self, on: bool) -> Self {
236        self.lenient_inline_colons = on;
237        self
238    }
239
240    /// `serde_yaml` compatibility, not YAML 1.2: when a **typed enum** reads
241    /// a locally tagged plain scalar (`!Name value`) as its variant `Name`,
242    /// the content is resolved as an untagged plain scalar under the Core
243    /// schema (with the document's YAML 1.1 integer, boolean, … settings),
244    /// so `!Square 2.0` reads as `Square(2.0)` and `!Enabled true` as
245    /// `Enabled(true)`. This is how `serde_yaml` writes a newtype variant
246    /// around a number, bool, or null; with the setting on,
247    /// [`Layout::SerdeYaml`] writes such variants that way too.
248    ///
249    /// Off by default: YAML 1.2 leaves the meaning of a local tag to the
250    /// application (§6.9.2), and without the setting a custom-tagged scalar
251    /// is the tagged *string*. On in [`Compat::serde_yaml()`] only.
252    /// Only typed enum reads change: `Value` keeps
253    /// `Tagged("!Square", String("2.0"))`, untyped reads (`deserialize_any`)
254    /// are unchanged, and quoted content stays a string (`!Square '2.0'` is
255    /// text; `from_value_with` cannot tell, as a `Value` keeps no style, and
256    /// there resolves to a bool or a number only, never to null).
257    /// Global and `%TAG`-expanded tags are not local and never resolve.
258    ///
259    /// Under this setting (with [`Layout::SerdeYaml`]) the streaming
260    /// serializer (`to_string_with` and friends) writes such a variant as
261    /// `!Name v`, while `to_value_with` followed by emitting the `Value`
262    /// writes `Name: v`: a `Value` has no enums, and its tagged scalars are
263    /// strings, so the meaning-level map form is the only `Value` that reads
264    /// back as the variant.
265    #[must_use]
266    pub const fn resolve_tagged_variant_content(mut self, on: bool) -> Self {
267        self.resolve_tagged_variant_content = on;
268        self
269    }
270
271    /// Replaces the resource limits (default [`Limits::default()`]).
272    #[must_use]
273    pub const fn limits(mut self, limits: Limits) -> Self {
274        self.limits = limits;
275        self
276    }
277
278    /// Shortcut for [`Limits::max_depth`] on the current limits.
279    #[must_use]
280    pub const fn max_depth(mut self, depth: usize) -> Self {
281        self.limits = self.limits.max_depth(depth);
282        self
283    }
284
285    /// Replaces the output settings (default [`Emit::default()`]).
286    #[must_use]
287    pub const fn emit(mut self, emit: Emit) -> Self {
288        self.emit = emit;
289        self
290    }
291
292    /// Shortcut for [`Emit::indent`] on the current output settings.
293    #[must_use]
294    pub const fn indent(mut self, spaces: usize) -> Self {
295        self.emit = self.emit.indent(spaces);
296        self
297    }
298
299    /// Whether merge keys are on.
300    #[must_use]
301    pub const fn get_merge_keys(&self) -> bool {
302        self.merge_keys
303    }
304
305    /// Whether YAML 1.1 booleans are on.
306    #[must_use]
307    pub const fn get_yaml11_booleans(&self) -> bool {
308        self.yaml11_booleans
309    }
310
311    /// Whether YAML 1.1 legacy octal is on.
312    #[must_use]
313    pub const fn get_legacy_octal(&self) -> bool {
314        self.legacy_octal
315    }
316
317    /// Whether YAML 1.1 sexagesimal numbers are on.
318    #[must_use]
319    pub const fn get_sexagesimal(&self) -> bool {
320        self.sexagesimal
321    }
322
323    /// Whether YAML 1.1 digit separators are on.
324    #[must_use]
325    pub const fn get_digit_separators(&self) -> bool {
326        self.digit_separators
327    }
328
329    /// Whether the YAML 1.1 integer forms (binary, signed hex/octal) are on.
330    #[must_use]
331    pub const fn get_yaml11_int_forms(&self) -> bool {
332        self.yaml11_int_forms
333    }
334
335    /// The schema.
336    #[must_use]
337    pub const fn get_schema(&self) -> Schema {
338        self.schema
339    }
340
341    /// The duplicate-key policy.
342    #[must_use]
343    pub const fn get_duplicate_keys(&self) -> DuplicateKeys {
344        self.duplicate_keys
345    }
346
347    /// Whether `%YAML 1.1` switches to the YAML 1.1 behaviours.
348    #[must_use]
349    pub const fn get_honor_version_directive(&self) -> bool {
350        self.honor_version_directive
351    }
352
353    /// Whether inline colons in unquoted block values are read as text.
354    #[must_use]
355    pub const fn get_lenient_inline_colons(&self) -> bool {
356        self.lenient_inline_colons
357    }
358
359    /// Whether typed enum reads resolve a locally tagged plain scalar's
360    /// content (`!Square 2.0` → `Square(2.0)`).
361    #[must_use]
362    pub const fn get_resolve_tagged_variant_content(&self) -> bool {
363        self.resolve_tagged_variant_content
364    }
365
366    /// The resource limits.
367    #[must_use]
368    pub const fn get_limits(&self) -> &Limits {
369        &self.limits
370    }
371
372    /// The output settings.
373    #[must_use]
374    pub const fn get_emit(&self) -> &Emit {
375        &self.emit
376    }
377}
378
379impl Default for Settings {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385/// Compatibility presets. Each returns a [`Settings`] that can be
386/// refined further; presets never change [`Limits`].
387#[non_exhaustive]
388#[derive(Clone, Copy, Debug)]
389pub struct Compat;
390
391impl Compat {
392    /// Behave like `serde_yaml` for migration: duplicate keys are tolerated
393    /// (last wins, with a warning), the YAML 1.1 integer forms `serde_yaml`
394    /// reads as integers are integers ([`Settings::yaml11_int_forms`]:
395    /// `0b1010`, `-0x10`, `+0x10`), and output follows `serde_yaml`'s layout
396    /// (non-indented sequences, [`Layout::SerdeYaml`]), and typed enums
397    /// read and write newtype variants around numbers, bools, and nulls as
398    /// tagged scalars (`!Square 2.0`,
399    /// [`Settings::resolve_tagged_variant_content`]).
400    ///
401    /// The remaining reading differences from `serde_yaml` are listed, with
402    /// their reasons, in `docs/migrating-from-serde-yaml.md`.
403    ///
404    /// A preset is a starting point: refine it like any [`Settings`], here
405    /// for a `docker-compose.yml` that relies on merge keys:
406    ///
407    /// ```
408    /// use oneyaml_core::{Compat, DuplicateKeys, Layout, Settings};
409    ///
410    /// const COMPOSE: Settings = Compat::serde_yaml().merge_keys(true);
411    ///
412    /// assert_eq!(COMPOSE.get_duplicate_keys(), DuplicateKeys::LastWins);
413    /// assert!(COMPOSE.get_yaml11_int_forms() && COMPOSE.get_merge_keys());
414    /// assert_eq!(COMPOSE.get_emit().get_layout(), Layout::SerdeYaml);
415    /// // Presets never change limits.
416    /// assert_eq!(COMPOSE.get_limits(), Settings::new().get_limits());
417    /// ```
418    #[must_use]
419    pub const fn serde_yaml() -> Settings {
420        Settings::new()
421            .duplicate_keys(DuplicateKeys::LastWins)
422            // `serde_yaml` reads `0b…` and signed `0x…` as integers
423            // (the compat differential); an existing named setting.
424            .yaml11_int_forms(true)
425            // `!Square 2.0` reads into
426            // `Square(2.0)` in typed enums, and is written that way.
427            .resolve_tagged_variant_content(true)
428            .emit(
429                Emit::new()
430                    .indent_sequences(false)
431                    .layout(Layout::SerdeYaml),
432            )
433    }
434
435    /// Full YAML 1.1 typing: merge keys, 1.1 booleans, legacy octal,
436    /// sexagesimal numbers, digit separators, and the 1.1 integer forms.
437    #[must_use]
438    pub const fn yaml_1_1() -> Settings {
439        Settings::new()
440            .merge_keys(true)
441            .yaml11_booleans(true)
442            .legacy_octal(true)
443            .sexagesimal(true)
444            .digit_separators(true)
445            .yaml11_int_forms(true)
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::{Layout, QuoteStyle};
453
454    /// Tripwire: `Schema` is `#[non_exhaustive]`, so downstream
455    /// matches need a wildcard. This exhaustive match (legal only inside
456    /// this crate) stops compiling when a variant is added: give it a
457    /// resolver in `oneyaml`'s `Resolver::for_document` first, whose
458    /// wildcard otherwise reports every node as Y9000.
459    #[test]
460    fn schema_variants_are_all_resolved() {
461        for s in [Schema::Core, Schema::Json, Schema::Failsafe] {
462            match s {
463                Schema::Core | Schema::Json | Schema::Failsafe => {}
464            }
465        }
466    }
467
468    /// Tripwire, as for `Schema`: handle a new `DuplicateKeys`
469    /// variant in `oneyaml`'s `on_duplicate` first, whose wildcard
470    /// otherwise fails every duplicate with Y9000.
471    #[test]
472    fn duplicate_keys_variants_are_all_handled() {
473        for p in [DuplicateKeys::Error, DuplicateKeys::LastWins] {
474            match p {
475                DuplicateKeys::Error | DuplicateKeys::LastWins => {}
476            }
477        }
478    }
479
480    #[test]
481    fn defaults_are_yaml_1_2() {
482        let s = Settings::default();
483        assert_eq!(s, Settings::new());
484        assert!(!s.get_merge_keys());
485        assert!(!s.get_yaml11_booleans());
486        assert!(!s.get_legacy_octal());
487        assert!(!s.get_sexagesimal());
488        assert!(!s.get_digit_separators());
489        assert!(!s.get_yaml11_int_forms());
490        assert_eq!(s.get_schema(), Schema::Core);
491        assert_eq!(s.get_duplicate_keys(), DuplicateKeys::Error);
492        assert!(!s.get_honor_version_directive());
493        assert!(!s.get_lenient_inline_colons());
494        assert!(!s.get_resolve_tagged_variant_content());
495        assert_eq!(s.get_limits(), &Limits::default());
496        assert_eq!(s.get_emit(), &Emit::default());
497    }
498
499    #[test]
500    fn every_setter_chains_in_const() {
501        const S: Settings = Settings::new()
502            .merge_keys(true)
503            .yaml11_booleans(true)
504            .legacy_octal(true)
505            .sexagesimal(true)
506            .digit_separators(true)
507            .yaml11_int_forms(true)
508            .schema(Schema::Failsafe)
509            .duplicate_keys(DuplicateKeys::LastWins)
510            .honor_version_directive(true)
511            .lenient_inline_colons(true)
512            .resolve_tagged_variant_content(true)
513            .limits(Limits::untrusted())
514            .max_depth(5)
515            .emit(Emit::new().quote_style(QuoteStyle::Double))
516            .indent(3);
517        assert!(S.get_merge_keys() && S.get_yaml11_booleans() && S.get_legacy_octal());
518        assert!(S.get_sexagesimal() && S.get_digit_separators() && S.get_yaml11_int_forms());
519        assert!(S.get_honor_version_directive() && S.get_lenient_inline_colons());
520        assert!(S.get_resolve_tagged_variant_content());
521        assert_eq!(S.get_schema(), Schema::Failsafe);
522        assert_eq!(S.get_duplicate_keys(), DuplicateKeys::LastWins);
523        assert_eq!(S.get_limits(), &Limits::untrusted().max_depth(5));
524        assert_eq!(
525            S.get_emit(),
526            &Emit::new().quote_style(QuoteStyle::Double).indent(3)
527        );
528        assert_eq!(Schema::default(), Schema::Core);
529        assert_eq!(DuplicateKeys::default(), DuplicateKeys::Error);
530    }
531
532    #[test]
533    fn presets() {
534        let s = Compat::serde_yaml();
535        assert_eq!(s.get_duplicate_keys(), DuplicateKeys::LastWins);
536        assert_eq!(s.get_emit().get_layout(), Layout::SerdeYaml);
537        assert!(!s.get_emit().get_indent_sequences());
538        assert!(!s.get_merge_keys() && !s.get_lenient_inline_colons());
539        assert!(s.get_yaml11_int_forms() && s.get_resolve_tagged_variant_content());
540        assert!(!s.get_yaml11_booleans() && !s.get_legacy_octal());
541        assert!(!s.get_sexagesimal() && !s.get_digit_separators());
542
543        let y = Compat::yaml_1_1();
544        assert!(y.get_merge_keys() && y.get_yaml11_booleans() && y.get_legacy_octal());
545        assert!(y.get_sexagesimal() && y.get_digit_separators() && y.get_yaml11_int_forms());
546        assert!(!y.get_lenient_inline_colons() && !y.get_resolve_tagged_variant_content());
547        assert_eq!(y.get_schema(), Schema::Core);
548
549        // Presets never change limits.
550        for preset in [s, y] {
551            assert_eq!(preset.get_limits(), &Limits::default());
552        }
553        // Overrides chain on presets.
554        let o = Compat::serde_yaml().duplicate_keys(DuplicateKeys::Error);
555        assert_eq!(o.get_duplicate_keys(), DuplicateKeys::Error);
556    }
557}