From d87b67106e56b004d8d2f8ca1bcf9e40d2652d87 Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Thu, 28 Jul 2022 00:10:26 -0700 Subject: [PATCH 1/2] Add a yaml merge key implementation --- src/error.rs | 18 ++++++++++++ src/value/mod.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/error.rs b/src/error.rs index a09efb87..d08febe2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,10 @@ pub(crate) enum ErrorImpl { RepetitionLimitExceeded, BytesUnsupported, UnknownAnchor(libyaml::Mark), + ScalarInMerge, + TaggedInMerge, + ScalarInMergeElement, + SequenceInMergeElement, Shared(Arc), } @@ -216,6 +220,16 @@ impl ErrorImpl { f.write_str("serialization and deserialization of bytes in YAML is not implemented") } ErrorImpl::UnknownAnchor(mark) => write!(f, "unknown anchor at {}", mark), + ErrorImpl::ScalarInMerge => { + f.write_str("expected a mapping or list of mappings for merging, but found scalar") + } + ErrorImpl::TaggedInMerge => f.write_str("unexpected tagged value in merge"), + ErrorImpl::ScalarInMergeElement => { + f.write_str("expected a mapping for merging, but found scalar") + } + ErrorImpl::SequenceInMergeElement => { + f.write_str("expected a mapping for merging, but found sequence") + } ErrorImpl::Shared(err) => err.display(f), } } @@ -234,6 +248,10 @@ impl ErrorImpl { ErrorImpl::RepetitionLimitExceeded => f.write_str("RepetitionLimitExceeded"), ErrorImpl::BytesUnsupported => f.write_str("BytesUnsupported"), ErrorImpl::UnknownAnchor(mark) => f.debug_tuple("UnknownAnchor").field(mark).finish(), + ErrorImpl::ScalarInMerge => f.write_str("ScalarInMerge"), + ErrorImpl::TaggedInMerge => f.write_str("TaggedInMerge"), + ErrorImpl::ScalarInMergeElement => f.write_str("ScalarInMergeElement"), + ErrorImpl::SequenceInMergeElement => f.write_str("SequenceInMergeElement"), ErrorImpl::Shared(err) => err.debug(f), } } diff --git a/src/value/mod.rs b/src/value/mod.rs index 0979dbcb..98b89762 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -7,6 +7,7 @@ mod partial_eq; mod ser; mod tagged; +use crate::error::{self, ErrorImpl}; use crate::{Error, Mapping}; use serde::de::{Deserialize, DeserializeOwned, IntoDeserializer}; use serde::Serialize; @@ -593,6 +594,78 @@ impl Value { _ => None, } } + + /// Performs merging of `<<` keys into the surrounding mapping. + /// + /// The intended use of this in YAML is described in + /// . + /// + /// ``` + /// use serde_yaml::Value; + /// + /// let config = "\ + /// tasks: + /// build: &webpack_shared + /// command: webpack + /// args: build + /// inputs: + /// - 'src/**/*' + /// start: + /// <<: *webpack_shared + /// args: start + /// "; + /// + /// let mut value: Value = serde_yaml::from_str(config).unwrap(); + /// value.apply_merge().unwrap(); + /// + /// assert_eq!(value["tasks"]["start"]["command"], "webpack"); + /// assert_eq!(value["tasks"]["start"]["args"], "start"); + /// ``` + pub fn apply_merge(&mut self) -> Result<(), Error> { + let mut stack = Vec::new(); + stack.push(self); + while let Some(node) = stack.pop() { + match node { + Value::Mapping(mapping) => { + match mapping.remove("<<") { + Some(Value::Mapping(merge)) => { + for (k, v) in merge { + mapping.entry(k).or_insert(v); + } + } + Some(Value::Sequence(sequence)) => { + for value in sequence { + match value { + Value::Mapping(merge) => { + for (k, v) in merge { + mapping.entry(k).or_insert(v); + } + } + Value::Sequence(_) => { + return Err(error::new(ErrorImpl::SequenceInMergeElement)); + } + Value::Tagged(_) => { + return Err(error::new(ErrorImpl::TaggedInMerge)); + } + _unexpected => { + return Err(error::new(ErrorImpl::ScalarInMergeElement)); + } + } + } + } + None => {} + Some(Value::Tagged(_)) => return Err(error::new(ErrorImpl::TaggedInMerge)), + Some(_unexpected) => return Err(error::new(ErrorImpl::ScalarInMerge)), + } + stack.extend(mapping.values_mut()); + } + Value::Sequence(sequence) => stack.extend(sequence), + Value::Tagged(tagged) => stack.push(&mut tagged.value), + _ => {} + } + } + Ok(()) + } } impl Eq for Value {} From 4de6b48d6004c6b3eb4113a52da1c2b814aae840 Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Thu, 28 Jul 2022 09:56:38 -0700 Subject: [PATCH 2/2] Add yaml merge test from draft spec --- tests/test_value.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_value.rs b/tests/test_value.rs index 67b522a7..96448e84 100644 --- a/tests/test_value.rs +++ b/tests/test_value.rs @@ -1,5 +1,6 @@ #![allow(clippy::derive_partial_eq_without_eq, clippy::eq_op)] +use indoc::indoc; use serde::de::IntoDeserializer; use serde::Deserialize; use serde_derive::Deserialize; @@ -52,3 +53,43 @@ fn test_into_deserializer() { } ); } + +#[test] +fn test_merge() { + // From https://yaml.org/type/merge.html. + let yaml = indoc! {" + --- + - &CENTER { x: 1, y: 2 } + - &LEFT { x: 0, y: 2 } + - &BIG { r: 10 } + - &SMALL { r: 1 } + + # All the following maps are equal: + + - # Explicit keys + x: 1 + y: 2 + r: 10 + label: center/big + + - # Merge one map + << : *CENTER + r: 10 + label: center/big + + - # Merge multiple maps + << : [ *CENTER, *BIG ] + label: center/big + + - # Override + << : [ *BIG, *LEFT, *SMALL ] + x: 1 + label: center/big + "}; + + let mut value: Value = serde_yaml::from_str(yaml).unwrap(); + value.apply_merge().unwrap(); + for i in 5..=7 { + assert_eq!(value[4], value[i]); + } +}