Repository: mitsuhiko/minijinja (language: rust)

Issue:
Rendering template using yaml data file containing anchors
## Description

Currently yaml anchors inside yaml data files are not fully supported because of serde_yaml author refused to implement them. There is wonderful [yaml-rust2](https://crates.io/crates/yaml-rust2) that could help mitigate the issue. Would you mind to consider migration from serde_yaml to yaml-rust2?

Additional helpful information:

- Version of minijinja: 2.0.1
- Version of rustc: 1.78
- Operating system and version: Arch Linux

## What did you expect

Structures like:
```yaml
dict1: &dict1_anchor
  key1: value1
  key2: value2

dict2:
  <<: *dict1_anchor
```
should serialize into:
```yaml
dict1:
  key1: value1
  key2: value2

dict2:
  key1: value1
  key2: value2
```


Discussion / hints:
I would love to support this. Unfortunately `yaml-rust2` does not support serde today. `serde_yaml2` which uses `yaml-rust2` does support serde, but it does not support anchors: https://github.com/zim32/serde_yaml2/blob/06d2d5ecca84c6d3c0d5e2f3cbac75bf2cc97e1e/src/de/mod.rs#L275-L277

So the actual work required here is quite high. Ideally support for this lands in a YAML library that directly supports serde.
Well, the library you mentioned doesn't seem to be well maintained too unfortunately. Also, is it that hard to convert `serde_yaml`'s `Value` into `yaml-rust2`'s `Yaml` (by implementing `TryFrom` or something)? Sorry for naive question, I'm a novice in Rust.
I cannot say because I'm not entirely sure how `Alias` is being handled in `yaml-rust2`. With buffering it wouldn't be too hard would be my guess but again, I cannot say. In general that is something I would  not want to maintain as part of MiniJinja. If functionality lands in a serde compatible crate I would switch to it.
Turns out `serde_yml` does support aliases but it only supports a singular merge key. So this would not be supported:

```yaml
dict1: &dict1_anchor
  key1: value1

dict2: &dict2_anchor
  key2: value2

dict3:
  <<: *dict1_anchor
  <<: *dict2_anchor
  key3: value3
```

However this would be:

```yaml
dict1: &dict1_anchor
  key1: value1

dict2: &dict2_anchor
  key2: value2

dict3:
  <<: [*dict1_anchor, *dict2_anchor]
  key3: value3
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99d99d5d..cf5606c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ All notable changes to MiniJinja are documented here.
   `str.isalnum`, `str.isalpha`, `str.isascii`, `str.isdigit`,
   `str.isnumeric`, `str.join`, `str.startswith`.  #522
 - Added the missing tests `boolean`, `divisibleby`, `lower` and `upper`.  #592
+- minijinja-cli now supports YAML aliases and merge keys.  #531
 
 ## 2.0.2
 
diff --git a/Cargo.lock b/Cargo.lock
index 1c42761c..383d198e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1420,7 +1420,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http",
- "indexmap 2.2.5",
+ "indexmap 2.2.6",
  "slab",
  "tokio",
  "tokio-util",
@@ -1593,9 +1593,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.2.5"
+version = "2.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
 dependencies = [
  "equivalent",
  "hashbrown 0.14.3",
@@ -1693,9 +1693,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.10"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
 [[package]]
 name = "jobserver"
@@ -1769,6 +1769,12 @@ version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
+[[package]]
+name = "libyml"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e281a65eeba3d4503a2839252f86374528f9ceafe6fed97c1d3b52e1fb625c1"
+
 [[package]]
 name = "line-statements"
 version = "0.1.0"
@@ -1909,9 +1915,9 @@ dependencies = [
 
 [[package]]
 name = "memchr"
-version = "2.7.1"
+version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "memo-map"
@@ -2007,7 +2013,7 @@ dependencies = [
  "serde_json",
  "serde_json5",
  "serde_qs",
- "serde_yaml",
+ "serde_yml",
  "tempfile",
  "toml",
 ]
@@ -2378,7 +2384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9"
 dependencies = [
  "base64",
- "indexmap 2.2.5",
+ "indexmap 2.2.6",
  "line-wrap",
  "quick-xml",
  "serde",
@@ -2782,9 +2788,9 @@ dependencies = [
 
 [[package]]
 name = "ryu"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
 [[package]]
 name = "same-file"
@@ -2822,9 +2828,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
 
 [[package]]
 name = "serde"
-version = "1.0.197"
+version = "1.0.203"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
 dependencies = [
  "serde_derive",
 ]
@@ -2854,9 +2860,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.197"
+version = "1.0.203"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2865,9 +2871,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.114"
+version = "1.0.120"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
 dependencies = [
  "itoa",
  "ryu",
@@ -2918,16 +2924,20 @@ dependencies = [
 ]
 
 [[package]]
-name = "serde_yaml"
-version = "0.9.33"
+name = "serde_yml"
+version = "0.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9"
+checksum = "78ce6afeda22f0b55dde2c34897bce76a629587348480384231205c14b59a01f"
 dependencies = [
- "indexmap 2.2.5",
+ "indexmap 2.2.6",
  "itoa",
+ "libyml",
+ "log",
+ "memchr",
  "ryu",
  "serde",
- "unsafe-libyaml",
+ "serde_json",
+ "tempfile",
 ]
 
 [[package]]
@@ -3319,7 +3329,7 @@ version = "0.19.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
 dependencies = [
- "indexmap 2.2.5",
+ "indexmap 2.2.6",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -3475,12 +3485,6 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
 
-[[package]]
-name = "unsafe-libyaml"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
-
 [[package]]
 name = "url"
 version = "2.5.0"
diff --git a/minijinja-cabi/rustfmt.toml b/minijinja-cabi/rustfmt.toml
deleted file mode 100644
index d6c6b3a8..00000000
--- a/minijinja-cabi/rustfmt.toml
+++ /dev/null
@@ -1,1 +0,0 @@
-format_brace_macros = true
diff --git a/minijinja-cli/Cargo.toml b/minijinja-cli/Cargo.toml
index 3a0b0b82..a7423db5 100644
--- a/minijinja-cli/Cargo.toml
+++ b/minijinja-cli/Cargo.toml
@@ -13,7 +13,7 @@ rust-version = "1.65"
 
 [features]
 default = ["toml", "yaml", "querystring", "cbor", "datetime", "json5", "repl", "unicode"]
-yaml = ["serde_yaml"]
+yaml = ["serde_yml"]
 querystring = ["serde_qs"]
 cbor = ["ciborium"]
 datetime = ["minijinja-contrib/datetime", "minijinja-contrib/timezone"]
@@ -47,7 +47,7 @@ serde = "1.0.183"
 serde_json = "1.0.105"
 serde_json5 = { version = "0.1.0", optional = true }
 serde_qs = { version = "0.12.0", optional = true }
-serde_yaml = { version = "0.9.25", optional = true }
+serde_yml = { version = "0.0.10", optional = true }
 tempfile = "3.9.0"
 toml = { version = "0.7.6", optional = true }
 clap_complete = { version = "4", optional = true }
diff --git a/minijinja-cli/examples/alias.j2 b/minijinja-cli/examples/alias.j2
new file mode 100644
index 00000000..6068f207
--- /dev/null
+++ b/minijinja-cli/examples/alias.j2
@@ -0,0 +1,1 @@
+{{ dict3 }}
\ No newline at end of file
diff --git a/minijinja-cli/examples/alias.yaml b/minijinja-cli/examples/alias.yaml
new file mode 100644
index 00000000..e1f78249
--- /dev/null
+++ b/minijinja-cli/examples/alias.yaml
@@ -0,0 +1,9 @@
+dict1: &dict1_anchor
+  key1: value1
+
+dict2: &dict2_anchor
+  key2: value2
+
+dict3:
+  <<: [*dict1_anchor, *dict2_anchor]
+  key3: value3
\ No newline at end of file
diff --git a/minijinja-cli/examples/dump.j2 b/minijinja-cli/examples/dump.j2
new file mode 100644
index 00000000..ed586fa7
--- /dev/null
+++ b/minijinja-cli/examples/dump.j2
@@ -0,0 +1,1 @@
+{{ debug() }}
\ No newline at end of file
diff --git a/minijinja-cli/src/main.rs b/minijinja-cli/src/main.rs
index fbd3e16c..2ac57f81 100644
--- a/minijinja-cli/src/main.rs
+++ b/minijinja-cli/src/main.rs
@@ -116,7 +116,14 @@ fn load_data(
         #[cfg(feature = "querystring")]
         "querystring" => Value::from(serde_qs::from_str::<HashMap<String, Value>>(&contents)?),
         #[cfg(feature = "yaml")]
-        "yaml" => serde_yaml::from_str(&contents)?,
+        "yaml" => {
+            // for merge keys to work we need to manually call `apply_merge`.
+            // For this reason we need to deserialize into a serde_yml::Value
+            // before converting it into a final value.
+            let mut v: serde_yml::Value = serde_yml::from_str(&contents)?;
+            v.apply_merge()?;
+            Value::from_serialize(v)
+        }
         #[cfg(feature = "toml")]
         "toml" => toml::from_str(&contents)?,
         #[cfg(feature = "cbor")]
@@ -157,7 +164,7 @@ fn interpret_raw_value(s: &str) -> Result<Value, Error> {
     }
     #[cfg(feature = "yaml")]
     mod imp {
-        pub use serde_yaml::from_str;
+        pub use serde_yml::from_str;
         pub const FMT: &str = "JSON";
     }
     imp::from_str::<Value>(s)


Test patch:
diff --git a/minijinja-cli/tests/test_basic.rs b/minijinja-cli/tests/test_basic.rs
index e8eca2b1..990c241b 100644
--- a/minijinja-cli/tests/test_basic.rs
+++ b/minijinja-cli/tests/test_basic.rs
@@ -138,6 +138,43 @@ fn test_yaml() {
     "###);
 }
 
+#[test]
+#[cfg(feature = "yaml")]
+fn test_yaml_aliases() {
+    let input = file_with_contents_and_ext(
+        r#"
+a: &a
+  key1: value1
+
+b: &b
+  key2: value2
+
+c:
+  <<: *a
+  key2: from-c
+
+d:
+  <<: [*a, *b]
+  key3: value3
+"#,
+        ".yaml",
+    );
+    let tmpl = file_with_contents(r#"{{ [c.key1, c.key2] }}\n{{ [d.key1, d.key2, d.key3] }}"#);
+
+    assert_cmd_snapshot!(
+        cli()
+            .arg(tmpl.path())
+            .arg(input.path()),
+        @r###"
+    success: true
+    exit_code: 0
+    ----- stdout -----
+    ["value1", "from-c"]\n["value1", "value2", "value3"]
+
+    ----- stderr -----
+    "###);
+}
+
 #[test]
 #[cfg(feature = "toml")]
 fn test_toml() {