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}