Skip to content
This repository has been archived by the owner on Mar 25, 2024. It is now read-only.

Commit

Permalink
Merge pull request #286 from dtolnay/merge
Browse files Browse the repository at this point in the history
Add a yaml merge key implementation
  • Loading branch information
dtolnay committed Jul 28, 2022
2 parents 23cbbef + 4de6b48 commit f667c2e
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/error.rs
Expand Up @@ -28,6 +28,10 @@ pub(crate) enum ErrorImpl {
RepetitionLimitExceeded,
BytesUnsupported,
UnknownAnchor(libyaml::Mark),
ScalarInMerge,
TaggedInMerge,
ScalarInMergeElement,
SequenceInMergeElement,

Shared(Arc<ErrorImpl>),
}
Expand Down Expand Up @@ -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),
}
}
Expand All @@ -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),
}
}
Expand Down
73 changes: 73 additions & 0 deletions src/value/mod.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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
/// <https://yaml.org/type/merge.html>.
///
/// ```
/// 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 {}
Expand Down
41 changes: 41 additions & 0 deletions 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;
Expand Down Expand Up @@ -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]);
}
}

0 comments on commit f667c2e

Please sign in to comment.