├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── README.tpl ├── benches ├── bench.rs └── generator │ └── mod.rs ├── codecov.yml ├── release.toml ├── specs ├── merge_tests.json ├── revert_tests.json ├── spec_tests.json └── tests.json ├── src ├── diff.rs └── lib.rs ├── tests ├── basic.rs ├── errors.yaml ├── schemars.json ├── schemars.rs ├── suite.rs ├── utoipa.json └── utoipa.rs └── update-readme.sh /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | - actions-* 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | name: Build 10 | 11 | jobs: 12 | check: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v3 18 | 19 | - name: Install stable toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | 26 | - name: Run cargo build 27 | run: cargo build --verbose 28 | 29 | bench: 30 | name: Bench 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout sources 34 | uses: actions/checkout@v3 35 | 36 | - name: Install nightly toolchain 37 | uses: actions-rs/toolchain@v1 38 | with: 39 | profile: minimal 40 | toolchain: nightly 41 | override: true 42 | 43 | - name: Run cargo bench 44 | run: cargo +nightly bench --verbose 45 | 46 | test: 47 | name: Test 48 | runs-on: ubuntu-latest 49 | container: 50 | image: xd009642/tarpaulin:0.22.0 51 | options: --security-opt seccomp=unconfined 52 | steps: 53 | - name: Checkout sources 54 | uses: actions/checkout@v3 55 | 56 | - name: Install stable toolchain 57 | uses: actions-rs/toolchain@v1 58 | with: 59 | profile: minimal 60 | toolchain: stable 61 | override: true 62 | 63 | - name: Run cargo test 64 | run: cargo tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml 65 | 66 | - name: Codecov 67 | uses: codecov/codecov-action@v3.1.4 68 | with: 69 | token: ${{ secrets.CODECOV_TOKEN }} 70 | verbose: true 71 | fail_ci_if_error: true 72 | 73 | lints: 74 | name: Lints 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: Checkout sources 78 | uses: actions/checkout@v2 79 | 80 | - name: Install stable toolchain 81 | uses: actions-rs/toolchain@v1 82 | with: 83 | profile: minimal 84 | toolchain: stable 85 | override: true 86 | components: rustfmt, clippy 87 | 88 | - name: Run cargo fmt 89 | run: cargo fmt --all -- --check 90 | 91 | - name: Run cargo clippy 92 | run: cargo clippy -- -D warnings 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 (2022-12-10) 4 | 5 | ### Breaking Changes 6 | 7 | - Removed `json_patch::patch_unsafe` operation as regular `patch` is it does not provide enough value. 8 | - Error types changed to include some context. 9 | - Removed `json_patch::from_value`. Use `serde_json::from_value` instead. 10 | 11 | ## 0.2.7 (2022-12-09) 12 | 13 | ### Fixed 14 | 15 | - Fixed incorrect diffing for the whole document. Previously, differ would incorrectly yield path of `"/"` when the 16 | whole document is replaced. The correct path should be `""`. This is a breaking change. 17 | [#18](https://github.com/idubrov/json-patch/pull/18) 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "json-patch" 3 | version = "4.0.0" 4 | authors = ["Ivan Dubrov "] 5 | categories = [] 6 | keywords = ["json", "json-patch"] 7 | description = "RFC 6902, JavaScript Object Notation (JSON) Patch" 8 | repository = "https://github.com/idubrov/json-patch" 9 | license = "MIT/Apache-2.0" 10 | readme = "README.md" 11 | edition = "2021" 12 | 13 | [features] 14 | default = ["diff"] 15 | diff = [] 16 | 17 | [dependencies] 18 | jsonptr = "0.7.1" 19 | schemars = { version = "0.8", optional = true } 20 | serde = { version = "1.0.159", features = ["derive"] } 21 | serde_json = "1.0.95" 22 | thiserror = "1.0.40" 23 | utoipa = { version = "4.0", optional = true } 24 | 25 | [dev-dependencies] 26 | expectorate = "1.0" 27 | rand = "0.8.5" 28 | schemars = "0.8.22" 29 | serde_json = { version = "1.0.95", features = ["preserve_order"] } 30 | serde_yaml = "0.9.19" 31 | utoipa = { version = "4.0", features = ["debug"] } 32 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ivan Dubrov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![crates.io](https://img.shields.io/crates/v/json-patch.svg)](https://crates.io/crates/json-patch) 2 | [![crates.io](https://img.shields.io/crates/d/json-patch.svg)](https://crates.io/crates/json-patch) 3 | [![Build](https://github.com/idubrov/json-patch/actions/workflows/main.yml/badge.svg)](https://github.com/idubrov/json-patch/actions) 4 | [![Codecov](https://codecov.io/gh/idubrov/json-patch/branch/main/graph/badge.svg?token=hdcr6yfBfa)](https://codecov.io/gh/idubrov/json-patch) 5 | 6 | # json-patch 7 | 8 | A [JSON Patch (RFC 6902)](https://tools.ietf.org/html/rfc6902) and 9 | [JSON Merge Patch (RFC 7396)](https://tools.ietf.org/html/rfc7396) implementation for Rust. 10 | 11 | ## Usage 12 | 13 | Add this to your *Cargo.toml*: 14 | ```toml 15 | [dependencies] 16 | json-patch = "*" 17 | ``` 18 | 19 | ## Examples 20 | Create and patch document using JSON Patch: 21 | 22 | ```rust 23 | #[macro_use] 24 | use json_patch::{Patch, patch}; 25 | use serde_json::{from_value, json}; 26 | 27 | let mut doc = json!([ 28 | { "name": "Andrew" }, 29 | { "name": "Maxim" } 30 | ]); 31 | 32 | let p: Patch = from_value(json!([ 33 | { "op": "test", "path": "/0/name", "value": "Andrew" }, 34 | { "op": "add", "path": "/0/happy", "value": true } 35 | ])).unwrap(); 36 | 37 | patch(&mut doc, &p).unwrap(); 38 | assert_eq!(doc, json!([ 39 | { "name": "Andrew", "happy": true }, 40 | { "name": "Maxim" } 41 | ])); 42 | 43 | ``` 44 | 45 | Create and patch document using JSON Merge Patch: 46 | 47 | ```rust 48 | #[macro_use] 49 | use json_patch::merge; 50 | use serde_json::json; 51 | 52 | let mut doc = json!({ 53 | "title": "Goodbye!", 54 | "author" : { 55 | "givenName" : "John", 56 | "familyName" : "Doe" 57 | }, 58 | "tags":[ "example", "sample" ], 59 | "content": "This will be unchanged" 60 | }); 61 | 62 | let patch = json!({ 63 | "title": "Hello!", 64 | "phoneNumber": "+01-123-456-7890", 65 | "author": { 66 | "familyName": null 67 | }, 68 | "tags": [ "example" ] 69 | }); 70 | 71 | merge(&mut doc, &patch); 72 | assert_eq!(doc, json!({ 73 | "title": "Hello!", 74 | "author" : { 75 | "givenName" : "John" 76 | }, 77 | "tags": [ "example" ], 78 | "content": "This will be unchanged", 79 | "phoneNumber": "+01-123-456-7890" 80 | })); 81 | ``` 82 | 83 | ## License 84 | 85 | Licensed under either of 86 | 87 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 88 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 89 | 90 | at your option. 91 | 92 | ### Contribution 93 | 94 | Unless you explicitly state otherwise, any contribution intentionally submitted 95 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 96 | additional terms or conditions. 97 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | [![crates.io](https://img.shields.io/crates/v/json-patch.svg)](https://crates.io/crates/json-patch) 2 | [![crates.io](https://img.shields.io/crates/d/json-patch.svg)](https://crates.io/crates/json-patch) 3 | [![Build](https://github.com/idubrov/json-patch/actions/workflows/main.yml/badge.svg)](https://github.com/idubrov/json-patch/actions) 4 | [![Codecov](https://codecov.io/gh/idubrov/json-patch/branch/main/graph/badge.svg?token=hdcr6yfBfa)](https://codecov.io/gh/idubrov/json-patch) 5 | 6 | # {{crate}} 7 | 8 | {{readme}} 9 | 10 | ## License 11 | 12 | Licensed under either of 13 | 14 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 15 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 16 | 17 | at your option. 18 | 19 | ### Contribution 20 | 21 | Unless you explicitly state otherwise, any contribution intentionally submitted 22 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 23 | additional terms or conditions. 24 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | use json_patch::*; 5 | use rand::SeedableRng; 6 | use test::Bencher; 7 | 8 | mod generator; 9 | 10 | #[bench] 11 | fn bench_add_removes(b: &mut Bencher) { 12 | let mut rng = rand::rngs::StdRng::from_seed(Default::default()); 13 | let params = generator::Params { 14 | ..Default::default() 15 | }; 16 | let doc = params.gen(&mut rng); 17 | let patches = generator::gen_add_remove_patches(&doc, &mut rng, 10, 10); 18 | 19 | b.iter(|| { 20 | let mut doc = doc.clone(); 21 | let mut result = Ok(()); 22 | for p in &patches { 23 | // Patch mutable 24 | result = result.and_then(|_| patch(&mut doc, p)); 25 | } 26 | }); 27 | } 28 | 29 | #[cfg(feature = "nightly")] 30 | #[bench] 31 | fn bench_add_removes_unsafe(b: &mut Bencher) { 32 | let mut rng = rand::StdRng::from_seed(Default::default()); 33 | let params = generator::Params { 34 | ..Default::default() 35 | }; 36 | let doc = params.gen(&mut rng); 37 | let patches = generator::gen_add_remove_patches(&doc, &mut rng, 10, 10); 38 | 39 | b.iter(|| { 40 | let mut doc = doc.clone(); 41 | let mut result = Ok(()); 42 | for ref p in &patches { 43 | // Patch mutable 44 | result = result.and_then(|_| patch_unsafe(&mut doc, p)); 45 | } 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /benches/generator/mod.rs: -------------------------------------------------------------------------------- 1 | use json_patch::{AddOperation, Patch, PatchOperation, RemoveOperation}; 2 | use jsonptr::PointerBuf; 3 | use rand::distributions::Alphanumeric; 4 | use rand::prelude::*; 5 | use serde_json::{Map, Value}; 6 | 7 | pub struct Params { 8 | pub array_size: usize, 9 | pub map_size: usize, 10 | pub value_size: usize, 11 | pub depth: usize, 12 | pub key_size: usize, 13 | } 14 | 15 | impl Default for Params { 16 | fn default() -> Self { 17 | Params { 18 | array_size: 6, 19 | map_size: 6, 20 | value_size: 100, 21 | depth: 8, 22 | key_size: 20, 23 | } 24 | } 25 | } 26 | 27 | fn rand_str(rng: &mut R, max_len: usize) -> String { 28 | let len = rng.gen::() % max_len + 1; 29 | rng.sample_iter(&Alphanumeric) 30 | .take(len) 31 | .map(char::from) 32 | .collect() 33 | } 34 | 35 | fn rand_literal(rng: &mut R, value_size: usize) -> Value { 36 | match rng.gen::() % 4 { 37 | 0 => Value::Null, 38 | 1 => Value::String(rand_str(rng, value_size)), 39 | 2 => Value::Bool(false), 40 | 3 => Value::from(rng.gen::()), 41 | _ => panic!(), 42 | } 43 | } 44 | 45 | impl Params { 46 | pub fn gen(&self, rng: &mut R) -> Value { 47 | self.gen_internal(self.depth, rng) 48 | } 49 | 50 | fn gen_internal(&self, depth: usize, rng: &mut R) -> Value { 51 | if depth == 0 { 52 | rand_literal(rng, self.value_size) 53 | } else if rng.gen::() { 54 | // Generate random array 55 | let len = (rng.gen::() % self.array_size) + 1; 56 | let vec: Vec = (0..len) 57 | .map(|_| self.gen_internal(depth - 1, rng)) 58 | .collect(); 59 | Value::from(vec) 60 | } else { 61 | // Generate random object 62 | let len = (rng.gen::() % self.map_size) + 1; 63 | let map: Map = (0..len) 64 | .map(|_| { 65 | ( 66 | rand_str(rng, self.key_size), 67 | self.gen_internal(depth - 1, rng), 68 | ) 69 | }) 70 | .collect(); 71 | Value::from(map) 72 | } 73 | } 74 | } 75 | 76 | pub fn gen_add_remove_patches( 77 | value: &Value, 78 | rnd: &mut R, 79 | patches: usize, 80 | operations: usize, 81 | ) -> Vec { 82 | let leaves = all_leaves(value); 83 | let mut vec = Vec::new(); 84 | for _ in 0..patches { 85 | let mut ops = Vec::new(); 86 | for _ in 0..operations { 87 | let path = leaves.choose(rnd).unwrap(); 88 | ops.push(PatchOperation::Remove(RemoveOperation { 89 | path: (*path).clone(), 90 | })); 91 | ops.push(PatchOperation::Add(AddOperation { 92 | path: (*path).clone(), 93 | value: Value::Null, 94 | })); 95 | } 96 | vec.push(Patch(ops)); 97 | } 98 | vec 99 | } 100 | 101 | fn all_leaves(value: &Value) -> Vec { 102 | let mut result = Vec::new(); 103 | collect_leaves(value, &mut PointerBuf::new(), &mut result); 104 | result 105 | } 106 | 107 | fn collect_leaves(value: &Value, prefix: &mut PointerBuf, result: &mut Vec) { 108 | match *value { 109 | Value::Array(ref arr) => { 110 | for (idx, val) in arr.iter().enumerate() { 111 | prefix.push_back(idx); 112 | collect_leaves(val, prefix, result); 113 | prefix.pop_back(); 114 | } 115 | } 116 | Value::Object(ref map) => { 117 | for (key, val) in map.iter() { 118 | prefix.push_back(key); 119 | collect_leaves(val, prefix, result); 120 | prefix.pop_back(); 121 | } 122 | } 123 | _ => { 124 | result.push(prefix.clone()); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | ignore: 6 | - "benches" 7 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-hook = "./update-readme.sh" 2 | -------------------------------------------------------------------------------- /specs/merge_tests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "1. introduction", 4 | "doc": { 5 | "a": "b", 6 | "c": { 7 | "d": "e", 8 | "f": "g" 9 | } 10 | }, 11 | "patch": { 12 | "a": "z", 13 | "c": { 14 | "f": null 15 | } 16 | }, 17 | "expected": { 18 | "a": "z", 19 | "c": { 20 | "d": "e" 21 | } 22 | }, 23 | "merge": true 24 | }, 25 | { 26 | "comment": "3. example", 27 | "doc": { 28 | "title": "Goodbye!", 29 | "author": { 30 | "givenName": "John", 31 | "familyName": "Doe" 32 | }, 33 | "tags": [ 34 | "example", 35 | "sample" 36 | ], 37 | "content": "This will be unchanged" 38 | }, 39 | "patch": { 40 | "title": "Hello!", 41 | "phoneNumber": "+01-123-456-7890", 42 | "author": { 43 | "familyName": null 44 | }, 45 | "tags": [ 46 | "example" 47 | ] 48 | }, 49 | "expected": { 50 | "title": "Hello!", 51 | "author": { 52 | "givenName": "John" 53 | }, 54 | "tags": [ 55 | "example" 56 | ], 57 | "content": "This will be unchanged", 58 | "phoneNumber": "+01-123-456-7890" 59 | }, 60 | "merge": true 61 | }, 62 | { 63 | "comment": "replacing non-object", 64 | "doc": { 65 | "title": "Goodbye!", 66 | "author": { 67 | "givenName": "John" 68 | }, 69 | "tags": [ 70 | "example", 71 | "sample" 72 | ], 73 | "content": "This will be unchanged" 74 | }, 75 | "patch": { 76 | "tags": { 77 | "kind": "example" 78 | } 79 | }, 80 | "expected": { 81 | "title": "Goodbye!", 82 | "author": { 83 | "givenName": "John" 84 | }, 85 | "tags": { 86 | "kind": "example" 87 | }, 88 | "content": "This will be unchanged" 89 | }, 90 | "merge": true 91 | } 92 | ] -------------------------------------------------------------------------------- /specs/revert_tests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "Can revert add (replace key)", 4 | "doc": { 5 | "foo": { 6 | "bar": { 7 | "baz": true 8 | } 9 | } 10 | }, 11 | "patch": [ 12 | { 13 | "op": "add", 14 | "path": "/foo", 15 | "value": false 16 | }, 17 | { 18 | "op": "remove", 19 | "path": "/foo/bar" 20 | } 21 | ], 22 | "error": "invalid pointer" 23 | }, 24 | { 25 | "comment": "Can revert add (insert into array)", 26 | "doc": { 27 | "foo": [1, 2, 3] 28 | }, 29 | "patch": [ 30 | { 31 | "op": "add", 32 | "path": "/foo/1", 33 | "value": false 34 | }, 35 | { 36 | "op": "remove", 37 | "path": "/foo/bar" 38 | } 39 | ], 40 | "error": "invalid pointer" 41 | }, 42 | { 43 | "comment": "Can revert add (insert last element into array)", 44 | "doc": { 45 | "foo": [1, 2, 3] 46 | }, 47 | "patch": [ 48 | { 49 | "op": "add", 50 | "path": "/foo/-", 51 | "value": false 52 | }, 53 | { 54 | "op": "remove", 55 | "path": "/foo/bar" 56 | } 57 | ], 58 | "error": "invalid pointer" 59 | }, 60 | { 61 | "comment": "Can revert remove (object)", 62 | "doc": { 63 | "foo": { 64 | "bar": { 65 | "baz": true 66 | } 67 | } 68 | }, 69 | "patch": [ 70 | { 71 | "op": "remove", 72 | "path": "/foo" 73 | }, 74 | { 75 | "op": "remove", 76 | "path": "/foo/bar" 77 | } 78 | ], 79 | "error": "invalid pointer" 80 | }, 81 | { 82 | "comment": "Can revert remove (array)", 83 | "doc": { 84 | "foo": [1, 2, 3] 85 | }, 86 | "patch": [ 87 | { 88 | "op": "remove", 89 | "path": "/foo/1" 90 | }, 91 | { 92 | "op": "remove", 93 | "path": "/foo/bar" 94 | } 95 | ], 96 | "error": "invalid pointer" 97 | }, 98 | { 99 | "comment": "Can revert replace (replace key)", 100 | "doc": { 101 | "foo": { 102 | "bar": { 103 | "baz": true 104 | } 105 | } 106 | }, 107 | "patch": [ 108 | { 109 | "op": "replace", 110 | "path": "/foo", 111 | "value": false 112 | }, 113 | { 114 | "op": "remove", 115 | "path": "/foo/bar" 116 | } 117 | ], 118 | "error": "invalid pointer" 119 | }, 120 | { 121 | "comment": "Can revert replace (replace array element)", 122 | "doc": { 123 | "foo": [1, 2, 3] 124 | }, 125 | "patch": [ 126 | { 127 | "op": "replace", 128 | "path": "/foo/1", 129 | "value": false 130 | }, 131 | { 132 | "op": "remove", 133 | "path": "/foo/bar" 134 | } 135 | ], 136 | "error": "invalid pointer" 137 | }, 138 | { 139 | "comment": "Can revert move (move into key)", 140 | "doc": { 141 | "foo": { 142 | "bar": { 143 | "baz": true 144 | } 145 | }, 146 | "abc": { 147 | "def": { 148 | "ghi": false 149 | } 150 | } 151 | }, 152 | "patch": [ 153 | { 154 | "op": "move", 155 | "from": "/abc", 156 | "path": "/foo", 157 | "value": false 158 | }, 159 | { 160 | "op": "remove", 161 | "path": "/foo/bar" 162 | } 163 | ], 164 | "error": "invalid pointer" 165 | }, 166 | { 167 | "comment": "Can revert move (move into array)", 168 | "doc": { 169 | "foo": [1, 2, 3], 170 | "abc": { 171 | "def": { 172 | "ghi": false 173 | } 174 | } 175 | }, 176 | "patch": [ 177 | { 178 | "op": "move", 179 | "path": "/foo/1", 180 | "from": "/abc" 181 | }, 182 | { 183 | "op": "remove", 184 | "path": "/foo/bar" 185 | } 186 | ], 187 | "error": "invalid pointer" 188 | }, 189 | { 190 | "comment": "Can revert move (move into last element of an array)", 191 | "doc": { 192 | "foo": [1, 2, 3], 193 | "abc": { 194 | "def": { 195 | "ghi": false 196 | } 197 | } 198 | }, 199 | "patch": [ 200 | { 201 | "op": "move", 202 | "path": "/foo/-", 203 | "from": "/abc" 204 | }, 205 | { 206 | "op": "remove", 207 | "path": "/foo/bar" 208 | } 209 | ], 210 | "error": "invalid pointer" 211 | }, 212 | { 213 | "comment": "Can revert copy (copy into key)", 214 | "doc": { 215 | "foo": { 216 | "bar": { 217 | "baz": true 218 | } 219 | }, 220 | "abc": { 221 | "def": { 222 | "ghi": false 223 | } 224 | } 225 | }, 226 | "patch": [ 227 | { 228 | "op": "copy", 229 | "from": "/abc", 230 | "path": "/foo", 231 | "value": false 232 | }, 233 | { 234 | "op": "remove", 235 | "path": "/foo/bar" 236 | } 237 | ], 238 | "error": "invalid pointer" 239 | }, 240 | { 241 | "comment": "Can revert copy (copy into array)", 242 | "doc": { 243 | "foo": [1, 2, 3], 244 | "abc": { 245 | "def": { 246 | "ghi": false 247 | } 248 | } 249 | }, 250 | "patch": [ 251 | { 252 | "op": "copy", 253 | "path": "/foo/1", 254 | "from": "/abc" 255 | }, 256 | { 257 | "op": "remove", 258 | "path": "/foo/bar" 259 | } 260 | ], 261 | "error": "invalid pointer" 262 | }, 263 | { 264 | "comment": "Can revert copy (copy into last element of an array)", 265 | "doc": { 266 | "foo": [1, 2, 3], 267 | "abc": { 268 | "def": { 269 | "ghi": false 270 | } 271 | } 272 | }, 273 | "patch": [ 274 | { 275 | "op": "copy", 276 | "path": "/foo/-", 277 | "from": "/abc" 278 | }, 279 | { 280 | "op": "remove", 281 | "path": "/foo/bar" 282 | } 283 | ], 284 | "error": "invalid pointer" 285 | } 286 | ] 287 | -------------------------------------------------------------------------------- /specs/spec_tests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "4.1. add with missing object", 4 | "doc": { 5 | "q": { 6 | "bar": 2 7 | } 8 | }, 9 | "patch": [ 10 | { 11 | "op": "add", 12 | "path": "/a/b", 13 | "value": 1 14 | } 15 | ], 16 | "error": "path /a does not exist -- missing objects are not created recursively" 17 | }, 18 | { 19 | "comment": "A.1. Adding an Object Member", 20 | "doc": { 21 | "foo": "bar" 22 | }, 23 | "patch": [ 24 | { 25 | "op": "add", 26 | "path": "/baz", 27 | "value": "qux" 28 | } 29 | ], 30 | "expected": { 31 | "baz": "qux", 32 | "foo": "bar" 33 | } 34 | }, 35 | { 36 | "comment": "A.2. Adding an Array Element", 37 | "doc": { 38 | "foo": [ 39 | "bar", 40 | "baz" 41 | ] 42 | }, 43 | "patch": [ 44 | { 45 | "op": "add", 46 | "path": "/foo/1", 47 | "value": "qux" 48 | } 49 | ], 50 | "expected": { 51 | "foo": [ 52 | "bar", 53 | "qux", 54 | "baz" 55 | ] 56 | } 57 | }, 58 | { 59 | "comment": "A.3. Removing an Object Member", 60 | "doc": { 61 | "baz": "qux", 62 | "foo": "bar" 63 | }, 64 | "patch": [ 65 | { 66 | "op": "remove", 67 | "path": "/baz" 68 | } 69 | ], 70 | "expected": { 71 | "foo": "bar" 72 | } 73 | }, 74 | { 75 | "comment": "A.4. Removing an Array Element", 76 | "doc": { 77 | "foo": [ 78 | "bar", 79 | "qux", 80 | "baz" 81 | ] 82 | }, 83 | "patch": [ 84 | { 85 | "op": "remove", 86 | "path": "/foo/1" 87 | } 88 | ], 89 | "expected": { 90 | "foo": [ 91 | "bar", 92 | "baz" 93 | ] 94 | } 95 | }, 96 | { 97 | "comment": "A.5. Replacing a Value", 98 | "doc": { 99 | "baz": "qux", 100 | "foo": "bar" 101 | }, 102 | "patch": [ 103 | { 104 | "op": "replace", 105 | "path": "/baz", 106 | "value": "boo" 107 | } 108 | ], 109 | "expected": { 110 | "baz": "boo", 111 | "foo": "bar" 112 | } 113 | }, 114 | { 115 | "comment": "A.6. Moving a Value", 116 | "doc": { 117 | "foo": { 118 | "bar": "baz", 119 | "waldo": "fred" 120 | }, 121 | "qux": { 122 | "corge": "grault" 123 | } 124 | }, 125 | "patch": [ 126 | { 127 | "op": "move", 128 | "from": "/foo/waldo", 129 | "path": "/qux/thud" 130 | } 131 | ], 132 | "expected": { 133 | "foo": { 134 | "bar": "baz" 135 | }, 136 | "qux": { 137 | "corge": "grault", 138 | "thud": "fred" 139 | } 140 | } 141 | }, 142 | { 143 | "comment": "A.7. Moving an Array Element", 144 | "doc": { 145 | "foo": [ 146 | "all", 147 | "grass", 148 | "cows", 149 | "eat" 150 | ] 151 | }, 152 | "patch": [ 153 | { 154 | "op": "move", 155 | "from": "/foo/1", 156 | "path": "/foo/3" 157 | } 158 | ], 159 | "expected": { 160 | "foo": [ 161 | "all", 162 | "cows", 163 | "eat", 164 | "grass" 165 | ] 166 | } 167 | }, 168 | { 169 | "comment": "A.8. Testing a Value: Success", 170 | "doc": { 171 | "baz": "qux", 172 | "foo": [ 173 | "a", 174 | 2, 175 | "c" 176 | ] 177 | }, 178 | "patch": [ 179 | { 180 | "op": "test", 181 | "path": "/baz", 182 | "value": "qux" 183 | }, 184 | { 185 | "op": "test", 186 | "path": "/foo/1", 187 | "value": 2 188 | } 189 | ], 190 | "expected": { 191 | "baz": "qux", 192 | "foo": [ 193 | "a", 194 | 2, 195 | "c" 196 | ] 197 | } 198 | }, 199 | { 200 | "comment": "A.9. Testing a Value: Error", 201 | "doc": { 202 | "baz": "qux" 203 | }, 204 | "patch": [ 205 | { 206 | "op": "test", 207 | "path": "/baz", 208 | "value": "bar" 209 | } 210 | ], 211 | "error": "string not equivalent" 212 | }, 213 | { 214 | "comment": "A.10. Adding a nested Member Object", 215 | "doc": { 216 | "foo": "bar" 217 | }, 218 | "patch": [ 219 | { 220 | "op": "add", 221 | "path": "/child", 222 | "value": { 223 | "grandchild": {} 224 | } 225 | } 226 | ], 227 | "expected": { 228 | "foo": "bar", 229 | "child": { 230 | "grandchild": { 231 | } 232 | } 233 | } 234 | }, 235 | { 236 | "comment": "A.11. Ignoring Unrecognized Elements", 237 | "doc": { 238 | "foo": "bar" 239 | }, 240 | "patch": [ 241 | { 242 | "op": "add", 243 | "path": "/baz", 244 | "value": "qux", 245 | "xyz": 123 246 | } 247 | ], 248 | "expected": { 249 | "foo": "bar", 250 | "baz": "qux" 251 | } 252 | }, 253 | { 254 | "comment": "A.12. Adding to a Non-existent Target", 255 | "doc": { 256 | "foo": "bar" 257 | }, 258 | "patch": [ 259 | { 260 | "op": "add", 261 | "path": "/baz/bat", 262 | "value": "qux" 263 | } 264 | ], 265 | "error": "add to a non-existent target" 266 | }, 267 | { 268 | "comment": "A.13 Invalid JSON Patch Document", 269 | "doc": { 270 | "foo": "bar" 271 | }, 272 | "patch": [ 273 | { 274 | "op": "add", 275 | "path": "/baz", 276 | "value": "qux", 277 | "op": "remove" 278 | } 279 | ], 280 | "error": "operation has two 'op' members", 281 | "disabled": true 282 | }, 283 | { 284 | "comment": "A.14. ~ Escape Ordering", 285 | "doc": { 286 | "/": 9, 287 | "~1": 10 288 | }, 289 | "patch": [ 290 | { 291 | "op": "test", 292 | "path": "/~01", 293 | "value": 10 294 | } 295 | ], 296 | "expected": { 297 | "/": 9, 298 | "~1": 10 299 | } 300 | }, 301 | { 302 | "comment": "A.15. Comparing Strings and Numbers", 303 | "doc": { 304 | "/": 9, 305 | "~1": 10 306 | }, 307 | "patch": [ 308 | { 309 | "op": "test", 310 | "path": "/~01", 311 | "value": "10" 312 | } 313 | ], 314 | "error": "number is not equal to string" 315 | }, 316 | { 317 | "comment": "A.16. Adding an Array Value", 318 | "doc": { 319 | "foo": [ 320 | "bar" 321 | ] 322 | }, 323 | "patch": [ 324 | { 325 | "op": "add", 326 | "path": "/foo/-", 327 | "value": [ 328 | "abc", 329 | "def" 330 | ] 331 | } 332 | ], 333 | "expected": { 334 | "foo": [ 335 | "bar", 336 | [ 337 | "abc", 338 | "def" 339 | ] 340 | ] 341 | } 342 | } 343 | ] 344 | -------------------------------------------------------------------------------- /specs/tests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "empty list, empty docs", 4 | "doc": {}, 5 | "patch": [], 6 | "expected": {} 7 | }, 8 | { 9 | "comment": "empty patch list", 10 | "doc": { 11 | "foo": 1 12 | }, 13 | "patch": [], 14 | "expected": { 15 | "foo": 1 16 | } 17 | }, 18 | { 19 | "comment": "rearrangements OK?", 20 | "doc": { 21 | "foo": 1, 22 | "bar": 2 23 | }, 24 | "patch": [], 25 | "expected": { 26 | "bar": 2, 27 | "foo": 1 28 | } 29 | }, 30 | { 31 | "comment": "rearrangements OK? How about one level down ... array", 32 | "doc": [ 33 | { 34 | "foo": 1, 35 | "bar": 2 36 | } 37 | ], 38 | "patch": [], 39 | "expected": [ 40 | { 41 | "bar": 2, 42 | "foo": 1 43 | } 44 | ] 45 | }, 46 | { 47 | "comment": "rearrangements OK? How about one level down...", 48 | "doc": { 49 | "foo": { 50 | "foo": 1, 51 | "bar": 2 52 | } 53 | }, 54 | "patch": [], 55 | "expected": { 56 | "foo": { 57 | "bar": 2, 58 | "foo": 1 59 | } 60 | } 61 | }, 62 | { 63 | "comment": "add replaces any existing field", 64 | "doc": { 65 | "foo": null 66 | }, 67 | "patch": [ 68 | { 69 | "op": "add", 70 | "path": "/foo", 71 | "value": 1 72 | } 73 | ], 74 | "expected": { 75 | "foo": 1 76 | } 77 | }, 78 | { 79 | "comment": "toplevel array", 80 | "doc": [], 81 | "patch": [ 82 | { 83 | "op": "add", 84 | "path": "/0", 85 | "value": "foo" 86 | } 87 | ], 88 | "expected": [ 89 | "foo" 90 | ] 91 | }, 92 | { 93 | "comment": "toplevel array, no change", 94 | "doc": [ 95 | "foo" 96 | ], 97 | "patch": [], 98 | "expected": [ 99 | "foo" 100 | ] 101 | }, 102 | { 103 | "comment": "toplevel object, numeric string", 104 | "doc": {}, 105 | "patch": [ 106 | { 107 | "op": "add", 108 | "path": "/foo", 109 | "value": "1" 110 | } 111 | ], 112 | "expected": { 113 | "foo": "1" 114 | } 115 | }, 116 | { 117 | "comment": "toplevel object, integer", 118 | "doc": {}, 119 | "patch": [ 120 | { 121 | "op": "add", 122 | "path": "/foo", 123 | "value": 1 124 | } 125 | ], 126 | "expected": { 127 | "foo": 1 128 | } 129 | }, 130 | { 131 | "comment": "Toplevel scalar values OK?", 132 | "doc": "foo", 133 | "patch": [ 134 | { 135 | "op": "replace", 136 | "path": "", 137 | "value": "bar" 138 | } 139 | ], 140 | "expected": "bar", 141 | "disabled": true 142 | }, 143 | { 144 | "comment": "replace object document with array document?", 145 | "doc": {}, 146 | "patch": [ 147 | { 148 | "op": "add", 149 | "path": "", 150 | "value": [] 151 | } 152 | ], 153 | "expected": [] 154 | }, 155 | { 156 | "comment": "replace array document with object document?", 157 | "doc": [], 158 | "patch": [ 159 | { 160 | "op": "add", 161 | "path": "", 162 | "value": {} 163 | } 164 | ], 165 | "expected": {} 166 | }, 167 | { 168 | "comment": "append to root array document?", 169 | "doc": [], 170 | "patch": [ 171 | { 172 | "op": "add", 173 | "path": "/-", 174 | "value": "hi" 175 | } 176 | ], 177 | "expected": [ 178 | "hi" 179 | ] 180 | }, 181 | { 182 | "comment": "Add, / target", 183 | "doc": {}, 184 | "patch": [ 185 | { 186 | "op": "add", 187 | "path": "/", 188 | "value": 1 189 | } 190 | ], 191 | "expected": { 192 | "": 1 193 | } 194 | }, 195 | { 196 | "comment": "Add, /foo/ deep target (trailing slash)", 197 | "doc": { 198 | "foo": {} 199 | }, 200 | "patch": [ 201 | { 202 | "op": "add", 203 | "path": "/foo/", 204 | "value": 1 205 | } 206 | ], 207 | "expected": { 208 | "foo": { 209 | "": 1 210 | } 211 | } 212 | }, 213 | { 214 | "comment": "Add composite value at top level", 215 | "doc": { 216 | "foo": 1 217 | }, 218 | "patch": [ 219 | { 220 | "op": "add", 221 | "path": "/bar", 222 | "value": [ 223 | 1, 224 | 2 225 | ] 226 | } 227 | ], 228 | "expected": { 229 | "foo": 1, 230 | "bar": [ 231 | 1, 232 | 2 233 | ] 234 | } 235 | }, 236 | { 237 | "comment": "Add into composite value", 238 | "doc": { 239 | "foo": 1, 240 | "baz": [ 241 | { 242 | "qux": "hello" 243 | } 244 | ] 245 | }, 246 | "patch": [ 247 | { 248 | "op": "add", 249 | "path": "/baz/0/foo", 250 | "value": "world" 251 | } 252 | ], 253 | "expected": { 254 | "foo": 1, 255 | "baz": [ 256 | { 257 | "qux": "hello", 258 | "foo": "world" 259 | } 260 | ] 261 | } 262 | }, 263 | { 264 | "doc": { 265 | "bar": [ 266 | 1, 267 | 2 268 | ] 269 | }, 270 | "patch": [ 271 | { 272 | "op": "add", 273 | "path": "/bar/8", 274 | "value": "5" 275 | } 276 | ], 277 | "error": "Out of bounds (upper)" 278 | }, 279 | { 280 | "doc": { 281 | "bar": [ 282 | 1, 283 | 2 284 | ] 285 | }, 286 | "patch": [ 287 | { 288 | "op": "add", 289 | "path": "/bar/-1", 290 | "value": "5" 291 | } 292 | ], 293 | "error": "Out of bounds (lower)" 294 | }, 295 | { 296 | "doc": { 297 | "foo": 1 298 | }, 299 | "patch": [ 300 | { 301 | "op": "add", 302 | "path": "/bar", 303 | "value": true 304 | } 305 | ], 306 | "expected": { 307 | "foo": 1, 308 | "bar": true 309 | } 310 | }, 311 | { 312 | "doc": { 313 | "foo": 1 314 | }, 315 | "patch": [ 316 | { 317 | "op": "add", 318 | "path": "/bar", 319 | "value": false 320 | } 321 | ], 322 | "expected": { 323 | "foo": 1, 324 | "bar": false 325 | } 326 | }, 327 | { 328 | "doc": { 329 | "foo": 1 330 | }, 331 | "patch": [ 332 | { 333 | "op": "add", 334 | "path": "/bar", 335 | "value": null 336 | } 337 | ], 338 | "expected": { 339 | "foo": 1, 340 | "bar": null 341 | } 342 | }, 343 | { 344 | "comment": "0 can be an array index or object element name", 345 | "doc": { 346 | "foo": 1 347 | }, 348 | "patch": [ 349 | { 350 | "op": "add", 351 | "path": "/0", 352 | "value": "bar" 353 | } 354 | ], 355 | "expected": { 356 | "foo": 1, 357 | "0": "bar" 358 | } 359 | }, 360 | { 361 | "doc": [ 362 | "foo" 363 | ], 364 | "patch": [ 365 | { 366 | "op": "add", 367 | "path": "/1", 368 | "value": "bar" 369 | } 370 | ], 371 | "expected": [ 372 | "foo", 373 | "bar" 374 | ] 375 | }, 376 | { 377 | "doc": [ 378 | "foo", 379 | "sil" 380 | ], 381 | "patch": [ 382 | { 383 | "op": "add", 384 | "path": "/1", 385 | "value": "bar" 386 | } 387 | ], 388 | "expected": [ 389 | "foo", 390 | "bar", 391 | "sil" 392 | ] 393 | }, 394 | { 395 | "doc": [ 396 | "foo", 397 | "sil" 398 | ], 399 | "patch": [ 400 | { 401 | "op": "add", 402 | "path": "/0", 403 | "value": "bar" 404 | } 405 | ], 406 | "expected": [ 407 | "bar", 408 | "foo", 409 | "sil" 410 | ] 411 | }, 412 | { 413 | "comment": "push item to array via last index + 1", 414 | "doc": [ 415 | "foo", 416 | "sil" 417 | ], 418 | "patch": [ 419 | { 420 | "op": "add", 421 | "path": "/2", 422 | "value": "bar" 423 | } 424 | ], 425 | "expected": [ 426 | "foo", 427 | "sil", 428 | "bar" 429 | ] 430 | }, 431 | { 432 | "comment": "add item to array at index > length should fail", 433 | "doc": [ 434 | "foo", 435 | "sil" 436 | ], 437 | "patch": [ 438 | { 439 | "op": "add", 440 | "path": "/3", 441 | "value": "bar" 442 | } 443 | ], 444 | "error": "index is greater than number of items in array" 445 | }, 446 | { 447 | "comment": "test against implementation-specific numeric parsing", 448 | "doc": { 449 | "1e0": "foo" 450 | }, 451 | "patch": [ 452 | { 453 | "op": "test", 454 | "path": "/1e0", 455 | "value": "foo" 456 | } 457 | ], 458 | "expected": { 459 | "1e0": "foo" 460 | } 461 | }, 462 | { 463 | "comment": "test with bad number should fail", 464 | "doc": [ 465 | "foo", 466 | "bar" 467 | ], 468 | "patch": [ 469 | { 470 | "op": "test", 471 | "path": "/1e0", 472 | "value": "bar" 473 | } 474 | ], 475 | "error": "test op shouldn't get array element 1" 476 | }, 477 | { 478 | "doc": [ 479 | "foo", 480 | "sil" 481 | ], 482 | "patch": [ 483 | { 484 | "op": "add", 485 | "path": "/bar", 486 | "value": 42 487 | } 488 | ], 489 | "error": "Object operation on array target" 490 | }, 491 | { 492 | "doc": [ 493 | "foo", 494 | "sil" 495 | ], 496 | "patch": [ 497 | { 498 | "op": "add", 499 | "path": "/1", 500 | "value": [ 501 | "bar", 502 | "baz" 503 | ] 504 | } 505 | ], 506 | "expected": [ 507 | "foo", 508 | [ 509 | "bar", 510 | "baz" 511 | ], 512 | "sil" 513 | ], 514 | "comment": "value in array add not flattened" 515 | }, 516 | { 517 | "doc": { 518 | "foo": 1, 519 | "bar": [ 520 | 1, 521 | 2, 522 | 3, 523 | 4 524 | ] 525 | }, 526 | "patch": [ 527 | { 528 | "op": "remove", 529 | "path": "/bar" 530 | } 531 | ], 532 | "expected": { 533 | "foo": 1 534 | } 535 | }, 536 | { 537 | "doc": { 538 | "foo": 1, 539 | "baz": [ 540 | { 541 | "qux": "hello" 542 | } 543 | ] 544 | }, 545 | "patch": [ 546 | { 547 | "op": "remove", 548 | "path": "/baz/0/qux" 549 | } 550 | ], 551 | "expected": { 552 | "foo": 1, 553 | "baz": [ 554 | {} 555 | ] 556 | } 557 | }, 558 | { 559 | "doc": { 560 | "foo": 1, 561 | "baz": [ 562 | { 563 | "qux": "hello" 564 | } 565 | ] 566 | }, 567 | "patch": [ 568 | { 569 | "op": "replace", 570 | "path": "/foo", 571 | "value": [ 572 | 1, 573 | 2, 574 | 3, 575 | 4 576 | ] 577 | } 578 | ], 579 | "expected": { 580 | "foo": [ 581 | 1, 582 | 2, 583 | 3, 584 | 4 585 | ], 586 | "baz": [ 587 | { 588 | "qux": "hello" 589 | } 590 | ] 591 | } 592 | }, 593 | { 594 | "doc": { 595 | "foo": [ 596 | 1, 597 | 2, 598 | 3, 599 | 4 600 | ], 601 | "baz": [ 602 | { 603 | "qux": "hello" 604 | } 605 | ] 606 | }, 607 | "patch": [ 608 | { 609 | "op": "replace", 610 | "path": "/baz/0/qux", 611 | "value": "world" 612 | } 613 | ], 614 | "expected": { 615 | "foo": [ 616 | 1, 617 | 2, 618 | 3, 619 | 4 620 | ], 621 | "baz": [ 622 | { 623 | "qux": "world" 624 | } 625 | ] 626 | } 627 | }, 628 | { 629 | "doc": [ 630 | "foo" 631 | ], 632 | "patch": [ 633 | { 634 | "op": "replace", 635 | "path": "/0", 636 | "value": "bar" 637 | } 638 | ], 639 | "expected": [ 640 | "bar" 641 | ] 642 | }, 643 | { 644 | "doc": [ 645 | "" 646 | ], 647 | "patch": [ 648 | { 649 | "op": "replace", 650 | "path": "/0", 651 | "value": 0 652 | } 653 | ], 654 | "expected": [ 655 | 0 656 | ] 657 | }, 658 | { 659 | "doc": [ 660 | "" 661 | ], 662 | "patch": [ 663 | { 664 | "op": "replace", 665 | "path": "/0", 666 | "value": true 667 | } 668 | ], 669 | "expected": [ 670 | true 671 | ] 672 | }, 673 | { 674 | "doc": [ 675 | "" 676 | ], 677 | "patch": [ 678 | { 679 | "op": "replace", 680 | "path": "/0", 681 | "value": false 682 | } 683 | ], 684 | "expected": [ 685 | false 686 | ] 687 | }, 688 | { 689 | "doc": [ 690 | "" 691 | ], 692 | "patch": [ 693 | { 694 | "op": "replace", 695 | "path": "/0", 696 | "value": null 697 | } 698 | ], 699 | "expected": [ 700 | null 701 | ] 702 | }, 703 | { 704 | "doc": [ 705 | "foo", 706 | "sil" 707 | ], 708 | "patch": [ 709 | { 710 | "op": "replace", 711 | "path": "/1", 712 | "value": [ 713 | "bar", 714 | "baz" 715 | ] 716 | } 717 | ], 718 | "expected": [ 719 | "foo", 720 | [ 721 | "bar", 722 | "baz" 723 | ] 724 | ], 725 | "comment": "value in array replace not flattened" 726 | }, 727 | { 728 | "comment": "replace whole document", 729 | "doc": { 730 | "foo": "bar" 731 | }, 732 | "patch": [ 733 | { 734 | "op": "replace", 735 | "path": "", 736 | "value": { 737 | "baz": "qux" 738 | } 739 | } 740 | ], 741 | "expected": { 742 | "baz": "qux" 743 | } 744 | }, 745 | { 746 | "comment": "spurious patch properties", 747 | "doc": { 748 | "foo": 1 749 | }, 750 | "patch": [ 751 | { 752 | "op": "test", 753 | "path": "/foo", 754 | "value": 1, 755 | "spurious": 1 756 | } 757 | ], 758 | "expected": { 759 | "foo": 1 760 | } 761 | }, 762 | { 763 | "doc": { 764 | "foo": null 765 | }, 766 | "patch": [ 767 | { 768 | "op": "test", 769 | "path": "/foo", 770 | "value": null 771 | } 772 | ], 773 | "comment": "null value should be valid obj property" 774 | }, 775 | { 776 | "doc": { 777 | "foo": null 778 | }, 779 | "patch": [ 780 | { 781 | "op": "replace", 782 | "path": "/foo", 783 | "value": "truthy" 784 | } 785 | ], 786 | "expected": { 787 | "foo": "truthy" 788 | }, 789 | "comment": "null value should be valid obj property to be replaced with something truthy" 790 | }, 791 | { 792 | "doc": { 793 | "foo": null 794 | }, 795 | "patch": [ 796 | { 797 | "op": "move", 798 | "from": "/foo", 799 | "path": "/bar" 800 | } 801 | ], 802 | "expected": { 803 | "bar": null 804 | }, 805 | "comment": "null value should be valid obj property to be moved" 806 | }, 807 | { 808 | "doc": { 809 | "foo": null 810 | }, 811 | "patch": [ 812 | { 813 | "op": "copy", 814 | "from": "/foo", 815 | "path": "/bar" 816 | } 817 | ], 818 | "expected": { 819 | "foo": null, 820 | "bar": null 821 | }, 822 | "comment": "null value should be valid obj property to be copied" 823 | }, 824 | { 825 | "doc": { 826 | "foo": null 827 | }, 828 | "patch": [ 829 | { 830 | "op": "remove", 831 | "path": "/foo" 832 | } 833 | ], 834 | "expected": {}, 835 | "comment": "null value should be valid obj property to be removed" 836 | }, 837 | { 838 | "doc": { 839 | "foo": "bar" 840 | }, 841 | "patch": [ 842 | { 843 | "op": "replace", 844 | "path": "/foo", 845 | "value": null 846 | } 847 | ], 848 | "expected": { 849 | "foo": null 850 | }, 851 | "comment": "null value should still be valid obj property replace other value" 852 | }, 853 | { 854 | "doc": { 855 | "foo": { 856 | "foo": 1, 857 | "bar": 2 858 | } 859 | }, 860 | "patch": [ 861 | { 862 | "op": "test", 863 | "path": "/foo", 864 | "value": { 865 | "bar": 2, 866 | "foo": 1 867 | } 868 | } 869 | ], 870 | "comment": "test should pass despite rearrangement" 871 | }, 872 | { 873 | "doc": { 874 | "foo": [ 875 | { 876 | "foo": 1, 877 | "bar": 2 878 | } 879 | ] 880 | }, 881 | "patch": [ 882 | { 883 | "op": "test", 884 | "path": "/foo", 885 | "value": [ 886 | { 887 | "bar": 2, 888 | "foo": 1 889 | } 890 | ] 891 | } 892 | ], 893 | "comment": "test should pass despite (nested) rearrangement" 894 | }, 895 | { 896 | "doc": { 897 | "foo": { 898 | "bar": [ 899 | 1, 900 | 2, 901 | 5, 902 | 4 903 | ] 904 | } 905 | }, 906 | "patch": [ 907 | { 908 | "op": "test", 909 | "path": "/foo", 910 | "value": { 911 | "bar": [ 912 | 1, 913 | 2, 914 | 5, 915 | 4 916 | ] 917 | } 918 | } 919 | ], 920 | "comment": "test should pass - no error" 921 | }, 922 | { 923 | "doc": { 924 | "foo": { 925 | "bar": [ 926 | 1, 927 | 2, 928 | 5, 929 | 4 930 | ] 931 | } 932 | }, 933 | "patch": [ 934 | { 935 | "op": "test", 936 | "path": "/foo", 937 | "value": [ 938 | 1, 939 | 2 940 | ] 941 | } 942 | ], 943 | "error": "test op should fail" 944 | }, 945 | { 946 | "comment": "Whole document", 947 | "doc": { 948 | "foo": 1 949 | }, 950 | "patch": [ 951 | { 952 | "op": "test", 953 | "path": "", 954 | "value": { 955 | "foo": 1 956 | } 957 | } 958 | ], 959 | "disabled": true 960 | }, 961 | { 962 | "comment": "Empty-string element", 963 | "doc": { 964 | "": 1 965 | }, 966 | "patch": [ 967 | { 968 | "op": "test", 969 | "path": "/", 970 | "value": 1 971 | } 972 | ] 973 | }, 974 | { 975 | "doc": { 976 | "foo": [ 977 | "bar", 978 | "baz" 979 | ], 980 | "": 0, 981 | "a/b": 1, 982 | "c%d": 2, 983 | "e^f": 3, 984 | "g|h": 4, 985 | "i\\j": 5, 986 | "k\"l": 6, 987 | " ": 7, 988 | "m~n": 8 989 | }, 990 | "patch": [ 991 | { 992 | "op": "test", 993 | "path": "/foo", 994 | "value": [ 995 | "bar", 996 | "baz" 997 | ] 998 | }, 999 | { 1000 | "op": "test", 1001 | "path": "/foo/0", 1002 | "value": "bar" 1003 | }, 1004 | { 1005 | "op": "test", 1006 | "path": "/", 1007 | "value": 0 1008 | }, 1009 | { 1010 | "op": "test", 1011 | "path": "/a~1b", 1012 | "value": 1 1013 | }, 1014 | { 1015 | "op": "test", 1016 | "path": "/c%d", 1017 | "value": 2 1018 | }, 1019 | { 1020 | "op": "test", 1021 | "path": "/e^f", 1022 | "value": 3 1023 | }, 1024 | { 1025 | "op": "test", 1026 | "path": "/g|h", 1027 | "value": 4 1028 | }, 1029 | { 1030 | "op": "test", 1031 | "path": "/i\\j", 1032 | "value": 5 1033 | }, 1034 | { 1035 | "op": "test", 1036 | "path": "/k\"l", 1037 | "value": 6 1038 | }, 1039 | { 1040 | "op": "test", 1041 | "path": "/ ", 1042 | "value": 7 1043 | }, 1044 | { 1045 | "op": "test", 1046 | "path": "/m~0n", 1047 | "value": 8 1048 | } 1049 | ] 1050 | }, 1051 | { 1052 | "comment": "Move to same location has no effect", 1053 | "doc": { 1054 | "foo": 1 1055 | }, 1056 | "patch": [ 1057 | { 1058 | "op": "move", 1059 | "from": "/foo", 1060 | "path": "/foo" 1061 | } 1062 | ], 1063 | "expected": { 1064 | "foo": 1 1065 | } 1066 | }, 1067 | { 1068 | "doc": { 1069 | "foo": 1, 1070 | "baz": [ 1071 | { 1072 | "qux": "hello" 1073 | } 1074 | ] 1075 | }, 1076 | "patch": [ 1077 | { 1078 | "op": "move", 1079 | "from": "/foo", 1080 | "path": "/bar" 1081 | } 1082 | ], 1083 | "expected": { 1084 | "baz": [ 1085 | { 1086 | "qux": "hello" 1087 | } 1088 | ], 1089 | "bar": 1 1090 | } 1091 | }, 1092 | { 1093 | "doc": { 1094 | "baz": [ 1095 | { 1096 | "qux": "hello" 1097 | } 1098 | ], 1099 | "bar": 1 1100 | }, 1101 | "patch": [ 1102 | { 1103 | "op": "move", 1104 | "from": "/baz/0/qux", 1105 | "path": "/baz/1" 1106 | } 1107 | ], 1108 | "expected": { 1109 | "baz": [ 1110 | {}, 1111 | "hello" 1112 | ], 1113 | "bar": 1 1114 | } 1115 | }, 1116 | { 1117 | "doc": { 1118 | "baz": [ 1119 | { 1120 | "qux": "hello" 1121 | } 1122 | ], 1123 | "bar": 1 1124 | }, 1125 | "patch": [ 1126 | { 1127 | "op": "copy", 1128 | "from": "/baz/0", 1129 | "path": "/boo" 1130 | } 1131 | ], 1132 | "expected": { 1133 | "baz": [ 1134 | { 1135 | "qux": "hello" 1136 | } 1137 | ], 1138 | "bar": 1, 1139 | "boo": { 1140 | "qux": "hello" 1141 | } 1142 | } 1143 | }, 1144 | { 1145 | "comment": "replacing the root of the document is possible with add", 1146 | "doc": { 1147 | "foo": "bar" 1148 | }, 1149 | "patch": [ 1150 | { 1151 | "op": "add", 1152 | "path": "", 1153 | "value": { 1154 | "baz": "qux" 1155 | } 1156 | } 1157 | ], 1158 | "expected": { 1159 | "baz": "qux" 1160 | } 1161 | }, 1162 | { 1163 | "comment": "Adding to \"/-\" adds to the end of the array", 1164 | "doc": [ 1165 | 1, 1166 | 2 1167 | ], 1168 | "patch": [ 1169 | { 1170 | "op": "add", 1171 | "path": "/-", 1172 | "value": { 1173 | "foo": [ 1174 | "bar", 1175 | "baz" 1176 | ] 1177 | } 1178 | } 1179 | ], 1180 | "expected": [ 1181 | 1, 1182 | 2, 1183 | { 1184 | "foo": [ 1185 | "bar", 1186 | "baz" 1187 | ] 1188 | } 1189 | ] 1190 | }, 1191 | { 1192 | "comment": "Adding to \"/-\" adds to the end of the array, even n levels down", 1193 | "doc": [ 1194 | 1, 1195 | 2, 1196 | [ 1197 | 3, 1198 | [ 1199 | 4, 1200 | 5 1201 | ] 1202 | ] 1203 | ], 1204 | "patch": [ 1205 | { 1206 | "op": "add", 1207 | "path": "/2/1/-", 1208 | "value": { 1209 | "foo": [ 1210 | "bar", 1211 | "baz" 1212 | ] 1213 | } 1214 | } 1215 | ], 1216 | "expected": [ 1217 | 1, 1218 | 2, 1219 | [ 1220 | 3, 1221 | [ 1222 | 4, 1223 | 5, 1224 | { 1225 | "foo": [ 1226 | "bar", 1227 | "baz" 1228 | ] 1229 | } 1230 | ] 1231 | ] 1232 | ] 1233 | }, 1234 | { 1235 | "comment": "test remove with bad number should fail", 1236 | "doc": { 1237 | "foo": 1, 1238 | "baz": [ 1239 | { 1240 | "qux": "hello" 1241 | } 1242 | ] 1243 | }, 1244 | "patch": [ 1245 | { 1246 | "op": "remove", 1247 | "path": "/baz/1e0/qux" 1248 | } 1249 | ], 1250 | "error": "remove op shouldn't remove from array with bad number" 1251 | }, 1252 | { 1253 | "comment": "test remove on array", 1254 | "doc": [ 1255 | 1, 1256 | 2, 1257 | 3, 1258 | 4 1259 | ], 1260 | "patch": [ 1261 | { 1262 | "op": "remove", 1263 | "path": "/0" 1264 | } 1265 | ], 1266 | "expected": [ 1267 | 2, 1268 | 3, 1269 | 4 1270 | ] 1271 | }, 1272 | { 1273 | "comment": "test repeated removes", 1274 | "doc": [ 1275 | 1, 1276 | 2, 1277 | 3, 1278 | 4 1279 | ], 1280 | "patch": [ 1281 | { 1282 | "op": "remove", 1283 | "path": "/1" 1284 | }, 1285 | { 1286 | "op": "remove", 1287 | "path": "/2" 1288 | } 1289 | ], 1290 | "expected": [ 1291 | 1, 1292 | 3 1293 | ] 1294 | }, 1295 | { 1296 | "comment": "test remove with bad index should fail", 1297 | "doc": [ 1298 | 1, 1299 | 2, 1300 | 3, 1301 | 4 1302 | ], 1303 | "patch": [ 1304 | { 1305 | "op": "remove", 1306 | "path": "/1e0" 1307 | } 1308 | ], 1309 | "error": "remove op shouldn't remove from array with bad number" 1310 | }, 1311 | { 1312 | "comment": "test replace with bad number should fail", 1313 | "doc": [ 1314 | "" 1315 | ], 1316 | "patch": [ 1317 | { 1318 | "op": "replace", 1319 | "path": "/1e0", 1320 | "value": false 1321 | } 1322 | ], 1323 | "error": "replace op shouldn't replace in array with bad number" 1324 | }, 1325 | { 1326 | "comment": "test copy with bad number should fail", 1327 | "doc": { 1328 | "baz": [ 1329 | 1, 1330 | 2, 1331 | 3 1332 | ], 1333 | "bar": 1 1334 | }, 1335 | "patch": [ 1336 | { 1337 | "op": "copy", 1338 | "from": "/baz/1e0", 1339 | "path": "/boo" 1340 | } 1341 | ], 1342 | "error": "copy op shouldn't work with bad number" 1343 | }, 1344 | { 1345 | "comment": "test move with bad number should fail", 1346 | "doc": { 1347 | "foo": 1, 1348 | "baz": [ 1349 | 1, 1350 | 2, 1351 | 3, 1352 | 4 1353 | ] 1354 | }, 1355 | "patch": [ 1356 | { 1357 | "op": "move", 1358 | "from": "/baz/1e0", 1359 | "path": "/foo" 1360 | } 1361 | ], 1362 | "error": "move op shouldn't work with bad number" 1363 | }, 1364 | { 1365 | "comment": "test add with bad number should fail", 1366 | "doc": [ 1367 | "foo", 1368 | "sil" 1369 | ], 1370 | "patch": [ 1371 | { 1372 | "op": "add", 1373 | "path": "/1e0", 1374 | "value": "bar" 1375 | } 1376 | ], 1377 | "error": "add op shouldn't add to array with bad number" 1378 | }, 1379 | { 1380 | "comment": "missing 'value' parameter to add", 1381 | "doc": [ 1382 | 1 1383 | ], 1384 | "patch": [ 1385 | { 1386 | "op": "add", 1387 | "path": "/-" 1388 | } 1389 | ], 1390 | "error": "missing 'value' parameter" 1391 | }, 1392 | { 1393 | "comment": "missing 'value' parameter to replace", 1394 | "doc": [ 1395 | 1 1396 | ], 1397 | "patch": [ 1398 | { 1399 | "op": "replace", 1400 | "path": "/0" 1401 | } 1402 | ], 1403 | "error": "missing 'value' parameter" 1404 | }, 1405 | { 1406 | "comment": "missing 'value' parameter to test", 1407 | "doc": [ 1408 | null 1409 | ], 1410 | "patch": [ 1411 | { 1412 | "op": "test", 1413 | "path": "/0" 1414 | } 1415 | ], 1416 | "error": "missing 'value' parameter" 1417 | }, 1418 | { 1419 | "comment": "missing value parameter to test - where undef is falsy", 1420 | "doc": [ 1421 | false 1422 | ], 1423 | "patch": [ 1424 | { 1425 | "op": "test", 1426 | "path": "/0" 1427 | } 1428 | ], 1429 | "error": "missing 'value' parameter" 1430 | }, 1431 | { 1432 | "comment": "missing from parameter to copy", 1433 | "doc": [ 1434 | 1 1435 | ], 1436 | "patch": [ 1437 | { 1438 | "op": "copy", 1439 | "path": "/-" 1440 | } 1441 | ], 1442 | "error": "missing 'from' parameter" 1443 | }, 1444 | { 1445 | "comment": "missing from parameter to move", 1446 | "doc": { 1447 | "foo": 1 1448 | }, 1449 | "patch": [ 1450 | { 1451 | "op": "move", 1452 | "path": "" 1453 | } 1454 | ], 1455 | "error": "missing 'from' parameter" 1456 | }, 1457 | { 1458 | "comment": "duplicate ops", 1459 | "doc": { 1460 | "foo": "bar" 1461 | }, 1462 | "patch": [ 1463 | { 1464 | "op": "add", 1465 | "path": "/baz", 1466 | "value": "qux", 1467 | "op": "move", 1468 | "from": "/foo" 1469 | } 1470 | ], 1471 | "error": "patch has two 'op' members", 1472 | "disabled": true 1473 | }, 1474 | { 1475 | "comment": "unrecognized op should fail", 1476 | "doc": { 1477 | "foo": 1 1478 | }, 1479 | "patch": [ 1480 | { 1481 | "op": "spam", 1482 | "path": "/foo", 1483 | "value": 1 1484 | } 1485 | ], 1486 | "error": "Unrecognized op 'spam'" 1487 | }, 1488 | { 1489 | "comment": "test with bad array number that has leading zeros", 1490 | "doc": [ 1491 | "foo", 1492 | "bar" 1493 | ], 1494 | "patch": [ 1495 | { 1496 | "op": "test", 1497 | "path": "/00", 1498 | "value": "foo" 1499 | } 1500 | ], 1501 | "error": "test op should reject the array value, it has leading zeros" 1502 | }, 1503 | { 1504 | "comment": "test with bad array number that has leading zeros", 1505 | "doc": [ 1506 | "foo", 1507 | "bar" 1508 | ], 1509 | "patch": [ 1510 | { 1511 | "op": "test", 1512 | "path": "/01", 1513 | "value": "bar" 1514 | } 1515 | ], 1516 | "error": "test op should reject the array value, it has leading zeros" 1517 | }, 1518 | { 1519 | "comment": "Removing nonexistent field", 1520 | "doc": { 1521 | "foo": "bar" 1522 | }, 1523 | "patch": [ 1524 | { 1525 | "op": "remove", 1526 | "path": "/baz" 1527 | } 1528 | ], 1529 | "error": "removing a nonexistent field should fail" 1530 | }, 1531 | { 1532 | "comment": "Removing nonexistent index", 1533 | "doc": [ 1534 | "foo", 1535 | "bar" 1536 | ], 1537 | "patch": [ 1538 | { 1539 | "op": "remove", 1540 | "path": "/2" 1541 | } 1542 | ], 1543 | "error": "removing a nonexistent index should fail" 1544 | }, 1545 | { 1546 | "comment": "Patch with different capitalisation than doc", 1547 | "doc": { 1548 | "foo": "bar" 1549 | }, 1550 | "patch": [ 1551 | { 1552 | "op": "add", 1553 | "path": "/FOO", 1554 | "value": "BAR" 1555 | } 1556 | ], 1557 | "expected": { 1558 | "foo": "bar", 1559 | "FOO": "BAR" 1560 | } 1561 | }, 1562 | { 1563 | "comment": "Cannot index literal (add)", 1564 | "doc": { 1565 | "foo": true 1566 | }, 1567 | "patch": [ 1568 | { 1569 | "op": "add", 1570 | "path": "/foo/bar", 1571 | "value": "BAR" 1572 | } 1573 | ], 1574 | "error": "cannot index literal" 1575 | }, 1576 | { 1577 | "comment": "Cannot index literal (remove)", 1578 | "doc": { 1579 | "foo": true 1580 | }, 1581 | "patch": [ 1582 | { 1583 | "op": "remove", 1584 | "path": "/foo/bar" 1585 | } 1586 | ], 1587 | "error": "cannot index literal" 1588 | }, 1589 | { 1590 | "comment": "Invalid index", 1591 | "doc": { 1592 | "foo": true 1593 | }, 1594 | "patch": [ 1595 | { 1596 | "op": "add", 1597 | "path": "hello", 1598 | "value": "boo" 1599 | } 1600 | ], 1601 | "error": "cannot find parent" 1602 | }, 1603 | { 1604 | "comment": "Changes are atomic", 1605 | "doc": { 1606 | "foo": true 1607 | }, 1608 | "patch": [ 1609 | { 1610 | "op": "add", 1611 | "path": "/foo", 1612 | "value": false 1613 | }, 1614 | { 1615 | "op": "remove", 1616 | "path": "/bar" 1617 | } 1618 | ], 1619 | "error": "invalid pointer" 1620 | }, 1621 | { 1622 | "comment": "Slashes in object keys", 1623 | "doc": { 1624 | "a/b": true 1625 | }, 1626 | "patch": [ 1627 | { 1628 | "op": "add", 1629 | "path": "/a~1b", 1630 | "value": false 1631 | } 1632 | ], 1633 | "expected": { 1634 | "a/b": false 1635 | } 1636 | }, 1637 | { 1638 | "comment": "Slashes in parent object key", 1639 | "doc": { 1640 | "a/b": { 1641 | "foo": true 1642 | } 1643 | }, 1644 | "patch": [ 1645 | { 1646 | "op": "add", 1647 | "path": "/a~1b/foo", 1648 | "value": false 1649 | } 1650 | ], 1651 | "expected": { 1652 | "a/b": { 1653 | "foo": false 1654 | } 1655 | } 1656 | }, 1657 | { 1658 | "comment": "Substitution order (add)", 1659 | "doc": { 1660 | "~1": true 1661 | }, 1662 | "patch": [ 1663 | { 1664 | "op": "add", 1665 | "path": "/~01", 1666 | "value": false 1667 | } 1668 | ], 1669 | "expected": { 1670 | "~1": false 1671 | } 1672 | }, 1673 | { 1674 | "comment": "Substitution order (remove)", 1675 | "doc": { 1676 | "~1": true 1677 | }, 1678 | "patch": [ 1679 | { 1680 | "op": "remove", 1681 | "path": "/~01" 1682 | } 1683 | ], 1684 | "expected": { 1685 | } 1686 | }, 1687 | { 1688 | "comment": "Leading zeroes are not supported", 1689 | "doc": { 1690 | "foo": [1, 2, 3] 1691 | }, 1692 | "patch": [ 1693 | { 1694 | "op": "add", 1695 | "path": "/foo/002", 1696 | "value": 4 1697 | } 1698 | ], 1699 | "error": "invalid pointer" 1700 | }, 1701 | { 1702 | "comment": "Useless move is fine", 1703 | "doc": { 1704 | "foo": [1, 2, 3] 1705 | }, 1706 | "patch": [ 1707 | { 1708 | "op": "move", 1709 | "path": "/foo", 1710 | "from": "/foo" 1711 | } 1712 | ], 1713 | "expected": { 1714 | "foo": [1, 2, 3] 1715 | } 1716 | }, 1717 | { 1718 | "comment": "Cannot move inside children", 1719 | "doc": { 1720 | "foo": { 1721 | "bar": { 1722 | "baz": true 1723 | } 1724 | } 1725 | }, 1726 | "patch": [ 1727 | { 1728 | "op": "move", 1729 | "path": "/foo/bar", 1730 | "from": "/foo" 1731 | } 1732 | ], 1733 | "error": "cannot move inside children" 1734 | }, 1735 | { 1736 | "comment": "Can move into similar path", 1737 | "doc": { 1738 | "foo": { 1739 | "bar": { 1740 | "baz": true 1741 | } 1742 | } 1743 | }, 1744 | "patch": [ 1745 | { 1746 | "op": "move", 1747 | "path": "/foobar", 1748 | "from": "/foo" 1749 | } 1750 | ], 1751 | "expected": { 1752 | "foobar": { 1753 | "bar": { 1754 | "baz": true 1755 | } 1756 | } 1757 | } 1758 | }, 1759 | { 1760 | "comment": "Can move outside children", 1761 | "doc": { 1762 | "foo": { 1763 | "bar": { 1764 | "baz": true 1765 | } 1766 | } 1767 | }, 1768 | "patch": [ 1769 | { 1770 | "op": "move", 1771 | "path": "/foo", 1772 | "from": "/foo/bar" 1773 | } 1774 | ], 1775 | "expected": { 1776 | "foo": { 1777 | "baz": true 1778 | } 1779 | } 1780 | }, 1781 | { 1782 | "comment": "Can move into root", 1783 | "doc": { 1784 | "foo": { 1785 | "bar": { 1786 | "baz": true 1787 | } 1788 | } 1789 | }, 1790 | "patch": [ 1791 | { 1792 | "op": "move", 1793 | "path": "", 1794 | "from": "/foo/bar" 1795 | } 1796 | ], 1797 | "expected": { 1798 | "baz": true 1799 | } 1800 | }, 1801 | { 1802 | "comment": "cannot remove last", 1803 | "doc": { 1804 | "foo": [1, 2, 3] 1805 | }, 1806 | "patch": [ 1807 | { 1808 | "op": "remove", 1809 | "path": "/foo/-" 1810 | } 1811 | ], 1812 | "error": "invalid pointer" 1813 | }, 1814 | { 1815 | "comment": "cannot copy from last", 1816 | "doc": { 1817 | "foo": [1, 2, 3] 1818 | }, 1819 | "patch": [ 1820 | { 1821 | "op": "copy", 1822 | "from": "/foo/-", 1823 | "path": "/bar" 1824 | } 1825 | ], 1826 | "error": "invalid pointer" 1827 | }, 1828 | { 1829 | "comment": "cannot move from last", 1830 | "doc": { 1831 | "foo": [1, 2, 3] 1832 | }, 1833 | "patch": [ 1834 | { 1835 | "op": "move", 1836 | "from": "/foo/-", 1837 | "path": "/bar" 1838 | } 1839 | ], 1840 | "error": "invalid pointer" 1841 | }, 1842 | { 1843 | "comment": "can move into last element", 1844 | "doc": { 1845 | "foo": [1, 2, 3], 1846 | "bar": 4 1847 | }, 1848 | "patch": [ 1849 | { 1850 | "op": "move", 1851 | "from": "/bar", 1852 | "path": "/foo/-" 1853 | } 1854 | ], 1855 | "expected": { 1856 | "foo": [1, 2, 3, 4] 1857 | } 1858 | }, 1859 | { 1860 | "comment": "can copy into last element", 1861 | "doc": { 1862 | "foo": [1, 2, 3], 1863 | "bar": 4 1864 | }, 1865 | "patch": [ 1866 | { 1867 | "op": "copy", 1868 | "from": "/bar", 1869 | "path": "/foo/-" 1870 | } 1871 | ], 1872 | "expected": { 1873 | "foo": [1, 2, 3, 4], 1874 | "bar": 4 1875 | } 1876 | } 1877 | ] 1878 | -------------------------------------------------------------------------------- /src/diff.rs: -------------------------------------------------------------------------------- 1 | use crate::Patch; 2 | use jsonptr::PointerBuf; 3 | use serde_json::{Map, Value}; 4 | 5 | fn diff_impl(left: &Value, right: &Value, pointer: &mut PointerBuf, patch: &mut super::Patch) { 6 | match (left, right) { 7 | (Value::Object(ref left_obj), Value::Object(ref right_obj)) => { 8 | diff_object(left_obj, right_obj, pointer, patch); 9 | } 10 | (Value::Array(ref left_array), Value::Array(ref ref_array)) => { 11 | diff_array(left_array, ref_array, pointer, patch); 12 | } 13 | (_, _) if left == right => { 14 | // Nothing to do 15 | } 16 | (_, _) => { 17 | // Values are different, replace the value at the path 18 | patch 19 | .0 20 | .push(super::PatchOperation::Replace(super::ReplaceOperation { 21 | path: pointer.clone(), 22 | value: right.clone(), 23 | })); 24 | } 25 | } 26 | } 27 | 28 | fn diff_array(left: &[Value], right: &[Value], pointer: &mut PointerBuf, patch: &mut Patch) { 29 | let len = left.len().max(right.len()); 30 | let mut shift = 0usize; 31 | for idx in 0..len { 32 | pointer.push_back(idx - shift); 33 | match (left.get(idx), right.get(idx)) { 34 | (Some(left), Some(right)) => { 35 | // Both array have an element at this index 36 | diff_impl(left, right, pointer, patch); 37 | } 38 | (Some(_left), None) => { 39 | // The left array has an element at this index, but not the right 40 | shift += 1; 41 | patch 42 | .0 43 | .push(super::PatchOperation::Remove(super::RemoveOperation { 44 | path: pointer.clone(), 45 | })); 46 | } 47 | (None, Some(right)) => { 48 | // The right array has an element at this index, but not the left 49 | patch 50 | .0 51 | .push(super::PatchOperation::Add(super::AddOperation { 52 | path: pointer.clone(), 53 | value: right.clone(), 54 | })); 55 | } 56 | (None, None) => { 57 | unreachable!() 58 | } 59 | } 60 | pointer.pop_back(); 61 | } 62 | } 63 | 64 | fn diff_object( 65 | left: &Map, 66 | right: &Map, 67 | pointer: &mut PointerBuf, 68 | patch: &mut Patch, 69 | ) { 70 | // Add or replace keys in the right object 71 | for (key, right_value) in right { 72 | pointer.push_back(key); 73 | match left.get(key) { 74 | Some(left_value) => { 75 | diff_impl(left_value, right_value, pointer, patch); 76 | } 77 | None => { 78 | patch 79 | .0 80 | .push(super::PatchOperation::Add(super::AddOperation { 81 | path: pointer.clone(), 82 | value: right_value.clone(), 83 | })); 84 | } 85 | } 86 | pointer.pop_back(); 87 | } 88 | 89 | // Remove keys that are not in the right object 90 | for key in left.keys() { 91 | if !right.contains_key(key) { 92 | pointer.push_back(key); 93 | patch 94 | .0 95 | .push(super::PatchOperation::Remove(super::RemoveOperation { 96 | path: pointer.clone(), 97 | })); 98 | pointer.pop_back(); 99 | } 100 | } 101 | } 102 | 103 | /// Diff two JSON documents and generate a JSON Patch (RFC 6902). 104 | /// 105 | /// # Example 106 | /// Diff two JSONs: 107 | /// 108 | /// ```rust 109 | /// #[macro_use] 110 | /// use json_patch::{Patch, patch, diff}; 111 | /// use serde_json::{json, from_value}; 112 | /// 113 | /// # pub fn main() { 114 | /// let left = json!({ 115 | /// "title": "Goodbye!", 116 | /// "author" : { 117 | /// "givenName" : "John", 118 | /// "familyName" : "Doe" 119 | /// }, 120 | /// "tags":[ "example", "sample" ], 121 | /// "content": "This will be unchanged" 122 | /// }); 123 | /// 124 | /// let right = json!({ 125 | /// "title": "Hello!", 126 | /// "author" : { 127 | /// "givenName" : "John" 128 | /// }, 129 | /// "tags": [ "example" ], 130 | /// "content": "This will be unchanged", 131 | /// "phoneNumber": "+01-123-456-7890" 132 | /// }); 133 | /// 134 | /// let p = diff(&left, &right); 135 | /// assert_eq!(p, from_value::(json!([ 136 | /// { "op": "replace", "path": "/title", "value": "Hello!" }, 137 | /// { "op": "remove", "path": "/author/familyName" }, 138 | /// { "op": "remove", "path": "/tags/1" }, 139 | /// { "op": "add", "path": "/phoneNumber", "value": "+01-123-456-7890" }, 140 | /// ])).unwrap()); 141 | /// 142 | /// let mut doc = left.clone(); 143 | /// patch(&mut doc, &p).unwrap(); 144 | /// assert_eq!(doc, right); 145 | /// 146 | /// # } 147 | /// ``` 148 | pub fn diff(left: &Value, right: &Value) -> super::Patch { 149 | let mut patch = super::Patch::default(); 150 | let mut path = PointerBuf::new(); 151 | diff_impl(left, right, &mut path, &mut patch); 152 | patch 153 | } 154 | 155 | #[cfg(test)] 156 | mod tests { 157 | use serde_json::{json, Value}; 158 | 159 | #[test] 160 | pub fn replace_all() { 161 | let mut left = json!({"title": "Hello!"}); 162 | let patch = super::diff(&left, &Value::Null); 163 | assert_eq!( 164 | patch, 165 | serde_json::from_value(json!([ 166 | { "op": "replace", "path": "", "value": null }, 167 | ])) 168 | .unwrap() 169 | ); 170 | crate::patch(&mut left, &patch).unwrap(); 171 | } 172 | 173 | #[test] 174 | pub fn diff_empty_key() { 175 | let mut left = json!({"title": "Something", "": "Hello!"}); 176 | let right = json!({"title": "Something", "": "Bye!"}); 177 | let patch = super::diff(&left, &right); 178 | assert_eq!( 179 | patch, 180 | serde_json::from_value(json!([ 181 | { "op": "replace", "path": "/", "value": "Bye!" }, 182 | ])) 183 | .unwrap() 184 | ); 185 | crate::patch(&mut left, &patch).unwrap(); 186 | assert_eq!(left, right); 187 | } 188 | 189 | #[test] 190 | pub fn add_all() { 191 | let right = json!({"title": "Hello!"}); 192 | let patch = super::diff(&Value::Null, &right); 193 | assert_eq!( 194 | patch, 195 | serde_json::from_value(json!([ 196 | { "op": "replace", "path": "", "value": { "title": "Hello!" } }, 197 | ])) 198 | .unwrap() 199 | ); 200 | 201 | let mut left = Value::Null; 202 | crate::patch(&mut left, &patch).unwrap(); 203 | assert_eq!(left, right); 204 | } 205 | 206 | #[test] 207 | pub fn remove_all() { 208 | let mut left = json!(["hello", "bye"]); 209 | let right = json!([]); 210 | let patch = super::diff(&left, &right); 211 | assert_eq!( 212 | patch, 213 | serde_json::from_value(json!([ 214 | { "op": "remove", "path": "/0" }, 215 | { "op": "remove", "path": "/0" }, 216 | ])) 217 | .unwrap() 218 | ); 219 | 220 | crate::patch(&mut left, &patch).unwrap(); 221 | assert_eq!(left, right); 222 | } 223 | 224 | #[test] 225 | pub fn remove_tail() { 226 | let mut left = json!(["hello", "bye", "hi"]); 227 | let right = json!(["hello"]); 228 | let patch = super::diff(&left, &right); 229 | assert_eq!( 230 | patch, 231 | serde_json::from_value(json!([ 232 | { "op": "remove", "path": "/1" }, 233 | { "op": "remove", "path": "/1" }, 234 | ])) 235 | .unwrap() 236 | ); 237 | 238 | crate::patch(&mut left, &patch).unwrap(); 239 | assert_eq!(left, right); 240 | } 241 | 242 | #[test] 243 | pub fn add_tail() { 244 | let mut left = json!(["hello"]); 245 | let right = json!(["hello", "bye", "hi"]); 246 | let patch = super::diff(&left, &right); 247 | assert_eq!( 248 | patch, 249 | serde_json::from_value(json!([ 250 | { "op": "add", "path": "/1", "value": "bye" }, 251 | { "op": "add", "path": "/2", "value": "hi" } 252 | ])) 253 | .unwrap() 254 | ); 255 | 256 | crate::patch(&mut left, &patch).unwrap(); 257 | assert_eq!(left, right); 258 | } 259 | 260 | #[test] 261 | pub fn replace_object() { 262 | let mut left = json!(["hello", "bye"]); 263 | let right = json!({"hello": "bye"}); 264 | let patch = super::diff(&left, &right); 265 | assert_eq!( 266 | patch, 267 | serde_json::from_value(json!([ 268 | { "op": "replace", "path": "", "value": {"hello": "bye"} } 269 | ])) 270 | .unwrap() 271 | ); 272 | 273 | crate::patch(&mut left, &patch).unwrap(); 274 | assert_eq!(left, right); 275 | } 276 | 277 | #[test] 278 | fn escape_json_keys() { 279 | let mut left = json!({ 280 | "/slashed/path/with/~": 1 281 | }); 282 | let right = json!({ 283 | "/slashed/path/with/~": 2, 284 | }); 285 | let patch = super::diff(&left, &right); 286 | 287 | crate::patch(&mut left, &patch).unwrap(); 288 | assert_eq!(left, right); 289 | } 290 | 291 | #[test] 292 | pub fn replace_object_array() { 293 | let mut left = json!({ "style": { "ref": {"name": "name"} } }); 294 | let right = json!({ "style": [{ "ref": {"hello": "hello"} }]}); 295 | let patch = crate::diff(&left, &right); 296 | 297 | assert_eq!( 298 | patch, 299 | serde_json::from_value(json!([ 300 | { "op": "replace", "path": "/style", "value": [{ "ref": {"hello": "hello"} }] }, 301 | ])) 302 | .unwrap() 303 | ); 304 | crate::patch(&mut left, &patch).unwrap(); 305 | assert_eq!(left, right); 306 | } 307 | 308 | #[test] 309 | pub fn replace_array_object() { 310 | let mut left = json!({ "style": [{ "ref": {"hello": "hello"} }]}); 311 | let right = json!({ "style": { "ref": {"name": "name"} } }); 312 | let patch = crate::diff(&left, &right); 313 | 314 | assert_eq!( 315 | patch, 316 | serde_json::from_value(json!([ 317 | { "op": "replace", "path": "/style", "value": { "ref": {"name": "name"} } }, 318 | ])) 319 | .unwrap() 320 | ); 321 | crate::patch(&mut left, &patch).unwrap(); 322 | assert_eq!(left, right); 323 | } 324 | 325 | #[test] 326 | pub fn remove_keys() { 327 | let mut left = json!({"first": 1, "second": 2, "third": 3}); 328 | let right = json!({"first": 1, "second": 2}); 329 | let patch = super::diff(&left, &right); 330 | assert_eq!( 331 | patch, 332 | serde_json::from_value(json!([ 333 | { "op": "remove", "path": "/third" } 334 | ])) 335 | .unwrap() 336 | ); 337 | 338 | crate::patch(&mut left, &patch).unwrap(); 339 | assert_eq!(left, right); 340 | } 341 | 342 | #[test] 343 | pub fn add_keys() { 344 | let mut left = json!({"first": 1, "second": 2}); 345 | let right = json!({"first": 1, "second": 2, "third": 3}); 346 | let patch = super::diff(&left, &right); 347 | assert_eq!( 348 | patch, 349 | serde_json::from_value(json!([ 350 | { "op": "add", "path": "/third", "value": 3 } 351 | ])) 352 | .unwrap() 353 | ); 354 | 355 | crate::patch(&mut left, &patch).unwrap(); 356 | assert_eq!(left, right); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A [JSON Patch (RFC 6902)](https://tools.ietf.org/html/rfc6902) and 2 | //! [JSON Merge Patch (RFC 7396)](https://tools.ietf.org/html/rfc7396) implementation for Rust. 3 | //! 4 | //! # Usage 5 | //! 6 | //! Add this to your *Cargo.toml*: 7 | //! ```toml 8 | //! [dependencies] 9 | //! json-patch = "*" 10 | //! ``` 11 | //! 12 | //! # Examples 13 | //! Create and patch document using JSON Patch: 14 | //! 15 | //! ```rust 16 | //! #[macro_use] 17 | //! use json_patch::{Patch, patch}; 18 | //! use serde_json::{from_value, json}; 19 | //! 20 | //! # pub fn main() { 21 | //! let mut doc = json!([ 22 | //! { "name": "Andrew" }, 23 | //! { "name": "Maxim" } 24 | //! ]); 25 | //! 26 | //! let p: Patch = from_value(json!([ 27 | //! { "op": "test", "path": "/0/name", "value": "Andrew" }, 28 | //! { "op": "add", "path": "/0/happy", "value": true } 29 | //! ])).unwrap(); 30 | //! 31 | //! patch(&mut doc, &p).unwrap(); 32 | //! assert_eq!(doc, json!([ 33 | //! { "name": "Andrew", "happy": true }, 34 | //! { "name": "Maxim" } 35 | //! ])); 36 | //! 37 | //! # } 38 | //! ``` 39 | //! 40 | //! Create and patch document using JSON Merge Patch: 41 | //! 42 | //! ```rust 43 | //! #[macro_use] 44 | //! use json_patch::merge; 45 | //! use serde_json::json; 46 | //! 47 | //! # pub fn main() { 48 | //! let mut doc = json!({ 49 | //! "title": "Goodbye!", 50 | //! "author" : { 51 | //! "givenName" : "John", 52 | //! "familyName" : "Doe" 53 | //! }, 54 | //! "tags":[ "example", "sample" ], 55 | //! "content": "This will be unchanged" 56 | //! }); 57 | //! 58 | //! let patch = json!({ 59 | //! "title": "Hello!", 60 | //! "phoneNumber": "+01-123-456-7890", 61 | //! "author": { 62 | //! "familyName": null 63 | //! }, 64 | //! "tags": [ "example" ] 65 | //! }); 66 | //! 67 | //! merge(&mut doc, &patch); 68 | //! assert_eq!(doc, json!({ 69 | //! "title": "Hello!", 70 | //! "author" : { 71 | //! "givenName" : "John" 72 | //! }, 73 | //! "tags": [ "example" ], 74 | //! "content": "This will be unchanged", 75 | //! "phoneNumber": "+01-123-456-7890" 76 | //! })); 77 | //! # } 78 | //! ``` 79 | #![warn(missing_docs)] 80 | 81 | use jsonptr::{index::Index, Pointer, PointerBuf}; 82 | use serde::{Deserialize, Serialize}; 83 | use serde_json::{Map, Value}; 84 | use std::fmt::{self, Display, Formatter}; 85 | use thiserror::Error; 86 | 87 | // So users can instance `jsonptr::PointerBuf` and others without 88 | // having to explicitly match our `jsonptr` version. 89 | pub use jsonptr; 90 | 91 | #[cfg(feature = "diff")] 92 | mod diff; 93 | 94 | #[cfg(feature = "diff")] 95 | pub use self::diff::diff; 96 | 97 | struct WriteAdapter<'a>(&'a mut dyn fmt::Write); 98 | 99 | impl std::io::Write for WriteAdapter<'_> { 100 | fn write(&mut self, buf: &[u8]) -> Result { 101 | let s = std::str::from_utf8(buf).unwrap(); 102 | self.0 103 | .write_str(s) 104 | .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))?; 105 | Ok(buf.len()) 106 | } 107 | 108 | fn flush(&mut self) -> Result<(), std::io::Error> { 109 | Ok(()) 110 | } 111 | } 112 | 113 | macro_rules! impl_display { 114 | ($name:ident) => { 115 | impl Display for $name { 116 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 117 | let alternate = f.alternate(); 118 | if alternate { 119 | serde_json::to_writer_pretty(WriteAdapter(f), self) 120 | .map_err(|_| std::fmt::Error)?; 121 | } else { 122 | serde_json::to_writer(WriteAdapter(f), self).map_err(|_| std::fmt::Error)?; 123 | } 124 | Ok(()) 125 | } 126 | } 127 | }; 128 | } 129 | 130 | /// Representation of JSON Patch (list of patch operations) 131 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 132 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 133 | #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] 134 | pub struct Patch(pub Vec); 135 | 136 | impl_display!(Patch); 137 | 138 | impl std::ops::Deref for Patch { 139 | type Target = [PatchOperation]; 140 | 141 | fn deref(&self) -> &[PatchOperation] { 142 | &self.0 143 | } 144 | } 145 | 146 | /// JSON Patch 'add' operation representation 147 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 148 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 149 | #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] 150 | pub struct AddOperation { 151 | /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location 152 | /// within the target document where the operation is performed. 153 | #[cfg_attr(feature = "schemars", schemars(schema_with = "String::json_schema"))] 154 | #[cfg_attr(feature = "utoipa", schema(value_type = String))] 155 | pub path: PointerBuf, 156 | /// Value to add to the target location. 157 | pub value: Value, 158 | } 159 | 160 | impl_display!(AddOperation); 161 | 162 | /// JSON Patch 'remove' operation representation 163 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 164 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 165 | #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] 166 | pub struct RemoveOperation { 167 | /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location 168 | /// within the target document where the operation is performed. 169 | #[cfg_attr(feature = "schemars", schemars(schema_with = "String::json_schema"))] 170 | #[cfg_attr(feature = "utoipa", schema(value_type = String))] 171 | pub path: PointerBuf, 172 | } 173 | 174 | impl_display!(RemoveOperation); 175 | 176 | /// JSON Patch 'replace' operation representation 177 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 178 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 179 | #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] 180 | pub struct ReplaceOperation { 181 | /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location 182 | /// within the target document where the operation is performed. 183 | #[cfg_attr(feature = "schemars", schemars(schema_with = "String::json_schema"))] 184 | #[cfg_attr(feature = "utoipa", schema(value_type = String))] 185 | pub path: PointerBuf, 186 | /// Value to replace with. 187 | pub value: Value, 188 | } 189 | 190 | impl_display!(ReplaceOperation); 191 | 192 | /// JSON Patch 'move' operation representation 193 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 194 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 195 | #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] 196 | pub struct MoveOperation { 197 | /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location 198 | /// to move value from. 199 | #[cfg_attr(feature = "schemars", schemars(schema_with = "String::json_schema"))] 200 | #[cfg_attr(feature = "utoipa", schema(value_type = String))] 201 | pub from: PointerBuf, 202 | /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location 203 | /// within the target document where the operation is performed. 204 | #[cfg_attr(feature = "schemars", schemars(schema_with = "String::json_schema"))] 205 | #[cfg_attr(feature = "utoipa", schema(value_type = String))] 206 | pub path: PointerBuf, 207 | } 208 | 209 | impl_display!(MoveOperation); 210 | 211 | /// JSON Patch 'copy' operation representation 212 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 213 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 214 | #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] 215 | pub struct CopyOperation { 216 | /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location 217 | /// to copy value from. 218 | #[cfg_attr(feature = "schemars", schemars(schema_with = "String::json_schema"))] 219 | #[cfg_attr(feature = "utoipa", schema(value_type = String))] 220 | pub from: PointerBuf, 221 | /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location 222 | /// within the target document where the operation is performed. 223 | #[cfg_attr(feature = "schemars", schemars(schema_with = "String::json_schema"))] 224 | #[cfg_attr(feature = "utoipa", schema(value_type = String))] 225 | pub path: PointerBuf, 226 | } 227 | 228 | impl_display!(CopyOperation); 229 | 230 | /// JSON Patch 'test' operation representation 231 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 232 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 233 | #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] 234 | pub struct TestOperation { 235 | /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location 236 | /// within the target document where the operation is performed. 237 | #[cfg_attr(feature = "schemars", schemars(schema_with = "String::json_schema"))] 238 | #[cfg_attr(feature = "utoipa", schema(value_type = String))] 239 | pub path: PointerBuf, 240 | /// Value to test against. 241 | pub value: Value, 242 | } 243 | 244 | impl_display!(TestOperation); 245 | 246 | /// JSON Patch single patch operation 247 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 248 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 249 | #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] 250 | #[serde(tag = "op")] 251 | #[serde(rename_all = "lowercase")] 252 | pub enum PatchOperation { 253 | /// 'add' operation 254 | Add(AddOperation), 255 | /// 'remove' operation 256 | Remove(RemoveOperation), 257 | /// 'replace' operation 258 | Replace(ReplaceOperation), 259 | /// 'move' operation 260 | Move(MoveOperation), 261 | /// 'copy' operation 262 | Copy(CopyOperation), 263 | /// 'test' operation 264 | Test(TestOperation), 265 | } 266 | 267 | impl_display!(PatchOperation); 268 | 269 | impl PatchOperation { 270 | /// Returns a reference to the path the operation applies to. 271 | pub fn path(&self) -> &Pointer { 272 | match self { 273 | Self::Add(op) => &op.path, 274 | Self::Remove(op) => &op.path, 275 | Self::Replace(op) => &op.path, 276 | Self::Move(op) => &op.path, 277 | Self::Copy(op) => &op.path, 278 | Self::Test(op) => &op.path, 279 | } 280 | } 281 | } 282 | 283 | impl Default for PatchOperation { 284 | fn default() -> Self { 285 | PatchOperation::Test(TestOperation::default()) 286 | } 287 | } 288 | 289 | /// This type represents all possible errors that can occur when applying JSON patch 290 | #[derive(Debug, Error)] 291 | #[non_exhaustive] 292 | pub enum PatchErrorKind { 293 | /// `test` operation failed because values did not match. 294 | #[error("value did not match")] 295 | TestFailed, 296 | /// `from` JSON pointer in a `move` or a `copy` operation was incorrect. 297 | #[error("\"from\" path is invalid")] 298 | InvalidFromPointer, 299 | /// `path` JSON pointer is incorrect. 300 | #[error("path is invalid")] 301 | InvalidPointer, 302 | /// `move` operation failed because target is inside the `from` location. 303 | #[error("cannot move the value inside itself")] 304 | CannotMoveInsideItself, 305 | } 306 | 307 | impl From for PatchErrorKind { 308 | fn from(_: jsonptr::index::ParseIndexError) -> Self { 309 | Self::InvalidPointer 310 | } 311 | } 312 | 313 | impl From for PatchErrorKind { 314 | fn from(_: jsonptr::index::OutOfBoundsError) -> Self { 315 | Self::InvalidPointer 316 | } 317 | } 318 | 319 | /// This type represents all possible errors that can occur when applying JSON patch 320 | #[derive(Debug, Error)] 321 | #[error("operation '/{operation}' failed at path '{path}': {kind}")] 322 | #[non_exhaustive] 323 | pub struct PatchError { 324 | /// Index of the operation that has failed. 325 | pub operation: usize, 326 | /// `path` of the operation. 327 | pub path: PointerBuf, 328 | /// Kind of the error. 329 | pub kind: PatchErrorKind, 330 | } 331 | 332 | fn translate_error(kind: PatchErrorKind, operation: usize, path: &Pointer) -> PatchError { 333 | PatchError { 334 | operation, 335 | path: path.to_owned(), 336 | kind, 337 | } 338 | } 339 | 340 | fn add(doc: &mut Value, path: &Pointer, value: Value) -> Result, PatchErrorKind> { 341 | let Some((parent, last)) = path.split_back() else { 342 | // empty path, add is replace the value wholesale 343 | return Ok(Some(std::mem::replace(doc, value))); 344 | }; 345 | 346 | let mut parent = doc 347 | .pointer_mut(parent.as_str()) 348 | .ok_or(PatchErrorKind::InvalidPointer)?; 349 | 350 | match &mut parent { 351 | Value::Object(obj) => Ok(obj.insert(last.decoded().into_owned(), value)), 352 | Value::Array(arr) => { 353 | let idx = last.to_index()?.for_len_incl(arr.len())?; 354 | arr.insert(idx, value); 355 | Ok(None) 356 | } 357 | _ => Err(PatchErrorKind::InvalidPointer), 358 | } 359 | } 360 | 361 | fn remove(doc: &mut Value, path: &Pointer, allow_last: bool) -> Result { 362 | let Some((parent, last)) = path.split_back() else { 363 | return Err(PatchErrorKind::InvalidPointer); 364 | }; 365 | let mut parent = doc 366 | .pointer_mut(parent.as_str()) 367 | .ok_or(PatchErrorKind::InvalidPointer)?; 368 | 369 | match &mut parent { 370 | Value::Object(obj) => match obj.remove(last.decoded().as_ref()) { 371 | None => Err(PatchErrorKind::InvalidPointer), 372 | Some(val) => Ok(val), 373 | }, 374 | // XXX: is this really correct? semantically it seems off, `-` refers to an empty position, not the last element 375 | Value::Array(arr) if allow_last && matches!(last.to_index(), Ok(Index::Next)) => { 376 | Ok(arr.pop().unwrap()) 377 | } 378 | Value::Array(arr) => { 379 | let idx = last.to_index()?.for_len(arr.len())?; 380 | Ok(arr.remove(idx)) 381 | } 382 | _ => Err(PatchErrorKind::InvalidPointer), 383 | } 384 | } 385 | 386 | fn replace(doc: &mut Value, path: &Pointer, value: Value) -> Result { 387 | let target = doc 388 | .pointer_mut(path.as_str()) 389 | .ok_or(PatchErrorKind::InvalidPointer)?; 390 | Ok(std::mem::replace(target, value)) 391 | } 392 | 393 | fn mov( 394 | doc: &mut Value, 395 | from: &Pointer, 396 | path: &Pointer, 397 | allow_last: bool, 398 | ) -> Result, PatchErrorKind> { 399 | // Check we are not moving inside own child 400 | if path.starts_with(from) && path.len() != from.len() { 401 | return Err(PatchErrorKind::CannotMoveInsideItself); 402 | } 403 | let val = remove(doc, from, allow_last).map_err(|err| match err { 404 | PatchErrorKind::InvalidPointer => PatchErrorKind::InvalidFromPointer, 405 | err => err, 406 | })?; 407 | add(doc, path, val) 408 | } 409 | 410 | fn copy(doc: &mut Value, from: &Pointer, path: &Pointer) -> Result, PatchErrorKind> { 411 | let source = doc 412 | .pointer(from.as_str()) 413 | .ok_or(PatchErrorKind::InvalidFromPointer)? 414 | .clone(); 415 | add(doc, path, source) 416 | } 417 | 418 | fn test(doc: &Value, path: &Pointer, expected: &Value) -> Result<(), PatchErrorKind> { 419 | let target = doc 420 | .pointer(path.as_str()) 421 | .ok_or(PatchErrorKind::InvalidPointer)?; 422 | if *target == *expected { 423 | Ok(()) 424 | } else { 425 | Err(PatchErrorKind::TestFailed) 426 | } 427 | } 428 | 429 | /// Patch provided JSON document (given as `serde_json::Value`) in-place. If any of the patch is 430 | /// failed, all previous operations are reverted. In case of internal error resulting in panic, 431 | /// document might be left in inconsistent state. 432 | /// 433 | /// # Example 434 | /// Create and patch document: 435 | /// 436 | /// ```rust 437 | /// #[macro_use] 438 | /// use json_patch::{Patch, patch}; 439 | /// use serde_json::{from_value, json}; 440 | /// 441 | /// # pub fn main() { 442 | /// let mut doc = json!([ 443 | /// { "name": "Andrew" }, 444 | /// { "name": "Maxim" } 445 | /// ]); 446 | /// 447 | /// let p: Patch = from_value(json!([ 448 | /// { "op": "test", "path": "/0/name", "value": "Andrew" }, 449 | /// { "op": "add", "path": "/0/happy", "value": true } 450 | /// ])).unwrap(); 451 | /// 452 | /// patch(&mut doc, &p).unwrap(); 453 | /// assert_eq!(doc, json!([ 454 | /// { "name": "Andrew", "happy": true }, 455 | /// { "name": "Maxim" } 456 | /// ])); 457 | /// 458 | /// # } 459 | /// ``` 460 | pub fn patch(doc: &mut Value, patch: &[PatchOperation]) -> Result<(), PatchError> { 461 | let mut undo_stack = Vec::with_capacity(patch.len()); 462 | if let Err(e) = apply_patches(doc, patch, Some(&mut undo_stack)) { 463 | if let Err(e) = undo_patches(doc, &undo_stack) { 464 | unreachable!("unable to undo applied patches: {e}") 465 | } 466 | return Err(e); 467 | } 468 | Ok(()) 469 | } 470 | 471 | /// Patch provided JSON document (given as `serde_json::Value`) in-place. Different from [`patch`] 472 | /// if any patch failed, the document is left in an inconsistent state. In case of internal error 473 | /// resulting in panic, document might be left in inconsistent state. 474 | /// 475 | /// # Example 476 | /// Create and patch document: 477 | /// 478 | /// ```rust 479 | /// #[macro_use] 480 | /// use json_patch::{Patch, patch_unsafe}; 481 | /// use serde_json::{from_value, json}; 482 | /// 483 | /// # pub fn main() { 484 | /// let mut doc = json!([ 485 | /// { "name": "Andrew" }, 486 | /// { "name": "Maxim" } 487 | /// ]); 488 | /// 489 | /// let p: Patch = from_value(json!([ 490 | /// { "op": "test", "path": "/0/name", "value": "Andrew" }, 491 | /// { "op": "add", "path": "/0/happy", "value": true } 492 | /// ])).unwrap(); 493 | /// 494 | /// patch_unsafe(&mut doc, &p).unwrap(); 495 | /// assert_eq!(doc, json!([ 496 | /// { "name": "Andrew", "happy": true }, 497 | /// { "name": "Maxim" } 498 | /// ])); 499 | /// 500 | /// # } 501 | /// ``` 502 | pub fn patch_unsafe(doc: &mut Value, patch: &[PatchOperation]) -> Result<(), PatchError> { 503 | apply_patches(doc, patch, None) 504 | } 505 | 506 | /// Undoes operations performed by `apply_patches`. This is useful to recover the original document 507 | /// in case of an error. 508 | fn undo_patches(doc: &mut Value, undo_patches: &[PatchOperation]) -> Result<(), PatchError> { 509 | for (operation, patch) in undo_patches.iter().enumerate().rev() { 510 | match patch { 511 | PatchOperation::Add(op) => { 512 | add(doc, &op.path, op.value.clone()) 513 | .map_err(|e| translate_error(e, operation, &op.path))?; 514 | } 515 | PatchOperation::Remove(op) => { 516 | remove(doc, &op.path, true).map_err(|e| translate_error(e, operation, &op.path))?; 517 | } 518 | PatchOperation::Replace(op) => { 519 | replace(doc, &op.path, op.value.clone()) 520 | .map_err(|e| translate_error(e, operation, &op.path))?; 521 | } 522 | PatchOperation::Move(op) => { 523 | mov(doc, &op.from, &op.path, true) 524 | .map_err(|e| translate_error(e, operation, &op.path))?; 525 | } 526 | PatchOperation::Copy(op) => { 527 | copy(doc, &op.from, &op.path) 528 | .map_err(|e| translate_error(e, operation, &op.path))?; 529 | } 530 | _ => unreachable!(), 531 | } 532 | } 533 | 534 | Ok(()) 535 | } 536 | 537 | // Apply patches while tracking all the changes being made so they can be reverted back in case 538 | // subsequent patches fail. The inverse of all state changes is recorded in the `undo_stack` which 539 | // can be reapplied using `undo_patches` to get back to the original document. 540 | fn apply_patches( 541 | doc: &mut Value, 542 | patches: &[PatchOperation], 543 | undo_stack: Option<&mut Vec>, 544 | ) -> Result<(), PatchError> { 545 | for (operation, patch) in patches.iter().enumerate() { 546 | match patch { 547 | PatchOperation::Add(ref op) => { 548 | let prev = add(doc, &op.path, op.value.clone()) 549 | .map_err(|e| translate_error(e, operation, &op.path))?; 550 | if let Some(&mut ref mut undo_stack) = undo_stack { 551 | undo_stack.push(match prev { 552 | None => PatchOperation::Remove(RemoveOperation { 553 | path: op.path.clone(), 554 | }), 555 | Some(v) => PatchOperation::Add(AddOperation { 556 | path: op.path.clone(), 557 | value: v, 558 | }), 559 | }) 560 | } 561 | } 562 | PatchOperation::Remove(ref op) => { 563 | let prev = remove(doc, &op.path, false) 564 | .map_err(|e| translate_error(e, operation, &op.path))?; 565 | if let Some(&mut ref mut undo_stack) = undo_stack { 566 | undo_stack.push(PatchOperation::Add(AddOperation { 567 | path: op.path.clone(), 568 | value: prev, 569 | })) 570 | } 571 | } 572 | PatchOperation::Replace(ref op) => { 573 | let prev = replace(doc, &op.path, op.value.clone()) 574 | .map_err(|e| translate_error(e, operation, &op.path))?; 575 | if let Some(&mut ref mut undo_stack) = undo_stack { 576 | undo_stack.push(PatchOperation::Replace(ReplaceOperation { 577 | path: op.path.clone(), 578 | value: prev, 579 | })) 580 | } 581 | } 582 | PatchOperation::Move(ref op) => { 583 | let prev = mov(doc, &op.from, &op.path, false) 584 | .map_err(|e| translate_error(e, operation, &op.path))?; 585 | if let Some(&mut ref mut undo_stack) = undo_stack { 586 | if let Some(prev) = prev { 587 | undo_stack.push(PatchOperation::Add(AddOperation { 588 | path: op.path.clone(), 589 | value: prev, 590 | })); 591 | } 592 | undo_stack.push(PatchOperation::Move(MoveOperation { 593 | from: op.path.clone(), 594 | path: op.from.clone(), 595 | })); 596 | } 597 | } 598 | PatchOperation::Copy(ref op) => { 599 | let prev = copy(doc, &op.from, &op.path) 600 | .map_err(|e| translate_error(e, operation, &op.path))?; 601 | if let Some(&mut ref mut undo_stack) = undo_stack { 602 | undo_stack.push(match prev { 603 | None => PatchOperation::Remove(RemoveOperation { 604 | path: op.path.clone(), 605 | }), 606 | Some(v) => PatchOperation::Add(AddOperation { 607 | path: op.path.clone(), 608 | value: v, 609 | }), 610 | }) 611 | } 612 | } 613 | PatchOperation::Test(ref op) => { 614 | test(doc, &op.path, &op.value) 615 | .map_err(|e| translate_error(e, operation, &op.path))?; 616 | } 617 | } 618 | } 619 | 620 | Ok(()) 621 | } 622 | 623 | /// Patch provided JSON document (given as `serde_json::Value`) in place with JSON Merge Patch 624 | /// (RFC 7396). 625 | /// 626 | /// # Example 627 | /// Create and patch document: 628 | /// 629 | /// ```rust 630 | /// #[macro_use] 631 | /// use json_patch::merge; 632 | /// use serde_json::json; 633 | /// 634 | /// # pub fn main() { 635 | /// let mut doc = json!({ 636 | /// "title": "Goodbye!", 637 | /// "author" : { 638 | /// "givenName" : "John", 639 | /// "familyName" : "Doe" 640 | /// }, 641 | /// "tags":[ "example", "sample" ], 642 | /// "content": "This will be unchanged" 643 | /// }); 644 | /// 645 | /// let patch = json!({ 646 | /// "title": "Hello!", 647 | /// "phoneNumber": "+01-123-456-7890", 648 | /// "author": { 649 | /// "familyName": null 650 | /// }, 651 | /// "tags": [ "example" ] 652 | /// }); 653 | /// 654 | /// merge(&mut doc, &patch); 655 | /// assert_eq!(doc, json!({ 656 | /// "title": "Hello!", 657 | /// "author" : { 658 | /// "givenName" : "John" 659 | /// }, 660 | /// "tags": [ "example" ], 661 | /// "content": "This will be unchanged", 662 | /// "phoneNumber": "+01-123-456-7890" 663 | /// })); 664 | /// # } 665 | /// ``` 666 | pub fn merge(doc: &mut Value, patch: &Value) { 667 | if !patch.is_object() { 668 | *doc = patch.clone(); 669 | return; 670 | } 671 | 672 | if !doc.is_object() { 673 | *doc = Value::Object(Map::new()); 674 | } 675 | let map = doc.as_object_mut().unwrap(); 676 | for (key, value) in patch.as_object().unwrap() { 677 | if value.is_null() { 678 | map.remove(key.as_str()); 679 | } else { 680 | merge(map.entry(key.as_str()).or_insert(Value::Null), value); 681 | } 682 | } 683 | } 684 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use json_patch::{ 2 | AddOperation, CopyOperation, MoveOperation, Patch, PatchOperation, RemoveOperation, 3 | ReplaceOperation, TestOperation, 4 | }; 5 | use serde_json::{from_str, from_value, json, Value}; 6 | 7 | #[test] 8 | fn parse_from_value() { 9 | let json = json!([{"op": "add", "path": "/a/b", "value": 1}, {"op": "remove", "path": "/c"}]); 10 | let patch: Patch = from_value(json).unwrap(); 11 | 12 | assert_eq!( 13 | patch, 14 | Patch(vec![ 15 | PatchOperation::Add(AddOperation { 16 | path: "/a/b".parse().unwrap(), 17 | value: Value::from(1), 18 | }), 19 | PatchOperation::Remove(RemoveOperation { 20 | path: "/c".parse().unwrap(), 21 | }), 22 | ]) 23 | ); 24 | 25 | let _patch: Patch = 26 | from_str(r#"[{"op": "add", "path": "/a/b", "value": 1}, {"op": "remove", "path": "/c"}]"#) 27 | .unwrap(); 28 | } 29 | 30 | #[test] 31 | fn parse_from_string() { 32 | let patch: Patch = 33 | from_str(r#"[{"op": "add", "path": "/a/b", "value": 1}, {"op": "remove", "path": "/c"}]"#) 34 | .unwrap(); 35 | 36 | assert_eq!( 37 | patch, 38 | Patch(vec![ 39 | PatchOperation::Add(AddOperation { 40 | path: "/a/b".parse().unwrap(), 41 | value: Value::from(1), 42 | }), 43 | PatchOperation::Remove(RemoveOperation { 44 | path: "/c".parse().unwrap(), 45 | }), 46 | ]) 47 | ); 48 | } 49 | 50 | #[test] 51 | fn serialize_patch() { 52 | let s = r#"[{"op":"add","path":"/a/b","value":1},{"op":"remove","path":"/c"}]"#; 53 | let patch: Patch = from_str(s).unwrap(); 54 | 55 | let serialized = serde_json::to_string(&patch).unwrap(); 56 | assert_eq!(serialized, s); 57 | } 58 | 59 | #[test] 60 | fn display_add_operation() { 61 | let op = PatchOperation::Add(AddOperation { 62 | path: "/a/b/c".parse().unwrap(), 63 | value: json!(["hello", "bye"]), 64 | }); 65 | assert_eq!( 66 | op.to_string(), 67 | r#"{"op":"add","path":"/a/b/c","value":["hello","bye"]}"# 68 | ); 69 | assert_eq!( 70 | format!("{:#}", op), 71 | r#"{ 72 | "op": "add", 73 | "path": "/a/b/c", 74 | "value": [ 75 | "hello", 76 | "bye" 77 | ] 78 | }"# 79 | ); 80 | } 81 | 82 | #[test] 83 | fn display_remove_operation() { 84 | let op = PatchOperation::Remove(RemoveOperation { 85 | path: "/a/b/c".parse().unwrap(), 86 | }); 87 | assert_eq!(op.to_string(), r#"{"op":"remove","path":"/a/b/c"}"#); 88 | assert_eq!( 89 | format!("{:#}", op), 90 | r#"{ 91 | "op": "remove", 92 | "path": "/a/b/c" 93 | }"# 94 | ); 95 | } 96 | 97 | #[test] 98 | fn display_replace_operation() { 99 | let op = PatchOperation::Replace(ReplaceOperation { 100 | path: "/a/b/c".parse().unwrap(), 101 | value: json!(42), 102 | }); 103 | assert_eq!( 104 | op.to_string(), 105 | r#"{"op":"replace","path":"/a/b/c","value":42}"# 106 | ); 107 | assert_eq!( 108 | format!("{:#}", op), 109 | r#"{ 110 | "op": "replace", 111 | "path": "/a/b/c", 112 | "value": 42 113 | }"# 114 | ); 115 | } 116 | 117 | #[test] 118 | fn display_move_operation() { 119 | let op = PatchOperation::Move(MoveOperation { 120 | from: "/a/b/c".parse().unwrap(), 121 | path: "/a/b/d".parse().unwrap(), 122 | }); 123 | assert_eq!( 124 | op.to_string(), 125 | r#"{"op":"move","from":"/a/b/c","path":"/a/b/d"}"# 126 | ); 127 | assert_eq!( 128 | format!("{:#}", op), 129 | r#"{ 130 | "op": "move", 131 | "from": "/a/b/c", 132 | "path": "/a/b/d" 133 | }"# 134 | ); 135 | } 136 | 137 | #[test] 138 | fn display_copy_operation() { 139 | let op = PatchOperation::Copy(CopyOperation { 140 | from: "/a/b/d".parse().unwrap(), 141 | path: "/a/b/e".parse().unwrap(), 142 | }); 143 | assert_eq!( 144 | op.to_string(), 145 | r#"{"op":"copy","from":"/a/b/d","path":"/a/b/e"}"# 146 | ); 147 | assert_eq!( 148 | format!("{:#}", op), 149 | r#"{ 150 | "op": "copy", 151 | "from": "/a/b/d", 152 | "path": "/a/b/e" 153 | }"# 154 | ); 155 | } 156 | 157 | #[test] 158 | fn display_test_operation() { 159 | let op = PatchOperation::Test(TestOperation { 160 | path: "/a/b/c".parse().unwrap(), 161 | value: json!("hello"), 162 | }); 163 | assert_eq!( 164 | op.to_string(), 165 | r#"{"op":"test","path":"/a/b/c","value":"hello"}"# 166 | ); 167 | assert_eq!( 168 | format!("{:#}", op), 169 | r#"{ 170 | "op": "test", 171 | "path": "/a/b/c", 172 | "value": "hello" 173 | }"# 174 | ); 175 | } 176 | 177 | #[test] 178 | fn display_patch() { 179 | let patch = Patch(vec![ 180 | PatchOperation::Add(AddOperation { 181 | path: "/a/b/c".parse().unwrap(), 182 | value: json!(["hello", "bye"]), 183 | }), 184 | PatchOperation::Remove(RemoveOperation { 185 | path: "/a/b/c".parse().unwrap(), 186 | }), 187 | ]); 188 | 189 | assert_eq!( 190 | patch.to_string(), 191 | r#"[{"op":"add","path":"/a/b/c","value":["hello","bye"]},{"op":"remove","path":"/a/b/c"}]"# 192 | ); 193 | assert_eq!( 194 | format!("{:#}", patch), 195 | r#"[ 196 | { 197 | "op": "add", 198 | "path": "/a/b/c", 199 | "value": [ 200 | "hello", 201 | "bye" 202 | ] 203 | }, 204 | { 205 | "op": "remove", 206 | "path": "/a/b/c" 207 | } 208 | ]"# 209 | ); 210 | } 211 | 212 | #[test] 213 | fn display_patch_default() { 214 | let patch = Patch::default(); 215 | assert_eq!(patch.to_string(), r#"[]"#); 216 | } 217 | 218 | #[test] 219 | fn display_patch_operation_default() { 220 | let op = PatchOperation::default(); 221 | assert_eq!(op.to_string(), r#"{"op":"test","path":"","value":null}"#); 222 | } 223 | -------------------------------------------------------------------------------- /tests/errors.yaml: -------------------------------------------------------------------------------- 1 | - doc: &1 2 | first: "Hello" 3 | second: "Bye" 4 | third: 5 | - "first" 6 | - "second" 7 | patch: 8 | - op: add 9 | path: "/first" 10 | value: "Hello!!!" 11 | - op: add 12 | path: "/third/00" 13 | value: "value" 14 | error: "operation '/1' failed at path '/third/00': path is invalid" 15 | - doc: *1 16 | patch: 17 | - op: add 18 | path: "/third/01" 19 | value: "value" 20 | error: "operation '/0' failed at path '/third/01': path is invalid" 21 | - doc: *1 22 | patch: 23 | - op: add 24 | path: "/third/1~1" 25 | value: "value" 26 | error: "operation '/0' failed at path '/third/1~1': path is invalid" 27 | - doc: *1 28 | patch: 29 | - op: add 30 | path: "/third/1.0" 31 | value: "value" 32 | error: "operation '/0' failed at path '/third/1.0': path is invalid" 33 | - doc: *1 34 | patch: 35 | - op: add 36 | path: "/third/1e2" 37 | value: "value" 38 | error: "operation '/0' failed at path '/third/1e2': path is invalid" 39 | - doc: *1 40 | patch: 41 | - op: add 42 | path: "/third/+1" 43 | value: "value" 44 | error: "operation '/0' failed at path '/third/+1': path is invalid" 45 | - doc: *1 46 | patch: 47 | - op: copy 48 | from: "/third/1~1" 49 | path: "/fourth" 50 | error: 'operation ''/0'' failed at path ''/fourth'': "from" path is invalid' 51 | - doc: *1 52 | patch: 53 | - op: move 54 | from: "/third/1~1" 55 | path: "/fourth" 56 | error: 'operation ''/0'' failed at path ''/fourth'': "from" path is invalid' 57 | - doc: *1 58 | patch: 59 | - op: move 60 | from: "/third" 61 | path: "/third/0" 62 | error: "operation '/0' failed at path '/third/0': cannot move the value inside itself" 63 | - doc: *1 64 | patch: 65 | - op: add 66 | path: "/invalid/add/path" 67 | value: true 68 | error: "operation '/0' failed at path '/invalid/add/path': path is invalid" 69 | - doc: *1 70 | patch: 71 | - op: remove 72 | path: "/invalid/remove/path" 73 | value: true 74 | error: "operation '/0' failed at path '/invalid/remove/path': path is invalid" 75 | - doc: *1 76 | patch: 77 | - op: replace 78 | path: "/invalid/replace/path" 79 | value: true 80 | error: "operation '/0' failed at path '/invalid/replace/path': path is invalid" 81 | - doc: *1 82 | patch: 83 | - op: test 84 | path: "/invalid/test/path" 85 | value: true 86 | error: "operation '/0' failed at path '/invalid/test/path': path is invalid" 87 | - doc: *1 88 | patch: 89 | - op: add 90 | path: "first" 91 | value: true 92 | error: "json pointer failed to parse; does not start with a slash ('/') and is not empty" 93 | - doc: *1 94 | patch: 95 | - op: replace 96 | path: "first" 97 | value: true 98 | error: "json pointer failed to parse; does not start with a slash ('/') and is not empty" 99 | - doc: *1 100 | patch: 101 | - op: remove 102 | path: "first" 103 | value: true 104 | error: "json pointer failed to parse; does not start with a slash ('/') and is not empty" 105 | - doc: *1 106 | patch: 107 | - op: add 108 | path: "/first/add_to_primitive" 109 | value: true 110 | error: "operation '/0' failed at path '/first/add_to_primitive': path is invalid" 111 | - doc: *1 112 | patch: 113 | - op: remove 114 | path: "/remove_non_existent" 115 | error: "operation '/0' failed at path '/remove_non_existent': path is invalid" 116 | - doc: *1 117 | patch: 118 | - op: remove 119 | path: "/first/remove_from_primitive" 120 | error: "operation '/0' failed at path '/first/remove_from_primitive': path is invalid" 121 | - doc: *1 122 | patch: 123 | - op: test 124 | path: "/first" 125 | value: "Other" 126 | error: "operation '/0' failed at path '/first': value did not match" 127 | -------------------------------------------------------------------------------- /tests/schemars.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "PatchOperation", 4 | "description": "JSON Patch single patch operation", 5 | "oneOf": [ 6 | { 7 | "description": "'add' operation", 8 | "type": "object", 9 | "required": [ 10 | "op", 11 | "path", 12 | "value" 13 | ], 14 | "properties": { 15 | "op": { 16 | "type": "string", 17 | "enum": [ 18 | "add" 19 | ] 20 | }, 21 | "path": { 22 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location within the target document where the operation is performed.", 23 | "type": "string" 24 | }, 25 | "value": { 26 | "description": "Value to add to the target location." 27 | } 28 | } 29 | }, 30 | { 31 | "description": "'remove' operation", 32 | "type": "object", 33 | "required": [ 34 | "op", 35 | "path" 36 | ], 37 | "properties": { 38 | "op": { 39 | "type": "string", 40 | "enum": [ 41 | "remove" 42 | ] 43 | }, 44 | "path": { 45 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location within the target document where the operation is performed.", 46 | "type": "string" 47 | } 48 | } 49 | }, 50 | { 51 | "description": "'replace' operation", 52 | "type": "object", 53 | "required": [ 54 | "op", 55 | "path", 56 | "value" 57 | ], 58 | "properties": { 59 | "op": { 60 | "type": "string", 61 | "enum": [ 62 | "replace" 63 | ] 64 | }, 65 | "path": { 66 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location within the target document where the operation is performed.", 67 | "type": "string" 68 | }, 69 | "value": { 70 | "description": "Value to replace with." 71 | } 72 | } 73 | }, 74 | { 75 | "description": "'move' operation", 76 | "type": "object", 77 | "required": [ 78 | "from", 79 | "op", 80 | "path" 81 | ], 82 | "properties": { 83 | "from": { 84 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location to move value from.", 85 | "type": "string" 86 | }, 87 | "op": { 88 | "type": "string", 89 | "enum": [ 90 | "move" 91 | ] 92 | }, 93 | "path": { 94 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location within the target document where the operation is performed.", 95 | "type": "string" 96 | } 97 | } 98 | }, 99 | { 100 | "description": "'copy' operation", 101 | "type": "object", 102 | "required": [ 103 | "from", 104 | "op", 105 | "path" 106 | ], 107 | "properties": { 108 | "from": { 109 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location to copy value from.", 110 | "type": "string" 111 | }, 112 | "op": { 113 | "type": "string", 114 | "enum": [ 115 | "copy" 116 | ] 117 | }, 118 | "path": { 119 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location within the target document where the operation is performed.", 120 | "type": "string" 121 | } 122 | } 123 | }, 124 | { 125 | "description": "'test' operation", 126 | "type": "object", 127 | "required": [ 128 | "op", 129 | "path", 130 | "value" 131 | ], 132 | "properties": { 133 | "op": { 134 | "type": "string", 135 | "enum": [ 136 | "test" 137 | ] 138 | }, 139 | "path": { 140 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location within the target document where the operation is performed.", 141 | "type": "string" 142 | }, 143 | "value": { 144 | "description": "Value to test against." 145 | } 146 | } 147 | } 148 | ] 149 | } -------------------------------------------------------------------------------- /tests/schemars.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "schemars")] 2 | #[test] 3 | fn schema() { 4 | use json_patch::*; 5 | 6 | let schema = schemars::schema_for!(PatchOperation); 7 | let json = serde_json::to_string_pretty(&schema).unwrap(); 8 | expectorate::assert_contents("tests/schemars.json", &json); 9 | } 10 | -------------------------------------------------------------------------------- /tests/suite.rs: -------------------------------------------------------------------------------- 1 | use json_patch::Patch; 2 | use serde::Deserialize; 3 | use serde_json::Value; 4 | 5 | #[test] 6 | fn errors() { 7 | run_specs("tests/errors.yaml", Errors::ExactMatch, PatchKind::Patch); 8 | } 9 | 10 | #[test] 11 | fn tests() { 12 | run_specs("specs/tests.json", Errors::IgnoreContent, PatchKind::Patch); 13 | } 14 | 15 | #[test] 16 | fn spec_tests() { 17 | run_specs( 18 | "specs/spec_tests.json", 19 | Errors::IgnoreContent, 20 | PatchKind::Patch, 21 | ); 22 | } 23 | 24 | #[test] 25 | fn revert_tests() { 26 | run_specs( 27 | "specs/revert_tests.json", 28 | Errors::IgnoreContent, 29 | PatchKind::Patch, 30 | ); 31 | } 32 | 33 | #[test] 34 | fn merge_tests() { 35 | run_specs( 36 | "specs/merge_tests.json", 37 | Errors::IgnoreContent, 38 | PatchKind::MergePatch, 39 | ); 40 | } 41 | 42 | #[derive(PartialEq, Eq, Clone, Copy)] 43 | enum Errors { 44 | ExactMatch, 45 | IgnoreContent, 46 | } 47 | 48 | #[derive(PartialEq, Eq, Clone, Copy)] 49 | enum PatchKind { 50 | Patch, 51 | MergePatch, 52 | } 53 | 54 | #[derive(Debug, Deserialize)] 55 | struct PatchTestCase { 56 | comment: Option, 57 | doc: Value, 58 | patch: Value, 59 | expected: Option, 60 | error: Option, 61 | #[serde(default)] 62 | disabled: bool, 63 | } 64 | 65 | fn run_patch_test_case(tc: &PatchTestCase, kind: PatchKind) -> Result { 66 | let mut actual = tc.doc.clone(); 67 | if kind == PatchKind::MergePatch { 68 | json_patch::merge(&mut actual, &tc.patch); 69 | return Ok(actual); 70 | } 71 | 72 | // Patch and verify that in case of error document wasn't changed 73 | let patch: Patch = serde_json::from_value(tc.patch.clone()).map_err(|err| err.to_string())?; 74 | json_patch::patch(&mut actual, &patch) 75 | .inspect_err(|_| { 76 | assert_eq!( 77 | tc.doc, actual, 78 | "no changes should be made to the original document" 79 | ); 80 | }) 81 | .map_err(|err| err.to_string())?; 82 | Ok(actual) 83 | } 84 | 85 | fn run_specs(path: &str, errors: Errors, kind: PatchKind) { 86 | let cases = std::fs::read_to_string(path).unwrap(); 87 | let is_yaml = path.ends_with(".yaml") || path.ends_with(".yml"); 88 | let cases: Vec = if is_yaml { 89 | serde_yaml::from_str(&cases).unwrap() 90 | } else { 91 | serde_json::from_str(&cases).unwrap() 92 | }; 93 | 94 | for (idx, tc) in cases.into_iter().enumerate() { 95 | if tc.disabled { 96 | continue; 97 | } 98 | match run_patch_test_case(&tc, kind) { 99 | Ok(actual) => { 100 | if let Some(error) = tc.error { 101 | panic!( 102 | "expected to fail with an error: {}, got document {:?}", 103 | error, actual 104 | ); 105 | } else { 106 | let comment = tc.comment.as_deref().unwrap_or(""); 107 | let expected = if let Some(ref expected) = tc.expected { 108 | expected 109 | } else { 110 | &tc.doc 111 | }; 112 | assert_eq!( 113 | *expected, actual, 114 | "\nActual does not match expected in test case {}: {}", 115 | idx, comment 116 | ); 117 | } 118 | } 119 | Err(actual_error) => { 120 | if let Some(expected_error) = tc.error { 121 | if errors == Errors::ExactMatch { 122 | assert_eq!(actual_error, expected_error, "Expected test case {} to fail with an error:\n{}\n\nbut instead failed with an error:\n{}", idx, expected_error, actual_error); 123 | } 124 | } else { 125 | panic!( 126 | "Patch expected to succeed, but failed with an error:\n{}", 127 | actual_error 128 | ); 129 | } 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/utoipa.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "json-patch", 5 | "description": "RFC 6902, JavaScript Object Notation (JSON) Patch", 6 | "contact": { 7 | "name": "Ivan Dubrov", 8 | "email": "dubrov.ivan@gmail.com" 9 | }, 10 | "license": { 11 | "name": "MIT/Apache-2.0" 12 | }, 13 | "version": "0.0.0" 14 | }, 15 | "paths": { 16 | "foo": { 17 | "get": { 18 | "tags": [ 19 | "crate" 20 | ], 21 | "operationId": "get_foo", 22 | "requestBody": { 23 | "content": { 24 | "application/json": { 25 | "schema": { 26 | "$ref": "#/components/schemas/Patch" 27 | } 28 | } 29 | }, 30 | "required": true 31 | }, 32 | "responses": { 33 | "200": { 34 | "description": "Patch completed" 35 | }, 36 | "406": { 37 | "description": "Not accepted" 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | "components": { 44 | "schemas": { 45 | "AddOperation": { 46 | "type": "object", 47 | "description": "JSON Patch 'add' operation representation", 48 | "required": [ 49 | "path", 50 | "value" 51 | ], 52 | "properties": { 53 | "path": { 54 | "type": "string", 55 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed." 56 | }, 57 | "value": { 58 | "description": "Value to add to the target location." 59 | } 60 | } 61 | }, 62 | "CopyOperation": { 63 | "type": "object", 64 | "description": "JSON Patch 'copy' operation representation", 65 | "required": [ 66 | "from", 67 | "path" 68 | ], 69 | "properties": { 70 | "from": { 71 | "type": "string", 72 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nto copy value from." 73 | }, 74 | "path": { 75 | "type": "string", 76 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed." 77 | } 78 | } 79 | }, 80 | "MoveOperation": { 81 | "type": "object", 82 | "description": "JSON Patch 'move' operation representation", 83 | "required": [ 84 | "from", 85 | "path" 86 | ], 87 | "properties": { 88 | "from": { 89 | "type": "string", 90 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nto move value from." 91 | }, 92 | "path": { 93 | "type": "string", 94 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed." 95 | } 96 | } 97 | }, 98 | "Patch": { 99 | "type": "array", 100 | "items": { 101 | "$ref": "#/components/schemas/PatchOperation" 102 | }, 103 | "description": "Representation of JSON Patch (list of patch operations)" 104 | }, 105 | "PatchOperation": { 106 | "oneOf": [ 107 | { 108 | "allOf": [ 109 | { 110 | "$ref": "#/components/schemas/AddOperation" 111 | }, 112 | { 113 | "type": "object", 114 | "required": [ 115 | "op" 116 | ], 117 | "properties": { 118 | "op": { 119 | "type": "string", 120 | "enum": [ 121 | "add" 122 | ] 123 | } 124 | } 125 | } 126 | ] 127 | }, 128 | { 129 | "allOf": [ 130 | { 131 | "$ref": "#/components/schemas/RemoveOperation" 132 | }, 133 | { 134 | "type": "object", 135 | "required": [ 136 | "op" 137 | ], 138 | "properties": { 139 | "op": { 140 | "type": "string", 141 | "enum": [ 142 | "remove" 143 | ] 144 | } 145 | } 146 | } 147 | ] 148 | }, 149 | { 150 | "allOf": [ 151 | { 152 | "$ref": "#/components/schemas/ReplaceOperation" 153 | }, 154 | { 155 | "type": "object", 156 | "required": [ 157 | "op" 158 | ], 159 | "properties": { 160 | "op": { 161 | "type": "string", 162 | "enum": [ 163 | "replace" 164 | ] 165 | } 166 | } 167 | } 168 | ] 169 | }, 170 | { 171 | "allOf": [ 172 | { 173 | "$ref": "#/components/schemas/MoveOperation" 174 | }, 175 | { 176 | "type": "object", 177 | "required": [ 178 | "op" 179 | ], 180 | "properties": { 181 | "op": { 182 | "type": "string", 183 | "enum": [ 184 | "move" 185 | ] 186 | } 187 | } 188 | } 189 | ] 190 | }, 191 | { 192 | "allOf": [ 193 | { 194 | "$ref": "#/components/schemas/CopyOperation" 195 | }, 196 | { 197 | "type": "object", 198 | "required": [ 199 | "op" 200 | ], 201 | "properties": { 202 | "op": { 203 | "type": "string", 204 | "enum": [ 205 | "copy" 206 | ] 207 | } 208 | } 209 | } 210 | ] 211 | }, 212 | { 213 | "allOf": [ 214 | { 215 | "$ref": "#/components/schemas/TestOperation" 216 | }, 217 | { 218 | "type": "object", 219 | "required": [ 220 | "op" 221 | ], 222 | "properties": { 223 | "op": { 224 | "type": "string", 225 | "enum": [ 226 | "test" 227 | ] 228 | } 229 | } 230 | } 231 | ] 232 | } 233 | ], 234 | "description": "JSON Patch single patch operation", 235 | "discriminator": { 236 | "propertyName": "op" 237 | } 238 | }, 239 | "RemoveOperation": { 240 | "type": "object", 241 | "description": "JSON Patch 'remove' operation representation", 242 | "required": [ 243 | "path" 244 | ], 245 | "properties": { 246 | "path": { 247 | "type": "string", 248 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed." 249 | } 250 | } 251 | }, 252 | "ReplaceOperation": { 253 | "type": "object", 254 | "description": "JSON Patch 'replace' operation representation", 255 | "required": [ 256 | "path", 257 | "value" 258 | ], 259 | "properties": { 260 | "path": { 261 | "type": "string", 262 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed." 263 | }, 264 | "value": { 265 | "description": "Value to replace with." 266 | } 267 | } 268 | }, 269 | "TestOperation": { 270 | "type": "object", 271 | "description": "JSON Patch 'test' operation representation", 272 | "required": [ 273 | "path", 274 | "value" 275 | ], 276 | "properties": { 277 | "path": { 278 | "type": "string", 279 | "description": "JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed." 280 | }, 281 | "value": { 282 | "description": "Value to test against." 283 | } 284 | } 285 | } 286 | } 287 | } 288 | } -------------------------------------------------------------------------------- /tests/utoipa.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "utoipa")] 2 | #[test] 3 | fn schema() { 4 | use json_patch::*; 5 | use utoipa::OpenApi; 6 | 7 | #[utoipa::path( 8 | get, 9 | path = "foo", 10 | request_body = Patch, 11 | responses( 12 | (status = 200, description = "Patch completed"), 13 | (status = 406, description = "Not accepted"), 14 | ), 15 | )] 16 | #[allow(unused)] 17 | fn get_foo(body: Patch) {} 18 | 19 | #[derive(OpenApi, Default)] 20 | #[openapi( 21 | paths(get_foo), 22 | components(schemas( 23 | AddOperation, 24 | CopyOperation, 25 | MoveOperation, 26 | PatchOperation, 27 | RemoveOperation, 28 | ReplaceOperation, 29 | TestOperation, 30 | Patch, 31 | )) 32 | )] 33 | struct ApiDoc; 34 | 35 | let mut doc = ApiDoc::openapi(); 36 | 37 | doc.info.version = "0.0.0".to_string(); 38 | let json = doc.to_pretty_json().unwrap(); 39 | expectorate::assert_contents("tests/utoipa.json", &json); 40 | } 41 | -------------------------------------------------------------------------------- /update-readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cargo readme > ./README.md --------------------------------------------------------------------------------