use std::fmt;
use serde::de::{DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
use serde_yaml::{Mapping, Value};
pub fn parse_merged(content: &str) -> Result<Value, serde_yaml::Error> {
let mut documents: Vec<Value> = serde_yaml::Deserializer::from_str(content)
.map(|document| Tolerant.deserialize(document))
.collect::<Result<_, _>>()?;
if documents.is_empty() {
return Ok(Value::Null);
}
let mut merged = documents.remove(0);
for document in documents {
merged = merge_values(merged, document);
}
resolve_merge_keys(&mut merged);
Ok(merged)
}
struct Tolerant;
impl<'de> DeserializeSeed<'de> for Tolerant {
type Value = Value;
fn deserialize<D>(self, deserializer: D) -> Result<Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(TolerantVisitor)
}
}
struct TolerantVisitor;
impl<'de> Visitor<'de> for TolerantVisitor {
type Value = Value;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("any YAML value")
}
fn visit_bool<E>(self, value: bool) -> Result<Value, E> {
Ok(Value::Bool(value))
}
fn visit_i64<E>(self, value: i64) -> Result<Value, E> {
Ok(Value::Number(value.into()))
}
fn visit_u64<E>(self, value: u64) -> Result<Value, E> {
Ok(Value::Number(value.into()))
}
fn visit_f64<E>(self, value: f64) -> Result<Value, E> {
Ok(Value::Number(serde_yaml::Number::from(value)))
}
fn visit_str<E>(self, value: &str) -> Result<Value, E> {
Ok(Value::String(value.to_string()))
}
fn visit_string<E>(self, value: String) -> Result<Value, E> {
Ok(Value::String(value))
}
fn visit_unit<E>(self) -> Result<Value, E> {
Ok(Value::Null)
}
fn visit_none<E>(self) -> Result<Value, E> {
Ok(Value::Null)
}
fn visit_some<D>(self, deserializer: D) -> Result<Value, D::Error>
where
D: Deserializer<'de>,
{
Tolerant.deserialize(deserializer)
}
fn visit_enum<A>(self, data: A) -> Result<Value, A::Error>
where
A: serde::de::EnumAccess<'de>,
{
use serde::de::VariantAccess;
let (_, access) = data.variant_seed(Tolerant)?;
access.newtype_variant_seed(Tolerant)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut values = Vec::new();
while let Some(value) = seq.next_element_seed(Tolerant)? {
values.push(value);
}
Ok(Value::Sequence(values))
}
fn visit_map<A>(self, mut map: A) -> Result<Value, A::Error>
where
A: MapAccess<'de>,
{
let mut mapping = Mapping::new();
while let Some((key, value)) = map.next_entry_seed(Tolerant, Tolerant)? {
if key.as_str() == Some("<<") {
match mapping.get_mut(&key) {
Some(existing) => {
let mut merged = match existing.clone() {
Value::Sequence(sequence) => sequence,
other => vec![other],
};
merged.push(value);
mapping.insert(key, Value::Sequence(merged));
}
None => {
mapping.insert(key, value);
}
}
} else {
mapping.insert(key, value);
}
}
Ok(Value::Mapping(mapping))
}
}
fn merge_values(base: Value, over: Value) -> Value {
match (base, over) {
(Value::Mapping(mut base), Value::Mapping(over)) => {
for (key, value) in over {
match base.remove(&key) {
Some(existing) => {
base.insert(key, merge_values(existing, value));
}
None => {
base.insert(key, value);
}
}
}
Value::Mapping(base)
}
(_, over) => over,
}
}
fn resolve_merge_keys(value: &mut Value) {
match value {
Value::Mapping(mapping) => {
let merges: Vec<Value> = mapping
.iter()
.filter(|(key, _)| key.as_str() == Some("<<"))
.map(|(_, merge)| merge.clone())
.collect();
if !merges.is_empty() {
let mut merged = Value::Mapping(Mapping::new());
for merge in merges.iter().rev() {
match merge {
Value::Mapping(_) => {
let mut merge = merge.clone();
resolve_merge_keys(&mut merge);
merged = merge_values(merged, merge);
}
Value::Sequence(sequence) => {
for item in sequence.iter().rev() {
let mut item = item.clone();
resolve_merge_keys(&mut item);
if let Value::Mapping(_) = item {
merged = merge_values(merged, item);
}
}
}
_ => {}
}
}
let explicit: Mapping = mapping
.iter()
.filter(|(key, _)| key.as_str() != Some("<<"))
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
let final_mapping = merge_values(merged, Value::Mapping(explicit));
if let Value::Mapping(final_mapping) = final_mapping {
*mapping = final_mapping;
}
}
for (_, value) in mapping.iter_mut() {
resolve_merge_keys(value);
}
}
Value::Sequence(sequence) => {
for item in sequence.iter_mut() {
resolve_merge_keys(item);
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merges_multiple_documents_later_wins() {
let value = parse_merged("a: 1\n---\nb: 2\n---\na: 3\n").unwrap();
let mapping = value.as_mapping().unwrap();
assert_eq!(mapping.get("a").and_then(Value::as_i64), Some(3));
assert_eq!(mapping.get("b").and_then(Value::as_i64), Some(2));
}
#[test]
fn merges_nested_mappings_across_documents() {
let value = parse_merged("job:\n env:\n A: 1\n---\njob:\n env:\n B: 2\n").unwrap();
let env = value["job"]["env"].as_mapping().unwrap();
assert_eq!(env.get("A").and_then(Value::as_i64), Some(1));
assert_eq!(env.get("B").and_then(Value::as_i64), Some(2));
}
#[test]
fn resolves_single_merge_key() {
let value = parse_merged(
"base: &b\n image: debian\n tags: [x]\njob:\n <<: *b\n script: echo hi\n",
)
.unwrap();
let job = value["job"].as_mapping().unwrap();
assert_eq!(job.get("image").and_then(Value::as_str), Some("debian"));
assert_eq!(job.get("script").and_then(Value::as_str), Some("echo hi"));
assert!(!job.contains_key(Value::String("<<".to_string())));
}
#[test]
fn resolves_two_merge_keys() {
let value = parse_merged(
"first: &f\n image: one\ntags: &t\n tags: [x]\njob:\n <<: *f\n <<: *t\n script: run\n",
)
.unwrap();
let job = value["job"].as_mapping().unwrap();
assert_eq!(job.get("image").and_then(Value::as_str), Some("one"));
assert_eq!(
job.get("tags").and_then(Value::as_sequence).unwrap()[0].as_str(),
Some("x")
);
assert_eq!(job.get("script").and_then(Value::as_str), Some("run"));
}
#[test]
fn earlier_merge_wins_on_conflict() {
let value = parse_merged(
"first: &f\n rules: [first]\nsecond: &s\n rules: [second]\njob:\n <<: *f\n <<: *s\n",
)
.unwrap();
let job = value["job"].as_mapping().unwrap();
assert_eq!(
job.get("rules").and_then(Value::as_sequence).unwrap()[0].as_str(),
Some("first")
);
}
#[test]
fn explicit_keys_win_over_merge() {
let value =
parse_merged("base: &b\n image: debian\njob:\n <<: *b\n image: custom\n").unwrap();
assert_eq!(value["job"]["image"].as_str(), Some("custom"));
}
#[test]
fn merges_sequence_of_merge_values() {
let value =
parse_merged("one: &o\n a: 1\ntwo: &t\n b: 2\njob:\n <<: [*o, *t]\n").unwrap();
let job = value["job"].as_mapping().unwrap();
assert_eq!(job.get("a").and_then(Value::as_i64), Some(1));
assert_eq!(job.get("b").and_then(Value::as_i64), Some(2));
}
#[test]
fn keeps_unknown_tagged_values() {
let value = parse_merged(
"job:\n before_script:\n - !reference [.template, before_script]\n - echo hi\n",
)
.unwrap();
let script = value["job"]["before_script"].as_sequence().unwrap();
assert_eq!(
script[0].as_sequence().unwrap()[0].as_str(),
Some(".template")
);
assert_eq!(script[1].as_str(), Some("echo hi"));
}
#[test]
fn resolves_chained_merge_keys() {
let value = parse_merged(
"base: &b\n image: debian\n tags: [x]\nmid: &m\n <<: *b\n script: build\njob:\n <<: *m\n",
)
.unwrap();
let job = value["job"].as_mapping().unwrap();
assert_eq!(job.get("image").and_then(Value::as_str), Some("debian"));
assert_eq!(job.get("script").and_then(Value::as_str), Some("build"));
}
}