├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── release-plz.yml │ └── main.yml ├── Cargo.toml ├── readme.md ├── CHANGELOG.md ├── Cargo.lock └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "dependencies" 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serde-toml-merge" 3 | description = "Merge your toml values." 4 | version = "0.3.11" 5 | edition = "2018" 6 | authors = ["Jeremie Drouet "] 7 | repository = "https://github.com/jdrouet/serde-toml-merge" 8 | license = "MIT" 9 | readme = "readme.md" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | toml = "0.9" 15 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Install Rust toolchain 22 | uses: dtolnay/rust-toolchain@stable 23 | - name: Run release-plz 24 | uses: MarcoIeni/release-plz-action@v0.5 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Serde Toml Merge 2 | 3 | [![codecov](https://codecov.io/gh/jdrouet/serde-toml-merge/branch/main/graph/badge.svg?token=VR6M1YHRFA)](https://codecov.io/gh/jdrouet/serde-toml-merge) 4 | 5 | Just like [serde_merge](https://crates.io/crates/serde_merge), this crate allows you to merge [`toml`](https://crates.io/crates/toml) values. 6 | 7 | ## How to use 8 | 9 | ```rust 10 | use serde_toml_merge::merge; 11 | use toml::Value; 12 | 13 | fn main() { 14 | let first = r#" 15 | string = "foo" 16 | integer = 42 17 | float = 42.24 18 | boolean = true 19 | keep_me = true 20 | "# 21 | .parse::() 22 | .unwrap(); 23 | 24 | let second = r#" 25 | string = "bar" 26 | integer = 43 27 | float = 24.42 28 | boolean = false 29 | missing = true 30 | "# 31 | .parse::() 32 | .unwrap(); 33 | 34 | let expected = r#" 35 | string = "bar" 36 | integer = 43 37 | float = 24.42 38 | boolean = false 39 | keep_me = true 40 | missing = true 41 | "# 42 | .parse::() 43 | .unwrap(); 44 | 45 | assert_eq!(merge(first, second).unwrap(), expected); 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: testing and coverage 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | components: rustfmt,clippy 15 | 16 | - name: run lint 17 | run: cargo fmt --check 18 | 19 | - name: run code check with clippy 20 | uses: giraffate/clippy-action@v1 21 | if: ${{ github.event_name == 'pull_request' }} 22 | with: 23 | clippy_flags: -- -Dwarnings 24 | fail_on_error: true 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | reporter: 'github-pr-review' 27 | 28 | - name: run code check with clippy 29 | if: ${{ github.event_name != 'pull_request' }} 30 | run: cargo clippy --tests -- -D warnings 31 | 32 | - name: install cargo-llvm-cov 33 | uses: taiki-e/install-action@cargo-llvm-cov 34 | 35 | - name: run tests 36 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 37 | 38 | - name: upload code coverage results 39 | uses: codecov/codecov-action@v3 40 | with: 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | files: lcov.info 43 | fail_ci_if_error: false 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.3.11](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.10...v0.3.11) - 2025-07-30 11 | 12 | ### Other 13 | 14 | - *(deps)* Bump toml from 0.9.2 to 0.9.3 ([#46](https://github.com/jdrouet/serde-toml-merge/pull/46)) 15 | # Changelog 16 | All notable changes to this project will be documented in this file. 17 | 18 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 19 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 20 | 21 | ## [Unreleased] 22 | 23 | ## [0.3.10](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.9...v0.3.10) - 2025-07-14 24 | 25 | ### Other 26 | 27 | - *(deps)* Bump toml from 0.8.23 to 0.9.2 ([#45](https://github.com/jdrouet/serde-toml-merge/pull/45)) 28 | - impl std::error::Error trait for custom Error type ([#42](https://github.com/jdrouet/serde-toml-merge/pull/42)) 29 | - *(deps)* Bump toml from 0.8.22 to 0.8.23 ([#40](https://github.com/jdrouet/serde-toml-merge/pull/40)) 30 | 31 | ## [0.3.9](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.8...v0.3.9) - 2025-04-29 32 | 33 | ### Other 34 | 35 | - *(deps)* Bump toml from 0.8.19 to 0.8.22 ([#37](https://github.com/jdrouet/serde-toml-merge/pull/37)) 36 | - *(deps)* Bump toml from 0.8.14 to 0.8.19 ([#35](https://github.com/jdrouet/serde-toml-merge/pull/35)) 37 | 38 | ## [0.3.8](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.7...v0.3.8) - 2024-06-04 39 | 40 | ### Other 41 | - *(deps)* Bump toml from 0.8.13 to 0.8.14 ([#30](https://github.com/jdrouet/serde-toml-merge/pull/30)) 42 | 43 | ## [0.3.7](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.6...v0.3.7) - 2024-05-15 44 | 45 | ### Other 46 | - *(deps)* Bump toml from 0.8.12 to 0.8.13 ([#28](https://github.com/jdrouet/serde-toml-merge/pull/28)) 47 | 48 | ## [0.3.6](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.5...v0.3.6) - 2024-03-18 49 | 50 | ### Other 51 | - *(deps)* Bump toml from 0.8.11 to 0.8.12 ([#26](https://github.com/jdrouet/serde-toml-merge/pull/26)) 52 | 53 | ## [0.3.5](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.4...v0.3.5) - 2024-03-12 54 | 55 | ### Other 56 | - *(deps)* Bump toml from 0.8.10 to 0.8.11 ([#25](https://github.com/jdrouet/serde-toml-merge/pull/25)) 57 | - *(deps)* Bump toml from 0.8.9 to 0.8.10 ([#23](https://github.com/jdrouet/serde-toml-merge/pull/23)) 58 | 59 | ## [0.3.4](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.3...v0.3.4) - 2024-02-01 60 | 61 | ### Other 62 | - update readme example ([#18](https://github.com/jdrouet/serde-toml-merge/pull/18)) 63 | - *(deps)* Bump toml from 0.8.8 to 0.8.9 64 | - *(deps)* Bump toml from 0.8.6 to 0.8.8 65 | - *(deps)* Bump toml from 0.8.5 to 0.8.6 66 | - *(deps)* Bump toml from 0.8.4 to 0.8.5 67 | - *(deps)* Bump toml from 0.8.2 to 0.8.4 68 | - *(ci)* remove useless release file 69 | 70 | ## [0.3.3](https://github.com/jdrouet/serde-toml-merge/compare/v0.3.2...v0.3.3) - 2023-10-07 71 | 72 | ### Other 73 | - *(ci)* configure release-plz 74 | - *(deps)* Bump toml from 0.8.1 to 0.8.2 75 | - *(deps)* Bump toml from 0.8.0 to 0.8.1 76 | - fix codecov action 77 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "equivalent" 7 | version = "1.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" 10 | 11 | [[package]] 12 | name = "hashbrown" 13 | version = "0.15.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 16 | 17 | [[package]] 18 | name = "indexmap" 19 | version = "2.11.4" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 22 | dependencies = [ 23 | "equivalent", 24 | "hashbrown", 25 | ] 26 | 27 | [[package]] 28 | name = "proc-macro2" 29 | version = "1.0.101" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 32 | dependencies = [ 33 | "unicode-ident", 34 | ] 35 | 36 | [[package]] 37 | name = "quote" 38 | version = "1.0.40" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 41 | dependencies = [ 42 | "proc-macro2", 43 | ] 44 | 45 | [[package]] 46 | name = "serde-toml-merge" 47 | version = "0.3.11" 48 | dependencies = [ 49 | "toml", 50 | ] 51 | 52 | [[package]] 53 | name = "serde_core" 54 | version = "1.0.225" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" 57 | dependencies = [ 58 | "serde_derive", 59 | ] 60 | 61 | [[package]] 62 | name = "serde_derive" 63 | version = "1.0.225" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" 66 | dependencies = [ 67 | "proc-macro2", 68 | "quote", 69 | "syn", 70 | ] 71 | 72 | [[package]] 73 | name = "serde_spanned" 74 | version = "1.0.3" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" 77 | dependencies = [ 78 | "serde_core", 79 | ] 80 | 81 | [[package]] 82 | name = "syn" 83 | version = "2.0.106" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 86 | dependencies = [ 87 | "proc-macro2", 88 | "quote", 89 | "unicode-ident", 90 | ] 91 | 92 | [[package]] 93 | name = "toml" 94 | version = "0.9.8" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" 97 | dependencies = [ 98 | "indexmap", 99 | "serde_core", 100 | "serde_spanned", 101 | "toml_datetime", 102 | "toml_parser", 103 | "toml_writer", 104 | "winnow", 105 | ] 106 | 107 | [[package]] 108 | name = "toml_datetime" 109 | version = "0.7.3" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 112 | dependencies = [ 113 | "serde_core", 114 | ] 115 | 116 | [[package]] 117 | name = "toml_parser" 118 | version = "1.0.4" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 121 | dependencies = [ 122 | "winnow", 123 | ] 124 | 125 | [[package]] 126 | name = "toml_writer" 127 | version = "1.0.4" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" 130 | 131 | [[package]] 132 | name = "unicode-ident" 133 | version = "1.0.19" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 136 | 137 | [[package]] 138 | name = "winnow" 139 | version = "0.7.13" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 142 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use toml::map::Map; 3 | use toml::Value; 4 | 5 | #[derive(Debug, PartialEq)] 6 | pub struct Error { 7 | pub path: String, 8 | pub expected: &'static str, 9 | pub existing: &'static str, 10 | } 11 | 12 | impl Error { 13 | pub fn new(path: String, expected: &'static str, existing: &'static str) -> Self { 14 | Self { 15 | path, 16 | expected, 17 | existing, 18 | } 19 | } 20 | } 21 | 22 | impl std::error::Error for Error {} 23 | 24 | impl fmt::Display for Error { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 26 | write!( 27 | f, 28 | "Incompatible types at path \"{}\", expected \"{}\" received \"{}\".", 29 | self.path, self.expected, self.existing 30 | ) 31 | } 32 | } 33 | 34 | fn merge_into_table_inner( 35 | value: &mut Map, 36 | other: Map, 37 | path: &str, 38 | ) -> Result<(), Error> { 39 | for (name, inner) in other { 40 | if let Some(existing) = value.remove(&name) { 41 | let inner_path = format!("{path}.{name}"); 42 | value.insert(name, merge_inner(existing, inner, &inner_path)?); 43 | } else { 44 | value.insert(name, inner); 45 | } 46 | } 47 | Ok(()) 48 | } 49 | 50 | /// Merges two toml tables into a single one. 51 | pub fn merge_tables( 52 | mut value: Map, 53 | other: Map, 54 | ) -> Result, Error> { 55 | merge_into_table_inner(&mut value, other, "$")?; 56 | Ok(value) 57 | } 58 | 59 | /// Merges two toml tables into a single one. 60 | pub fn merge_into_table( 61 | value: &mut Map, 62 | other: Map, 63 | ) -> Result<(), Error> { 64 | merge_into_table_inner(value, other, "$") 65 | } 66 | 67 | fn merge_inner(value: Value, other: Value, path: &str) -> Result { 68 | match (value, other) { 69 | (Value::String(_), Value::String(inner)) => Ok(Value::String(inner)), 70 | (Value::Integer(_), Value::Integer(inner)) => Ok(Value::Integer(inner)), 71 | (Value::Float(_), Value::Float(inner)) => Ok(Value::Float(inner)), 72 | (Value::Boolean(_), Value::Boolean(inner)) => Ok(Value::Boolean(inner)), 73 | (Value::Datetime(_), Value::Datetime(inner)) => Ok(Value::Datetime(inner)), 74 | (Value::Array(mut existing), Value::Array(inner)) => { 75 | existing.extend(inner); 76 | Ok(Value::Array(existing)) 77 | } 78 | (Value::Table(mut existing), Value::Table(inner)) => { 79 | merge_into_table_inner(&mut existing, inner, path)?; 80 | Ok(Value::Table(existing)) 81 | } 82 | (v, o) => Err(Error::new(path.to_owned(), v.type_str(), o.type_str())), 83 | } 84 | } 85 | 86 | /// Merges two toml values into a single one. 87 | pub fn merge(value: Value, other: Value) -> Result { 88 | merge_inner(value, other, "$") 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use crate::{merge, Error}; 94 | 95 | macro_rules! should_fail { 96 | ($first: expr, $second: expr) => { 97 | should_fail!($first, $second,) 98 | }; 99 | ($first: expr, $second: expr,) => {{ 100 | let first = $first.parse::().unwrap(); 101 | let second = $second.parse::().unwrap(); 102 | merge(first.into(), second.into()).unwrap_err() 103 | }}; 104 | } 105 | 106 | macro_rules! should_match { 107 | ($first: expr, $second: expr, $result: expr) => { 108 | should_match!($first, $second, $result,) 109 | }; 110 | ($first: expr, $second: expr, $result: expr,) => { 111 | let first = $first.parse::().unwrap(); 112 | let second = $second.parse::().unwrap(); 113 | let result = $result.parse::().unwrap(); 114 | assert_eq!(merge(first.into(), second.into()).unwrap(), result.into()); 115 | }; 116 | } 117 | 118 | #[test] 119 | fn with_basic() { 120 | should_match!( 121 | r#"string = 'foo' 122 | integer = 42 123 | float = 42.24 124 | boolean = true 125 | keep_me = true 126 | "#, 127 | r#"string = 'bar' 128 | integer = 43 129 | float = 24.42 130 | boolean = false 131 | missing = true 132 | "#, 133 | r#"string = 'bar' 134 | integer = 43 135 | float = 24.42 136 | boolean = false 137 | keep_me = true 138 | missing = true 139 | "#, 140 | ); 141 | } 142 | 143 | #[test] 144 | fn with_array() { 145 | should_match!( 146 | r#"foo = ["a", "b"]"#, 147 | r#"foo = ["c", "d"]"#, 148 | r#"foo = ["a", "b", "c", "d"]"#, 149 | ); 150 | } 151 | 152 | #[test] 153 | fn with_table() { 154 | should_match!( 155 | r#" 156 | [foo] 157 | bar = "baz" 158 | "#, 159 | r#" 160 | [foo] 161 | hello = "world" 162 | "#, 163 | r#" 164 | [foo] 165 | bar = "baz" 166 | hello = "world" 167 | "#, 168 | ); 169 | } 170 | 171 | #[test] 172 | fn invalid_kinds() { 173 | assert_eq!( 174 | should_fail!("foo = true", "foo = 42"), 175 | Error::new("$.foo".to_owned(), "boolean", "integer") 176 | ); 177 | assert_eq!( 178 | should_fail!("foo = \"true\"", "foo = 42.5"), 179 | Error::new("$.foo".to_owned(), "string", "float") 180 | ); 181 | } 182 | } 183 | --------------------------------------------------------------------------------