├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── diffus-derive-test ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── diffus-derive ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── diffus ├── .gitignore ├── Cargo.toml └── src │ ├── diffable_impls │ ├── borrow.rs │ ├── collection.rs │ ├── map.rs │ ├── mod.rs │ ├── option.rs │ ├── primitives.rs │ ├── set.rs │ └── string.rs │ ├── edit │ ├── collection.rs │ ├── enm.rs │ ├── map.rs │ ├── mod.rs │ ├── set.rs │ └── string.rs │ ├── lcs.rs │ ├── lib.rs │ ├── same.rs │ └── twodvec.rs ├── publish.sh └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: cargo 2 | dist: focal 3 | language: rust 4 | rust: 5 | - beta 6 | - stable 7 | - 1.37.0 8 | 9 | before_install: rustup component add rustfmt 10 | script: ./test.sh 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | # 0.10.0 4 | 5 | Update to snake_case 0.3 6 | 7 | ## Internal 8 | 9 | - Added Travis CI. MSRV is 1.37.0 (#60, @tommilligan) 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "diffus", 4 | "diffus-derive", 5 | "diffus-derive-test", 6 | ] 7 | 8 | default-members = [ "diffus", "diffus-derive", "diffus-derive-test" ] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diffus 2 | Finds the difference between two instances of any data structure. 3 | 4 | ## Diffus in action 5 | ```rust 6 | use diffus_derive::Diffus; 7 | use diffus::{edit, Diffable}; 8 | 9 | #[derive(Diffus)] 10 | struct Point { 11 | x: i32, 12 | y: i32, 13 | } 14 | 15 | fn main() { 16 | let left_point = Point { x: 1, y: 2 }; 17 | let right_point = Point { x: 1, y: 3 }; 18 | 19 | let diff = left_point.diff(&right_point); 20 | 21 | match diff { 22 | edit::Edit::Copy(_) => println!("point: no difference"), 23 | edit::Edit::Change(EditedPoint { x, y }) => { 24 | match x { 25 | edit::Edit::Copy(_) => println!("x: no difference"), 26 | edit::Edit::Change((left_x, right_x)) => println!("x: {} => {}", left_x, right_x), 27 | } 28 | match y { 29 | edit::Edit::Copy(_) => println!("y: no difference"), 30 | edit::Edit::Change((left_y, right_y)) => println!("y: {} => {}", left_y, right_y), 31 | } 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ### Diffus on maps 38 | Difference between maps is done through its keys. The identity of an element comes from its associated key, and it otherwise works in the same way as for collections with `Same`. 39 | 40 | The possible edits of a map are `Copy`, `Insert`, `Remove`, `Change` 41 | ```rust 42 | use diffus::{edit, Diffable}; 43 | 44 | fn main() { 45 | let unity: std::collections::HashMap<_, _> = 46 | [(1, 1), (2, 2), (3, 3)].iter().cloned().collect(); 47 | let not_unity: std::collections::HashMap<_, _> = 48 | [(1, 1), (2, 3), (4, 4)].iter().cloned().collect(); 49 | 50 | if let edit::Edit::Change(diff) = unity.diff(¬_unity) { 51 | assert!(diff[&1].is_copy()); 52 | assert_eq!(diff[&2].change().unwrap(), &(&2, &3)); 53 | assert!(diff[&3].is_remove()); 54 | assert_eq!(diff[&4].insert().unwrap(), &4); 55 | } else { 56 | unreachable!() 57 | } 58 | } 59 | ``` 60 | 61 | ### Diffus on collections 62 | Difference between collections is done through the Longest Common Subsequence (LCS) algorithm and with additional support for objects that have changed values but kept its `Same` "identity". 63 | 64 | Strings are considered as collections. 65 | 66 | The possible edits of a collection are `Copy`, `Insert`, `Remove`, `Change`. 67 | 68 | ```rust 69 | use diffus_derive::Diffus; 70 | use diffus::{edit::{self, collection}, Same, Diffable}; 71 | 72 | #[derive(Diffus, Debug)] 73 | struct Identified { 74 | id: u32, 75 | value: u32, 76 | } 77 | 78 | impl Same for Identified { 79 | fn same(&self, other: &Self) -> bool { 80 | self.id == other.id 81 | } 82 | } 83 | 84 | fn main() { 85 | let left = vec![ 86 | Identified { id: 1, value: 0 }, 87 | Identified { id: 2, value: 0 }, 88 | Identified { id: 3, value: 0 }, 89 | Identified { id: 4, value: 0 }, 90 | Identified { id: 5, value: 0 }, 91 | Identified { id: 6, value: 0 }, 92 | Identified { id: 7, value: 0 }, 93 | ]; 94 | let right = vec![ 95 | Identified { id: 1, value: 0 }, 96 | Identified { id: 2, value: 1 }, 97 | Identified { id: 4, value: 0 }, 98 | Identified { id: 3, value: 0 }, 99 | Identified { id: 5, value: 0 }, 100 | Identified { id: 6, value: 0 }, 101 | ]; 102 | 103 | let diff = left.diff(&right); 104 | 105 | match diff { 106 | edit::Edit::Copy(_) => println!("no difference"), 107 | edit::Edit::Change(diff) => { 108 | diff.into_iter().map(|edit| { 109 | match edit { 110 | collection::Edit::Copy(elem) => println!("copy: {:?}", elem), 111 | collection::Edit::Insert(elem) => println!("insert: {:?}", elem), 112 | collection::Edit::Remove(elem) => println!("remove: {:?}", elem), 113 | collection::Edit::Change(EditedIdentified { id, value}) => { 114 | println!("changed:"); 115 | match id { 116 | edit::Edit::Copy(_) => println!(" copy: id"), 117 | edit::Edit::Change((left_id, right_id)) => println!(" id: {} => {}", left_id, right_id), 118 | } 119 | match value { 120 | edit::Edit::Copy(_) => println!(" copy: value"), 121 | edit::Edit::Change((left_value, right_value)) => println!(" value: {} => {}", left_value, right_value), 122 | } 123 | } 124 | }; 125 | }).collect::>(); 126 | }, 127 | }; 128 | } 129 | ``` 130 | 131 | 132 | ### Diffus on enums 133 | Difference between two enums works as expected, it separates variant changes from associated variant field changes. 134 | 135 | Possible edits of an enum are `Copy`, `VariantChanged`, `AssociatedChanged`. 136 | 137 | ```rust 138 | use diffus_derive::Diffus; 139 | use diffus::{edit, Diffable}; 140 | 141 | #[derive(Diffus, Debug, PartialEq)] 142 | enum Test { 143 | A, 144 | B(String), 145 | Bd(String, u32), 146 | C { x: u32 }, 147 | Cd { x: u32, y: String }, 148 | } 149 | 150 | fn main() { 151 | let left = Test::Cd { 152 | x: 42, 153 | y: "Bilbo Baggins".to_owned(), 154 | }; 155 | let right = Test::Cd { 156 | x: 42, 157 | y: "Frodo Baggins".to_owned(), 158 | }; 159 | if let edit::Edit::Change(edit::enm::Edit::AssociatedChanged( 160 | EditedTest::Cd { x, y }, 161 | )) = left.diff(&right) 162 | { 163 | assert!(x.is_copy()); 164 | assert!(y.is_change()); 165 | } else { 166 | unreachable!() 167 | } 168 | 169 | let left = Test::Cd { 170 | x: 42, 171 | y: "Bilbo Baggins".to_owned(), 172 | }; 173 | let right = Test::B("Frodo Baggins".to_owned()); 174 | if let edit::Edit::Change(edit::enm::Edit::VariantChanged(l, r)) = 175 | left.diff(&right) 176 | { 177 | assert_eq!(&left, l); 178 | assert_eq!(&right, r); 179 | } else { 180 | unreachable!() 181 | } 182 | } 183 | ``` 184 | 185 | 186 | ### Custom difference with diffus 187 | Differences can easily be specialized to suit your needs. 188 | 189 | ```rust 190 | use diffus::{edit, Diffable}; 191 | 192 | struct Secret(String); 193 | 194 | impl<'a> Diffable<'a> for Secret { 195 | type Diff = (); 196 | 197 | fn diff(&'a self, other: &'a Self) -> edit::Edit { 198 | if self.0 == other.0 { 199 | edit::Edit::Copy 200 | } else { 201 | edit::Edit::Change(()) 202 | } 203 | } 204 | } 205 | 206 | fn main() { 207 | assert_eq!( 208 | Secret("Something".to_owned()).diff(&Secret("Else".to_owned())) 209 | .change().unwrap(), 210 | &() 211 | ); 212 | } 213 | ``` 214 | -------------------------------------------------------------------------------- /diffus-derive-test/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /diffus-derive-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diffus-derive-test" 3 | version = "0.10.0" 4 | authors = [ 5 | "Jim Holmström ", 6 | "Johan Gardell <736172+gardell@users.noreply.github.com>", 7 | ] 8 | 9 | description = "Tests to verify that derive(diffus) works as expected" 10 | 11 | homepage = "https://github.com/distil/diffus" 12 | 13 | publish = false 14 | 15 | license = "Apache-2.0" 16 | 17 | edition = "2018" 18 | 19 | 20 | [dependencies] 21 | diffus = { path = "../diffus", features = [ "derive" ]} 22 | 23 | serde = { version = "1.0", features = [ "derive" ], optional = true } 24 | serde_json = { version = "1.0", optional = true } 25 | 26 | [features] 27 | default = [] 28 | 29 | serialize-impl = [ "diffus/serialize-impl", "serde", "serde_json" ] 30 | -------------------------------------------------------------------------------- /diffus-derive-test/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[allow(dead_code)] 3 | mod test { 4 | use diffus::{self, edit, Diffable, Diffus, Same}; 5 | 6 | mod hide { 7 | use super::*; 8 | 9 | #[derive(Diffus)] 10 | pub struct Inside { 11 | pub p: u32, 12 | } 13 | 14 | impl Inside { 15 | pub fn new(p: u32) -> Self { 16 | Inside { p } 17 | } 18 | } 19 | } 20 | 21 | #[test] 22 | fn vis_check() { 23 | if let edit::Edit::Change(hide::EditedInside { 24 | p: edit::Edit::Change(diff), 25 | .. 26 | }) = hide::Inside::new(0).diff(&hide::Inside::new(1)) 27 | { 28 | assert_eq!(diff, (&0, &1)); 29 | } else { 30 | unreachable!() 31 | } 32 | } 33 | 34 | #[derive(Diffus)] 35 | struct Lifetime<'a>(&'a u32); 36 | 37 | #[derive(Diffus, Debug, PartialEq)] 38 | struct Identified { 39 | id: u32, 40 | value: u32, 41 | } 42 | 43 | impl Same for Identified { 44 | fn same(&self, other: &Self) -> bool { 45 | self.id == other.id 46 | } 47 | } 48 | 49 | #[test] 50 | fn non_trivial_same_collection() { 51 | let left = vec![ 52 | Identified { id: 1, value: 0 }, 53 | Identified { id: 2, value: 0 }, 54 | Identified { id: 3, value: 0 }, 55 | Identified { id: 4, value: 0 }, 56 | Identified { id: 5, value: 0 }, 57 | Identified { id: 6, value: 0 }, 58 | Identified { id: 7, value: 0 }, 59 | ]; 60 | let right = vec![ 61 | Identified { id: 1, value: 0 }, 62 | Identified { id: 2, value: 1 }, 63 | Identified { id: 4, value: 0 }, 64 | Identified { id: 3, value: 0 }, 65 | Identified { id: 5, value: 0 }, 66 | Identified { id: 6, value: 0 }, 67 | ]; 68 | 69 | let diff = left.diff(&right); 70 | 71 | use edit::{self, collection}; 72 | 73 | if let edit::Edit::Change(diff) = diff { 74 | let diff = diff.into_iter().collect::>(); 75 | 76 | if let ( 77 | &collection::Edit::Change(EditedIdentified { 78 | id: edit::Edit::Copy(&2), 79 | value: edit::Edit::Change((&0, &1)), 80 | }), 81 | &collection::Edit::Remove(&Identified { id: 3, value: 0 }), 82 | &collection::Edit::Copy(&Identified { id: 4, value: 0 }), 83 | &collection::Edit::Insert(&Identified { id: 3, value: 0 }), 84 | ) = (&diff[1], &diff[2], &diff[3], &diff[4]) 85 | { 86 | } else { 87 | unreachable!() 88 | } 89 | } else { 90 | unreachable!() 91 | } 92 | } 93 | 94 | #[test] 95 | fn changed_contents() { 96 | let left = vec![Identified { id: 1, value: 0 }]; 97 | let right = vec![Identified { id: 1, value: 1 }]; 98 | 99 | let diff = left.diff(&right); 100 | 101 | use edit::{self, collection}; 102 | 103 | if let edit::Edit::Change(diff) = diff { 104 | let diff = diff.into_iter().collect::>(); 105 | 106 | assert_eq!(diff.len(), 1); 107 | 108 | if let &collection::Edit::Change(EditedIdentified { 109 | id: edit::Edit::Copy(&1), 110 | value: edit::Edit::Change((&0, &1)), 111 | }) = &diff[0] 112 | { 113 | } else { 114 | unreachable!() 115 | } 116 | } else { 117 | unreachable!() 118 | } 119 | } 120 | 121 | #[derive(Diffus)] 122 | enum NestedTest { 123 | T { test: Test }, 124 | } 125 | 126 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 127 | #[derive(Debug, Diffus, PartialEq, Eq)] 128 | enum Test { 129 | A, 130 | B(String), 131 | Bd(String, u32), 132 | C { x: u32 }, 133 | Cd { x: u32, y: String }, 134 | } 135 | 136 | /* 137 | * Verify enum refering to own type via hashmap 138 | */ 139 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 140 | #[derive(Debug, Diffus, PartialEq)] 141 | enum RecursiveHashMap { 142 | Node(std::collections::HashMap), 143 | Empty, 144 | } 145 | 146 | /* 147 | * Verify enum refering to own type via box 148 | */ 149 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 150 | #[derive(Debug, Diffus, PartialEq)] 151 | enum RecursiveBox { 152 | Boxed(Box), 153 | } 154 | 155 | /* 156 | * Verify enums with only Unit variants. 157 | */ 158 | #[derive(Diffus)] 159 | enum EnumNoLifetimeParameter { 160 | A, 161 | B, 162 | } 163 | 164 | mod visibility_test { 165 | /* 166 | * Verify that the visibility of the Edited version is inherited. 167 | */ 168 | use diffus::Diffus; 169 | 170 | #[derive(Diffus)] 171 | pub struct VisTestStructUnit; 172 | 173 | #[derive(Diffus)] 174 | pub struct VisTestStructTuple(u32); 175 | 176 | #[derive(Diffus)] 177 | pub struct VisTestStruct { 178 | x: u32, 179 | } 180 | 181 | #[derive(Diffus)] 182 | pub enum VisTestEnum { 183 | A, 184 | B(u32), 185 | C { x: u32 }, 186 | } 187 | } 188 | 189 | #[test] 190 | fn enm_nested_test() { 191 | let left = NestedTest::T { 192 | test: Test::C { x: 32 }, 193 | }; 194 | let right = NestedTest::T { 195 | test: Test::C { x: 43 }, 196 | }; 197 | 198 | let diff = left.diff(&right); 199 | 200 | if let edit::enm::Edit::AssociatedChanged(EditedNestedTest::T { test }) = 201 | diff.change().unwrap() 202 | { 203 | if let edit::enm::Edit::AssociatedChanged(EditedTest::C { x }) = test.change().unwrap() 204 | { 205 | assert_eq!(x.change(), Some(&(&32, &43))); 206 | } else { 207 | unreachable!(); 208 | } 209 | } else { 210 | unreachable!(); 211 | } 212 | } 213 | 214 | #[test] 215 | fn enm_associated_not_change_tuple_variant() { 216 | let left = Test::Bd("Bilbo Baggins".to_owned(), 42); 217 | let right = Test::Bd("Bilbo Baggins".to_owned(), 42); 218 | 219 | assert!(left.diff(&right).is_copy()); 220 | } 221 | 222 | #[test] 223 | fn enm_associated_not_change() { 224 | let left = Test::Cd { 225 | x: 42, 226 | y: "Bilbo Baggins".to_owned(), 227 | }; 228 | let right = Test::Cd { 229 | x: 42, 230 | y: "Bilbo Baggins".to_owned(), 231 | }; 232 | 233 | assert!(left.diff(&right).is_copy()); 234 | } 235 | 236 | #[test] 237 | fn enm_associated_change() { 238 | let left = Test::Cd { 239 | x: 42, 240 | y: "Bilbo Baggins".to_owned(), 241 | }; 242 | let right = Test::Cd { 243 | x: 42, 244 | y: "Frodo Baggins".to_owned(), 245 | }; 246 | if let edit::Edit::Change(edit::enm::Edit::AssociatedChanged(EditedTest::Cd { x, y })) = 247 | left.diff(&right) 248 | { 249 | assert!(x.is_copy()); 250 | assert!(y.is_change()); 251 | } else { 252 | unreachable!() 253 | } 254 | } 255 | 256 | #[test] 257 | fn enm_variant_change() { 258 | let left = Test::Cd { 259 | x: 42, 260 | y: "Bilbo Baggins".to_owned(), 261 | }; 262 | let right = Test::B("Frodo Baggins".to_owned()); 263 | if let edit::Edit::Change(edit::enm::Edit::VariantChanged(l, r)) = left.diff(&right) { 264 | assert_eq!(&left, l); 265 | assert_eq!(&right, r); 266 | } else { 267 | unreachable!() 268 | } 269 | } 270 | 271 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 272 | #[derive(Diffus, Debug, PartialEq)] 273 | struct Inner { 274 | x: String, 275 | y: u32, 276 | } 277 | 278 | #[derive(Diffus, Debug, PartialEq)] 279 | struct Unit; 280 | 281 | #[derive(Diffus, Debug, PartialEq)] 282 | struct Unnamed(u32, String); 283 | 284 | #[derive(Diffus, Debug, PartialEq)] 285 | struct Outer { 286 | inner: Inner, 287 | lit: i32, 288 | } 289 | 290 | #[test] 291 | fn nested() { 292 | let left = Outer { 293 | inner: Inner { 294 | x: "x".to_owned(), 295 | y: 13, 296 | }, 297 | lit: 3, 298 | }; 299 | let right = Outer { 300 | inner: Inner { 301 | x: "x".to_owned(), 302 | y: 37, 303 | }, 304 | lit: 3, 305 | }; 306 | 307 | let diff = left.diff(&right); 308 | 309 | assert_eq!( 310 | diff.change() 311 | .unwrap() 312 | .inner 313 | .change() 314 | .unwrap() 315 | .y 316 | .change() 317 | .unwrap(), 318 | &(&13, &37) 319 | ); 320 | } 321 | 322 | #[cfg(feature = "serialize-impl")] 323 | mod serialize { 324 | use super::*; 325 | 326 | #[derive(Diffus, Default, serde::Serialize)] 327 | struct SB { 328 | u: u32, 329 | } 330 | 331 | #[derive(Diffus, Default, serde::Serialize)] 332 | struct SA { 333 | b: SB, 334 | s: String, 335 | } 336 | 337 | #[test] 338 | fn example() { 339 | use serde_json::*; 340 | 341 | let left = &SA { 342 | b: SB { u: 34 }, 343 | s: "string".to_string(), 344 | }; 345 | 346 | let string = to_string(&left.diff(&SA { 347 | b: SB { u: 34 }, 348 | s: "strga".to_string(), 349 | })) 350 | .unwrap(); 351 | 352 | let json: Value = from_str(&string).unwrap(); 353 | 354 | assert_eq!( 355 | json, 356 | serde_json::json!({ 357 | "Change": { 358 | "b": { 359 | "Copy": { 360 | "u": 34, 361 | } 362 | }, 363 | "s": { 364 | "Change": [ 365 | { 366 | "Copy": "s", 367 | }, 368 | { 369 | "Copy": "t", 370 | }, 371 | { 372 | "Copy": "r", 373 | }, 374 | { 375 | "Remove": "i", 376 | }, 377 | { 378 | "Remove": "n", 379 | }, 380 | { 381 | "Copy": "g", 382 | }, 383 | { 384 | "Insert": "a", 385 | }, 386 | ] 387 | } 388 | } 389 | }) 390 | ); 391 | } 392 | } 393 | 394 | #[test] 395 | fn struct_containing_str() { 396 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 397 | #[derive(Diffus, Debug, PartialEq)] 398 | struct A<'a> { 399 | a: &'a str, 400 | } 401 | 402 | let a = A { a: "a" }; 403 | let ap = A { a: "a'" }; 404 | 405 | let diff = a.diff(&ap); 406 | let actual = diff.change().unwrap().a.change().unwrap(); 407 | 408 | use edit::string; 409 | 410 | assert_eq!( 411 | actual, 412 | &vec![string::Edit::Copy('a'), string::Edit::Insert('\''),] 413 | ); 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /diffus-derive/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /diffus-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diffus-derive" 3 | version = "0.10.0" 4 | authors = [ 5 | "Jim Holmström ", 6 | "Johan Gardell <736172+gardell@users.noreply.github.com>", 7 | ] 8 | 9 | description = "Finds the difference between two instances of any data structure. Supports derive on structs and enums." 10 | 11 | homepage = "https://github.com/distil/diffus" 12 | repository = "https://github.com/distil/diffus" 13 | documentation = "https://docs.rs/diffus-derive" 14 | 15 | readme = "../README.md" 16 | 17 | keywords = [ "algorithm", "diff", "difference", "data", "data-structure" ] 18 | categories = [ "algorithms", "data-structures" ] 19 | publish = [ "crates-io" ] 20 | 21 | license = "Apache-2.0" 22 | 23 | edition = "2018" 24 | 25 | 26 | [lib] 27 | proc-macro = true 28 | 29 | 30 | [dependencies] 31 | proc-macro2 = "1.0" 32 | quote = "1.0" 33 | syn = "1.0" 34 | 35 | [features] 36 | serialize-impl = [] 37 | -------------------------------------------------------------------------------- /diffus-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use quote::{format_ident, quote}; 4 | 5 | type Output = proc_macro2::TokenStream; 6 | 7 | fn edit_fields(fields: &syn::Fields, lifetime: &syn::Lifetime) -> Output { 8 | let edit_fields = fields.iter().map(|field| match field { 9 | syn::Field { 10 | ident: Some(ident), 11 | ty, 12 | vis, 13 | .. 14 | } => quote! { 15 | #vis #ident: diffus::edit::Edit<#lifetime, #ty> 16 | }, 17 | syn::Field { 18 | ident: None, 19 | ty, 20 | vis, 21 | .. 22 | } => quote! { 23 | #vis diffus::edit::Edit<#lifetime, #ty> 24 | }, 25 | }); 26 | 27 | quote! { #(#edit_fields),* } 28 | } 29 | 30 | fn field_ident(enumerated_field: (usize, &syn::Field), prefix: &str) -> syn::Ident { 31 | match enumerated_field { 32 | ( 33 | _, 34 | syn::Field { 35 | ident: Some(ident), .. 36 | }, 37 | ) => format_ident!("{}{}", prefix, ident), 38 | (i, syn::Field { ident: None, .. }) => { 39 | format_ident!("{}{}", prefix, unnamed_field_ident(i)) 40 | } 41 | } 42 | } 43 | 44 | fn field_idents(fields: &syn::Fields, prefix: &str) -> Output { 45 | let field_idents = fields 46 | .iter() 47 | .enumerate() 48 | .map(|enumerated_field| field_ident(enumerated_field, prefix)); 49 | 50 | quote! { #(#field_idents),* } 51 | } 52 | 53 | fn renamed_field_ident(enumerated_field: (usize, &syn::Field), prefix: &str) -> Output { 54 | match enumerated_field { 55 | ( 56 | _, 57 | syn::Field { 58 | ident: Some(ident), .. 59 | }, 60 | ) => { 61 | let new_ident = format_ident!("{}{}", prefix, ident); 62 | 63 | quote! { #ident: #new_ident } 64 | } 65 | (_, syn::Field { ident: None, .. }) => unreachable!(), 66 | } 67 | } 68 | 69 | fn renamed_field_idents(fields: &syn::Fields, prefix: &str) -> Output { 70 | let field_idents = fields 71 | .iter() 72 | .enumerate() 73 | .map(|enumerated_field| renamed_field_ident(enumerated_field, prefix)); 74 | 75 | quote! { #(#field_idents),* } 76 | } 77 | 78 | fn matches_all_copy(fields: &syn::Fields) -> Output { 79 | let edit_fields_copy = fields.iter().enumerate().map(|_| { 80 | quote! { diffus::edit::Edit::Copy(_) } 81 | }); 82 | 83 | quote! { 84 | ( #(#edit_fields_copy),* ) => diffus::edit::Edit::Copy(self) 85 | } 86 | } 87 | 88 | fn field_diffs(fields: &syn::Fields) -> Output { 89 | let field_diffs = fields.iter().enumerate().map(|(index, field)| { 90 | let field_name = match field { 91 | syn::Field { 92 | ident: Some(ident), .. 93 | } => quote! { #ident }, 94 | syn::Field { ident: None, .. } => { 95 | let ident = unnamed_field_name(index); 96 | 97 | quote! { #ident } 98 | } 99 | }; 100 | 101 | quote! { 102 | diffus::Diffable::diff(&self.#field_name, &other.#field_name) 103 | } 104 | }); 105 | 106 | quote! { #(#field_diffs),* } 107 | } 108 | 109 | fn unnamed_field_ident(i: usize) -> syn::Ident { 110 | format_ident!("x{}", i as u32) 111 | } 112 | fn unnamed_field_name(i: usize) -> syn::Lit { 113 | syn::parse_str(&format!("{}", i as u32)).unwrap() 114 | } 115 | 116 | fn input_lifetime(generics: &syn::Generics) -> Option<&syn::Lifetime> { 117 | let mut lifetimes = generics.params.iter().filter_map(|generic_param| { 118 | if let syn::GenericParam::Lifetime(syn::LifetimeDef { lifetime, .. }) = generic_param { 119 | Some(lifetime) 120 | } else { 121 | None 122 | } 123 | }); 124 | 125 | let lifetime = lifetimes.next(); 126 | 127 | assert!( 128 | lifetimes.next().is_none(), 129 | "Multiple lifetimes not supported yet" 130 | ); 131 | 132 | lifetime 133 | } 134 | 135 | #[proc_macro_derive(Diffus)] 136 | pub fn derive_diffus(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 137 | let input: syn::DeriveInput = syn::parse2(proc_macro2::TokenStream::from(input)).unwrap(); 138 | 139 | let ident = &input.ident; 140 | let vis = &input.vis; 141 | let where_clause = &input.generics.where_clause; 142 | let edited_ident = syn::parse_str::(&format!("Edited{}", ident)).unwrap(); 143 | 144 | let data_lifetime = input_lifetime(&input.generics); 145 | let default_lifetime = syn::parse_str::("'diffus_a").unwrap(); 146 | let impl_lifetime = data_lifetime.unwrap_or(&default_lifetime); 147 | 148 | #[cfg(feature = "serialize-impl")] 149 | let derive_serialize = Some(quote! { #[derive(serde::Serialize)] }); 150 | #[cfg(not(feature = "serialize-impl"))] 151 | let derive_serialize: Option = None; 152 | 153 | proc_macro::TokenStream::from(match input.data { 154 | syn::Data::Enum(syn::DataEnum { variants, .. }) => { 155 | let edit_variants = variants.iter().map(|syn::Variant { ident, fields, .. }| { 156 | let edit_fields = edit_fields(&fields, &impl_lifetime); 157 | 158 | match fields { 159 | syn::Fields::Named(syn::FieldsNamed { .. }) => { 160 | quote! { 161 | #ident { #edit_fields } 162 | } 163 | } 164 | syn::Fields::Unnamed(syn::FieldsUnnamed { .. }) => { 165 | quote! { 166 | #ident ( #edit_fields ) 167 | } 168 | } 169 | syn::Fields::Unit => { 170 | quote! { 171 | #ident 172 | } 173 | } 174 | } 175 | }); 176 | 177 | let has_non_unit_variant = variants.iter().any(|syn::Variant { fields, .. }| { 178 | if let syn::Fields::Unit = fields { 179 | false 180 | } else { 181 | true 182 | } 183 | }); 184 | 185 | let unit_enum_impl_lifetime = if has_non_unit_variant { 186 | Some(impl_lifetime.clone()) 187 | } else { 188 | None 189 | }; 190 | 191 | let variants_matches = variants.iter().map(|syn::Variant { ident: variant_ident, fields, .. }| { 192 | 193 | let field_diffs = fields.iter().enumerate().map(|(i, field)| { 194 | let self_field_ident = field_ident((i, field), "self_"); 195 | let other_field_ident = field_ident((i, field), "other_"); 196 | 197 | quote! { 198 | #self_field_ident . diff(& #other_field_ident ) 199 | } 200 | }); 201 | let field_diffs = quote! { #(#field_diffs),* }; 202 | 203 | let matches_all_copy = matches_all_copy(&fields); 204 | let just_field_idents = field_idents(&fields, ""); 205 | let self_field_idents = field_idents(&fields, "self_"); 206 | let other_field_idents = field_idents(&fields, "other_"); 207 | 208 | match fields { 209 | syn::Fields::Named(syn::FieldsNamed { .. }) => { 210 | let self_field_idents = renamed_field_idents(&fields, "self_"); 211 | let other_field_idents = renamed_field_idents(&fields, "other_"); 212 | 213 | quote! { 214 | ( 215 | #ident::#variant_ident { #self_field_idents }, 216 | #ident::#variant_ident { #other_field_idents } 217 | ) => { 218 | match ( #field_diffs ) { 219 | #matches_all_copy, 220 | ( #just_field_idents ) => { 221 | diffus::edit::Edit::Change( 222 | diffus::edit::enm::Edit::AssociatedChanged( 223 | #edited_ident::#variant_ident { #just_field_idents } 224 | ) 225 | ) 226 | } 227 | } 228 | } 229 | } 230 | }, 231 | syn::Fields::Unnamed(syn::FieldsUnnamed { .. }) => { 232 | quote! { 233 | ( 234 | #ident::#variant_ident( #self_field_idents ), 235 | #ident::#variant_ident( #other_field_idents ) 236 | ) => { 237 | match ( #field_diffs ) { 238 | #matches_all_copy, 239 | ( #just_field_idents ) => { 240 | diffus::edit::Edit::Change( 241 | diffus::edit::enm::Edit::AssociatedChanged( 242 | #edited_ident::#variant_ident ( #just_field_idents ) 243 | ) 244 | ) 245 | } 246 | } 247 | } 248 | } 249 | }, 250 | syn::Fields::Unit => { 251 | quote! { 252 | ( 253 | #ident::#variant_ident, 254 | #ident::#variant_ident 255 | ) => { 256 | diffus::edit::Edit::Copy(self) 257 | } 258 | } 259 | }, 260 | } 261 | }); 262 | 263 | quote! { 264 | #derive_serialize 265 | #vis enum #edited_ident <#unit_enum_impl_lifetime> where #where_clause { 266 | #(#edit_variants),* 267 | } 268 | 269 | impl<#impl_lifetime> diffus::Diffable<#impl_lifetime> for #ident <#data_lifetime> where #where_clause { 270 | type Diff = diffus::edit::enm::Edit<#impl_lifetime, Self, #edited_ident <#unit_enum_impl_lifetime>>; 271 | 272 | fn diff(&#impl_lifetime self, other: &#impl_lifetime Self) -> diffus::edit::Edit<#impl_lifetime, Self> { 273 | match (self, other) { 274 | #(#variants_matches,)* 275 | (self_variant, other_variant) => diffus::edit::Edit::Change(diffus::edit::enm::Edit::VariantChanged( 276 | self_variant, other_variant 277 | )), 278 | } 279 | } 280 | } 281 | } 282 | } 283 | syn::Data::Struct(syn::DataStruct { fields, .. }) => { 284 | let edit_fields = edit_fields(&fields, &impl_lifetime); 285 | let field_diffs = field_diffs(&fields); 286 | let field_idents = field_idents(&fields, ""); 287 | let matches_all_copy = matches_all_copy(&fields); 288 | 289 | match fields { 290 | syn::Fields::Named(_) => { 291 | quote! { 292 | #derive_serialize 293 | #vis struct #edited_ident<#impl_lifetime> where #where_clause { 294 | #edit_fields 295 | } 296 | 297 | impl<#impl_lifetime> diffus::Diffable<#impl_lifetime> for #ident <#data_lifetime> where #where_clause { 298 | type Diff = #edited_ident<#impl_lifetime>; 299 | 300 | fn diff(&#impl_lifetime self, other: &#impl_lifetime Self) -> diffus::edit::Edit<#impl_lifetime, Self> { 301 | match ( #field_diffs ) { 302 | #matches_all_copy, 303 | ( #field_idents ) => diffus::edit::Edit::Change( 304 | #edited_ident { #field_idents } 305 | ) 306 | } 307 | } 308 | } 309 | } 310 | } 311 | syn::Fields::Unnamed(_) => { 312 | quote! { 313 | #derive_serialize 314 | #vis struct #edited_ident<#impl_lifetime> ( #edit_fields ) where #where_clause; 315 | 316 | impl<#impl_lifetime> diffus::Diffable<#impl_lifetime> for #ident <#data_lifetime> where #where_clause { 317 | type Diff = #edited_ident<#impl_lifetime>; 318 | 319 | fn diff(&#impl_lifetime self, other: &#impl_lifetime Self) -> diffus::edit::Edit<#impl_lifetime, Self> { 320 | match ( #field_diffs ) { 321 | #matches_all_copy, 322 | ( #field_idents ) => diffus::edit::Edit::Change( 323 | #edited_ident ( #field_idents ) 324 | ) 325 | } 326 | } 327 | } 328 | } 329 | } 330 | syn::Fields::Unit => { 331 | quote! { 332 | #derive_serialize 333 | #vis struct #edited_ident< > where #where_clause; 334 | 335 | impl<#impl_lifetime> diffus::Diffable<#impl_lifetime> for #ident< > where #where_clause { 336 | type Diff = #edited_ident; 337 | 338 | fn diff(&#impl_lifetime self, other: &#impl_lifetime Self) -> diffus::edit::Edit<#impl_lifetime, Self> { 339 | diffus::edit::Edit::Copy(self) 340 | } 341 | } 342 | } 343 | } 344 | } 345 | } 346 | syn::Data::Union(_) => panic!("union type not supported yet"), 347 | }) 348 | } 349 | -------------------------------------------------------------------------------- /diffus/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /diffus/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diffus" 3 | version = "0.10.0" 4 | authors = [ 5 | "Jim Holmström ", 6 | "Johan Gardell <736172+gardell@users.noreply.github.com>", 7 | ] 8 | 9 | description = "Finds the difference between two instances of any data structure. Supports: collections, Strings, Maps etc. Uses LCS where applicable. Also supports derive via `diffus-derive`." 10 | 11 | homepage = "https://github.com/distil/diffus" 12 | repository = "https://github.com/distil/diffus" 13 | documentation = "https://docs.rs/diffus" 14 | 15 | readme = "../README.md" 16 | 17 | keywords = [ "algorithm", "diff", "difference", "data", "data-structure" ] 18 | categories = [ "algorithms", "data-structures" ] 19 | publish = [ "crates-io" ] 20 | 21 | license = "Apache-2.0" 22 | 23 | edition = "2018" 24 | 25 | 26 | [lib] 27 | name = "diffus" 28 | path = "src/lib.rs" 29 | 30 | 31 | [dependencies] 32 | itertools = "0.10" 33 | 34 | indexmap = { version = "1", optional = true } 35 | uuid = { version = ">=0.5", optional = true } 36 | snake_case = { version = "0.3", optional = true } 37 | 38 | serde = { version = "1.0", features = [ "derive" ], optional = true } 39 | 40 | diffus-derive = { version = "=0.10.0", path = "../diffus-derive", optional = true } 41 | 42 | 43 | [features] 44 | default = [] 45 | 46 | derive = [ "diffus-derive" ] 47 | 48 | indexmap-impl = [ "indexmap" ] 49 | uuid-impl = [ "uuid" ] 50 | snake_case-impl = [ "snake_case" ] 51 | 52 | serialize-impl = [ 53 | "diffus-derive/serialize-impl", 54 | "serde", 55 | "indexmap/serde-1", 56 | "uuid/serde", 57 | "snake_case/serde" 58 | ] 59 | -------------------------------------------------------------------------------- /diffus/src/diffable_impls/borrow.rs: -------------------------------------------------------------------------------- 1 | use crate::{edit, Diffable}; 2 | use std::borrow::Borrow; 3 | 4 | fn diff_borrowable<'a, T, C, D>(left: &'a C, right: &'a C) -> edit::Edit<'a, C> 5 | where 6 | T: Diffable<'a> + ?Sized + 'a, 7 | C: Borrow + Diffable<'a, Diff = D> + ?Sized, 8 | D: From, 9 | { 10 | match left.borrow().diff(right.borrow()) { 11 | edit::Edit::Copy(_) => edit::Edit::Copy(left), 12 | edit::Edit::Change(diff) => edit::Edit::Change(diff.into()), 13 | } 14 | } 15 | 16 | macro_rules! borrow_impl { 17 | ($($typ:ident),*) => { 18 | $( 19 | impl<'a, T: Diffable<'a> + ?Sized + 'a> Diffable<'a> for $typ { 20 | type Diff = $typ; 21 | 22 | fn diff(&'a self, other: &'a Self) -> edit::Edit<'a, Self> { 23 | diff_borrowable::(self, other) 24 | } 25 | } 26 | )* 27 | } 28 | } 29 | 30 | use std::{rc::Rc, sync::Arc}; 31 | borrow_impl! { 32 | Box, Rc, Arc 33 | } 34 | 35 | impl<'a, T: Diffable<'a> + ?Sized + 'a> Diffable<'a> for &'a T { 36 | type Diff = T::Diff; 37 | 38 | fn diff(&'a self, other: &'a Self) -> edit::Edit<'a, Self> { 39 | diff_borrowable::(self, other) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | 47 | #[test] 48 | fn box_example() { 49 | let left = 13; 50 | let right = 37; 51 | 52 | if let edit::Edit::Change(diff) = Box::new(left).diff(&Box::new(right)) { 53 | assert_eq!(*diff, (&13, &37)); 54 | } 55 | } 56 | 57 | #[test] 58 | fn rc_example() { 59 | let left = 13; 60 | let right = 37; 61 | 62 | if let edit::Edit::Change(diff) = Rc::new(left).diff(&Rc::new(right)) { 63 | assert_eq!(*diff, (&13, &37)); 64 | } 65 | } 66 | 67 | #[test] 68 | fn arc_example() { 69 | let left = 13; 70 | let right = 37; 71 | 72 | if let edit::Edit::Change(diff) = Arc::new(left).diff(&Arc::new(right)) { 73 | assert_eq!(*diff, (&13, &37)); 74 | } 75 | } 76 | 77 | #[test] 78 | fn reference_example() { 79 | let left = 13; 80 | let right = 37; 81 | 82 | if let edit::Edit::Change(diff) = (&left).diff(&(&right)) { 83 | assert_eq!(diff, (&13, &37)); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /diffus/src/diffable_impls/collection.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | edit::{self, collection}, 3 | Diffable, Same, 4 | }; 5 | 6 | macro_rules! collection_impl { 7 | ($($typ:ident),*) => { 8 | $( 9 | impl<'a, T: Same + Diffable<'a> + 'a> Diffable<'a> for $typ { 10 | type Diff = Vec>; 11 | 12 | fn diff(&'a self, other: &'a Self) -> edit::Edit { 13 | 14 | let s = crate::lcs::lcs_post_change( 15 | crate::lcs::lcs( 16 | || self.iter(), 17 | || other.iter(), 18 | self.len(), 19 | other.len(), 20 | ) 21 | ) 22 | .collect::>(); 23 | 24 | if s.iter().all(collection::Edit::is_copy) { 25 | edit::Edit::Copy(self) 26 | } else { 27 | edit::Edit::Change(s) 28 | } 29 | } 30 | } 31 | )* 32 | } 33 | } 34 | 35 | use std::collections::{BinaryHeap, LinkedList, VecDeque}; 36 | collection_impl! { 37 | BinaryHeap, LinkedList, Vec, VecDeque 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | 44 | #[test] 45 | fn diff() { 46 | use super::Diffable; 47 | 48 | let left = b"XMJYAUZ".to_vec(); 49 | let right = b"MZJAWXU".to_vec(); 50 | 51 | let diff = left.diff(&right); 52 | if let edit::Edit::Change(diff) = diff { 53 | use collection::Edit::*; 54 | 55 | assert_eq!( 56 | diff.into_iter().collect::>(), 57 | vec![ 58 | Remove(&b'X'), 59 | Copy(&b'M'), 60 | Insert(&b'Z'), 61 | Copy(&b'J'), 62 | Remove(&b'Y'), 63 | Copy(&b'A'), 64 | Insert(&b'W'), 65 | Insert(&b'X'), 66 | Copy(&b'U'), 67 | Remove(&b'Z') 68 | ] 69 | ); 70 | } else { 71 | unreachable!() 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /diffus/src/diffable_impls/map.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | edit::{map, Edit}, 3 | Diffable, 4 | }; 5 | 6 | macro_rules! map_impl { 7 | ($(($typ:ident, $key_constraint:ident)),*) => { 8 | $( 9 | impl<'a, K: Eq + $key_constraint + 'a, V: Diffable<'a> + 'a> Diffable<'a> for $typ { 10 | type Diff = $typ<&'a K, map::Edit<'a, V>>; 11 | 12 | fn diff(&'a self, other: &'a Self) -> Edit { 13 | let intersection = self 14 | .iter() 15 | .filter_map(|(k, v)| Some((k, (v, other.get(k)?)))); 16 | 17 | let unique_self = self.iter().filter(|(k, _)| !other.contains_key(*k)); 18 | 19 | let unique_other = other.iter().filter(|(k, _)| !self.contains_key(*k)); 20 | 21 | let value_diffs = unique_other 22 | .map(|(k, v)| (k, map::Edit::Insert(v))) 23 | .chain(unique_self.map(|(k, v)| (k, map::Edit::Remove(v)))) 24 | .chain(intersection.map(|(k, (self_v, other_v))| (k, self_v.diff(other_v).into()))) 25 | .collect::<$typ<_, _>>(); 26 | 27 | if value_diffs.values().any(|v| !v.is_copy()) { 28 | Edit::Change(value_diffs) 29 | } else { 30 | Edit::Copy(self) 31 | } 32 | } 33 | } 34 | )* 35 | } 36 | } 37 | 38 | use std::{ 39 | collections::{BTreeMap, HashMap}, 40 | hash::Hash, 41 | }; 42 | map_impl! { 43 | (BTreeMap, Ord), 44 | (HashMap, Hash) 45 | } 46 | 47 | #[cfg(feature = "indexmap-impl")] 48 | use indexmap::IndexMap; 49 | #[cfg(feature = "indexmap-impl")] 50 | map_impl! { (IndexMap, Hash) } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::*; 55 | 56 | #[test] 57 | fn example() { 58 | let unity: std::collections::HashMap<_, _> = 59 | [(1, 1), (2, 2), (3, 3)].iter().cloned().collect(); 60 | let not_unity: std::collections::HashMap<_, _> = 61 | [(1, 1), (2, 3), (4, 4)].iter().cloned().collect(); 62 | 63 | if let Edit::Change(diff) = unity.diff(¬_unity) { 64 | assert!(diff[&1].is_copy()); 65 | assert_eq!(diff[&2].change().unwrap(), &(&2, &3)); 66 | assert!(diff[&3].is_remove()); 67 | assert_eq!(diff[&4].insert().unwrap(), &4); 68 | } else { 69 | unreachable!() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /diffus/src/diffable_impls/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod borrow; 2 | pub mod collection; 3 | pub mod map; 4 | pub mod option; 5 | pub mod primitives; 6 | pub mod set; 7 | pub mod string; 8 | -------------------------------------------------------------------------------- /diffus/src/diffable_impls/option.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | edit::{self, enm}, 3 | Diffable, 4 | }; 5 | 6 | impl<'a, T: Diffable<'a> + 'a> Diffable<'a> for Option { 7 | type Diff = enm::Edit<'a, Self, T::Diff>; 8 | 9 | fn diff(&'a self, other: &'a Self) -> edit::Edit { 10 | match (self, other) { 11 | (None, None) => edit::Edit::Copy(self), 12 | (Some(a), Some(b)) => match a.diff(&b) { 13 | edit::Edit::Copy(_) => edit::Edit::Copy(self), 14 | edit::Edit::Change(diff) => edit::Edit::Change(enm::Edit::AssociatedChanged(diff)), 15 | }, 16 | _ => edit::Edit::Change(enm::Edit::VariantChanged(self, other)), 17 | } 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use super::*; 24 | 25 | #[test] 26 | fn is_copy() { 27 | assert!((None as Option).diff(&None).is_copy()); 28 | assert!(Some(3).diff(&Some(3)).is_copy()); 29 | } 30 | 31 | #[test] 32 | fn variant_changed() { 33 | if let Some(enm::Edit::VariantChanged(&None, &Some(3))) = None.diff(&Some(3)).change() { 34 | } else { 35 | unreachable!(); 36 | } 37 | } 38 | 39 | #[test] 40 | fn associate_change() { 41 | if let Some(enm::Edit::AssociatedChanged((&1, &2))) = Some(1).diff(&Some(2)).change() { 42 | } else { 43 | unreachable!(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /diffus/src/diffable_impls/primitives.rs: -------------------------------------------------------------------------------- 1 | use crate::{edit, Diffable}; 2 | 3 | macro_rules! primitive_impl { 4 | ($($typ:ty),*) => { 5 | $( 6 | impl<'a> Diffable<'a> for $typ { 7 | type Diff = (&'a $typ, &'a $typ); 8 | 9 | fn diff(&'a self, other: &'a Self) -> edit::Edit { 10 | use crate::Same; 11 | if self.same(other) { 12 | edit::Edit::Copy(self) 13 | } else { 14 | edit::Edit::Change((self, other)) 15 | } 16 | } 17 | } 18 | )* 19 | } 20 | } 21 | 22 | primitive_impl! { i64, i32, i16, i8, u64, u32, u16, u8, char, bool, isize, usize, f32, f64, () } 23 | 24 | #[cfg(feature = "uuid-impl")] 25 | primitive_impl! { uuid::Uuid } 26 | 27 | #[cfg(feature = "snake_case-impl")] 28 | primitive_impl! { snake_case::SnakeCase } 29 | -------------------------------------------------------------------------------- /diffus/src/diffable_impls/set.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | edit::{set, Edit}, 3 | Diffable, 4 | }; 5 | 6 | macro_rules! set_impl { 7 | ($(($typ:ident, $key_constraint:ident, $diff_type:ident)),*) => { 8 | $( 9 | impl<'a, K: Diffable<'a> + Eq + $key_constraint + 'a> Diffable<'a> for $typ { 10 | type Diff = $diff_type<&'a K, set::Edit<'a, K>>; 11 | 12 | fn diff(&'a self, other: &'a Self) -> Edit { 13 | let intersection = self 14 | .iter() 15 | .filter(|k| other.contains(*k)); 16 | 17 | let unique_self = self.iter().filter(|k| !other.contains(*k)); 18 | 19 | let unique_other = other.iter().filter(|k| !self.contains(*k)); 20 | 21 | let value_diffs = unique_other 22 | .map(|k| (k, set::Edit::Insert(k))) 23 | .chain(unique_self.map(|k| (k, set::Edit::Remove(k)))) 24 | .chain(intersection.map(|k| (k, set::Edit::Copy(k)))) 25 | .collect::<$diff_type<_, _>>(); 26 | 27 | if value_diffs.iter().any(|(_, edit)| !edit.is_copy()) { 28 | Edit::Change(value_diffs) 29 | } else { 30 | Edit::Copy(self) 31 | } 32 | } 33 | } 34 | )* 35 | } 36 | } 37 | 38 | use std::{ 39 | collections::{BTreeMap, BTreeSet, HashMap, HashSet}, 40 | hash::Hash, 41 | }; 42 | set_impl! { 43 | (BTreeSet, Ord, BTreeMap), 44 | (HashSet, Hash, HashMap) 45 | } 46 | 47 | #[cfg(feature = "indexmap-impl")] 48 | use indexmap::{IndexMap, IndexSet}; 49 | #[cfg(feature = "indexmap-impl")] 50 | set_impl! { (IndexSet, Hash, IndexMap) } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::*; 55 | 56 | #[test] 57 | fn example() { 58 | let unity: std::collections::HashSet<_, _> = [1, 2, 3].iter().cloned().collect(); 59 | let not_unity: std::collections::HashSet<_, _> = [1, 2, 4].iter().cloned().collect(); 60 | 61 | if let Edit::Change(diff) = unity.diff(¬_unity) { 62 | assert!(diff[&1].is_copy()); 63 | assert!(diff[&2].is_copy()); 64 | assert!(diff[&3].is_remove()); 65 | assert_eq!(diff[&4].insert().unwrap(), &4); 66 | } else { 67 | unreachable!() 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /diffus/src/diffable_impls/string.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | edit::{self, string}, 3 | lcs, Diffable, 4 | }; 5 | 6 | impl<'a> Diffable<'a> for str { 7 | type Diff = Vec; 8 | 9 | fn diff(&'a self, other: &'a Self) -> edit::Edit { 10 | let s = lcs::lcs( 11 | || self.chars(), 12 | || other.chars(), 13 | self.chars().count(), 14 | other.chars().count(), 15 | ) 16 | .map(Into::into) 17 | .collect::>(); 18 | 19 | if s.iter().all(string::Edit::is_copy) { 20 | edit::Edit::Copy(self) 21 | } else { 22 | edit::Edit::Change(s) 23 | } 24 | } 25 | } 26 | 27 | impl<'a> Diffable<'a> for String { 28 | type Diff = >::Diff; 29 | 30 | fn diff(&'a self, other: &'a Self) -> edit::Edit { 31 | match self.as_str().diff(other.as_str()) { 32 | edit::Edit::Change(diff) => edit::Edit::Change(diff), 33 | edit::Edit::Copy(_) => edit::Edit::Copy(self), 34 | } 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use crate::edit::{self, string}; 41 | 42 | #[test] 43 | fn string() { 44 | use super::Diffable; 45 | 46 | let left = "XMJYAUZ".to_owned(); 47 | let right = "MZJAWXU".to_owned(); 48 | 49 | let diff = left.diff(&right); 50 | if let edit::Edit::Change(diff) = diff { 51 | assert_eq!( 52 | diff.into_iter().collect::>(), 53 | vec![ 54 | string::Edit::Remove('X'), 55 | string::Edit::Copy('M'), 56 | string::Edit::Insert('Z'), 57 | string::Edit::Copy('J'), 58 | string::Edit::Remove('Y'), 59 | string::Edit::Copy('A'), 60 | string::Edit::Insert('W'), 61 | string::Edit::Insert('X'), 62 | string::Edit::Copy('U'), 63 | string::Edit::Remove('Z') 64 | ] 65 | ); 66 | } else { 67 | unreachable!() 68 | } 69 | } 70 | 71 | #[test] 72 | fn str() { 73 | use super::Diffable; 74 | 75 | let left = "XMJYAUZ"; 76 | let right = "MZJAWXU"; 77 | 78 | let diff = left.diff(&right); 79 | if let edit::Edit::Change(diff) = diff { 80 | assert_eq!( 81 | diff.into_iter().collect::>(), 82 | vec![ 83 | string::Edit::Remove('X'), 84 | string::Edit::Copy('M'), 85 | string::Edit::Insert('Z'), 86 | string::Edit::Copy('J'), 87 | string::Edit::Remove('Y'), 88 | string::Edit::Copy('A'), 89 | string::Edit::Insert('W'), 90 | string::Edit::Insert('X'), 91 | string::Edit::Copy('U'), 92 | string::Edit::Remove('Z') 93 | ] 94 | ); 95 | } else { 96 | unreachable!() 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /diffus/src/edit/collection.rs: -------------------------------------------------------------------------------- 1 | use crate::Same; 2 | 3 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 4 | #[derive(Debug, PartialEq, Eq)] 5 | pub enum Edit<'a, T: ?Sized, Diff> { 6 | Copy(&'a T), 7 | Insert(&'a T), 8 | Remove(&'a T), 9 | Change(Diff), 10 | } 11 | 12 | impl<'a, T: Same + ?Sized, Diff> Edit<'a, T, Diff> { 13 | pub fn is_copy(&self) -> bool { 14 | if let Self::Copy(_) = self { 15 | true 16 | } else { 17 | false 18 | } 19 | } 20 | 21 | pub fn is_insert(&self) -> bool { 22 | if let Self::Insert(_) = self { 23 | true 24 | } else { 25 | false 26 | } 27 | } 28 | 29 | pub fn is_remove(&self) -> bool { 30 | if let Self::Remove(_) = self { 31 | true 32 | } else { 33 | false 34 | } 35 | } 36 | 37 | pub fn is_change(&self) -> bool { 38 | self.change().is_some() 39 | } 40 | 41 | pub fn copy(&self) -> Option<&T> { 42 | if let Self::Copy(value) = self { 43 | Some(value) 44 | } else { 45 | None 46 | } 47 | } 48 | 49 | pub fn insert(&self) -> Option<&T> { 50 | if let Self::Insert(value) = self { 51 | Some(value) 52 | } else { 53 | None 54 | } 55 | } 56 | 57 | pub fn remove(&self) -> Option<&T> { 58 | if let Self::Remove(value) = self { 59 | Some(value) 60 | } else { 61 | None 62 | } 63 | } 64 | 65 | pub fn change(&self) -> Option<&Diff> { 66 | if let Self::Change(value) = self { 67 | Some(value) 68 | } else { 69 | None 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /diffus/src/edit/enm.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 2 | #[derive(Debug, Eq, PartialEq)] 3 | pub enum Edit<'a, T: ?Sized, Diff> { 4 | Copy(&'a T), 5 | VariantChanged(&'a T, &'a T), 6 | AssociatedChanged(Diff), 7 | } 8 | 9 | impl<'a, T: ?Sized, Diff> Edit<'a, T, Diff> { 10 | pub fn is_copy(&self) -> bool { 11 | if let Self::Copy(_) = self { 12 | true 13 | } else { 14 | false 15 | } 16 | } 17 | 18 | pub fn is_variant_changed(&self) -> bool { 19 | if let Self::VariantChanged(_, _) = self { 20 | true 21 | } else { 22 | false 23 | } 24 | } 25 | 26 | pub fn is_associated_changed(&self) -> bool { 27 | if let Self::AssociatedChanged(_) = self { 28 | true 29 | } else { 30 | false 31 | } 32 | } 33 | 34 | pub fn variant_changed(&self) -> Option<(&'a T, &'a T)> { 35 | if let Self::VariantChanged(left, right) = self { 36 | Some((left, right)) 37 | } else { 38 | None 39 | } 40 | } 41 | 42 | pub fn associated_change(&self) -> Option<&Diff> { 43 | if let Self::AssociatedChanged(value) = self { 44 | Some(value) 45 | } else { 46 | None 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /diffus/src/edit/map.rs: -------------------------------------------------------------------------------- 1 | use crate::Diffable; 2 | 3 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 4 | #[derive(Debug, PartialEq)] 5 | pub enum Edit<'a, T: Diffable<'a> + ?Sized> { 6 | Copy(&'a T), 7 | Insert(&'a T), 8 | Remove(&'a T), 9 | Change(T::Diff), 10 | } 11 | 12 | impl<'a, T: Diffable<'a> + ?Sized> Edit<'a, T> { 13 | // 14 | // Checks if the edit is an insert. 15 | // 16 | // # Examples 17 | // 18 | // ``` 19 | // assert_eq!(Edit::Insert(&2).is_insert(), true); 20 | // assert_eq!(Edit::Remove.is_insert(), false); 21 | // ``` 22 | pub fn is_insert(&self) -> bool { 23 | if let Self::Insert(_) = self { 24 | true 25 | } else { 26 | false 27 | } 28 | } 29 | pub fn is_remove(&self) -> bool { 30 | if let Self::Remove(_) = self { 31 | true 32 | } else { 33 | false 34 | } 35 | } 36 | pub fn is_copy(&self) -> bool { 37 | if let Self::Copy(_) = self { 38 | true 39 | } else { 40 | false 41 | } 42 | } 43 | pub fn is_change(&self) -> bool { 44 | if let Self::Change(_) = self { 45 | true 46 | } else { 47 | false 48 | } 49 | } 50 | pub fn insert(&self) -> Option<&'a T> { 51 | if let Self::Insert(value) = self { 52 | Some(value) 53 | } else { 54 | None 55 | } 56 | } 57 | pub fn remove(&self) -> Option<&'a T> { 58 | if let Self::Remove(value) = self { 59 | Some(value) 60 | } else { 61 | None 62 | } 63 | } 64 | pub fn change(&self) -> Option<&T::Diff> { 65 | if let Self::Change(value_diff) = self { 66 | Some(value_diff) 67 | } else { 68 | None 69 | } 70 | } 71 | pub fn copy(&self) -> Option<&'a T> { 72 | if let Self::Copy(value) = self { 73 | Some(value) 74 | } else { 75 | None 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /diffus/src/edit/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod collection; 2 | pub mod enm; 3 | pub mod map; 4 | pub mod set; 5 | pub mod string; 6 | 7 | use crate::Diffable; 8 | 9 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 10 | #[derive(Debug, PartialEq, Eq)] 11 | pub enum Edit<'a, T: Diffable<'a> + ?Sized> { 12 | Copy(&'a T), 13 | Change(T::Diff), 14 | } 15 | 16 | impl<'a, T: Diffable<'a> + ?Sized> Edit<'a, T> { 17 | pub fn is_copy(&self) -> bool { 18 | if let Self::Copy(_) = self { 19 | true 20 | } else { 21 | false 22 | } 23 | } 24 | 25 | pub fn copy(&self) -> Option<&'a T> { 26 | if let Self::Copy(value) = self { 27 | Some(value) 28 | } else { 29 | None 30 | } 31 | } 32 | 33 | pub fn is_change(&self) -> bool { 34 | if let Self::Change(_) = self { 35 | true 36 | } else { 37 | false 38 | } 39 | } 40 | 41 | pub fn change(&self) -> Option<&T::Diff> { 42 | if let Self::Change(value_diff) = self { 43 | Some(value_diff) 44 | } else { 45 | None 46 | } 47 | } 48 | } 49 | 50 | impl<'a, Diff, T: Diffable<'a, Diff = Diff> + 'a> Into> for Edit<'a, T> { 51 | fn into(self) -> map::Edit<'a, T> { 52 | match self { 53 | Self::Copy(value) => map::Edit::Copy(value), 54 | Self::Change(diff) => map::Edit::Change(diff), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /diffus/src/edit/set.rs: -------------------------------------------------------------------------------- 1 | use crate::Diffable; 2 | 3 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 4 | #[derive(Debug, PartialEq)] 5 | pub enum Edit<'a, T: Diffable<'a> + ?Sized> { 6 | Copy(&'a T), 7 | Insert(&'a T), 8 | Remove(&'a T), 9 | } 10 | 11 | impl<'a, T: Diffable<'a> + ?Sized> Edit<'a, T> { 12 | pub fn is_copy(&self) -> bool { 13 | if let Self::Copy(_) = self { 14 | true 15 | } else { 16 | false 17 | } 18 | } 19 | // 20 | // Checks if the edit is an insert. 21 | // 22 | // # Examples 23 | // 24 | // ``` 25 | // assert_eq!(Edit::Insert(&2).is_insert(), true); 26 | // assert_eq!(Edit::Remove.is_insert(), false); 27 | // ``` 28 | pub fn is_insert(&self) -> bool { 29 | if let Self::Insert(_) = self { 30 | true 31 | } else { 32 | false 33 | } 34 | } 35 | pub fn is_remove(&self) -> bool { 36 | if let Self::Remove(_) = self { 37 | true 38 | } else { 39 | false 40 | } 41 | } 42 | pub fn copy(&self) -> Option<&'a T> { 43 | if let Self::Copy(value) = self { 44 | Some(value) 45 | } else { 46 | None 47 | } 48 | } 49 | pub fn insert(&self) -> Option<&'a T> { 50 | if let Self::Insert(value) = self { 51 | Some(value) 52 | } else { 53 | None 54 | } 55 | } 56 | pub fn remove(&self) -> Option<&'a T> { 57 | if let Self::Remove(value) = self { 58 | Some(value) 59 | } else { 60 | None 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /diffus/src/edit/string.rs: -------------------------------------------------------------------------------- 1 | use crate::lcs; 2 | 3 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 4 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 5 | pub enum Edit { 6 | Copy(char), 7 | Insert(char), 8 | Remove(char), 9 | } 10 | 11 | impl From> for Edit { 12 | fn from(edit: lcs::Edit) -> Self { 13 | use lcs::Edit::*; 14 | match edit { 15 | Same(left, _) => Self::Copy(left), 16 | Insert(value) => Self::Insert(value), 17 | Remove(value) => Self::Remove(value), 18 | } 19 | } 20 | } 21 | 22 | impl Edit { 23 | pub fn is_copy(&self) -> bool { 24 | if let Self::Copy(_) = self { 25 | true 26 | } else { 27 | false 28 | } 29 | } 30 | pub fn is_insert(&self) -> bool { 31 | if let Self::Insert(_) = self { 32 | true 33 | } else { 34 | false 35 | } 36 | } 37 | 38 | pub fn is_remove(&self) -> bool { 39 | if let Self::Remove(_) = self { 40 | true 41 | } else { 42 | false 43 | } 44 | } 45 | 46 | pub fn copy(self) -> Option { 47 | if let Self::Copy(value) = self { 48 | Some(value) 49 | } else { 50 | None 51 | } 52 | } 53 | 54 | pub fn insert(self) -> Option { 55 | if let Self::Insert(value) = self { 56 | Some(value) 57 | } else { 58 | None 59 | } 60 | } 61 | 62 | pub fn remove(self) -> Option { 63 | if let Self::Remove(value) = self { 64 | Some(value) 65 | } else { 66 | None 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /diffus/src/lcs.rs: -------------------------------------------------------------------------------- 1 | use crate::{edit, Diffable, Same}; 2 | 3 | #[cfg_attr(feature = "serialize-impl", derive(serde::Serialize))] 4 | #[derive(Debug, PartialEq, Eq)] 5 | pub enum Edit { 6 | Same(T, T), 7 | Insert(T), 8 | Remove(T), 9 | } 10 | 11 | impl Edit { 12 | pub fn is_same(&self) -> bool { 13 | if let Edit::Same(_, _) = self { 14 | true 15 | } else { 16 | false 17 | } 18 | } 19 | } 20 | 21 | fn c_matrix( 22 | x: impl Fn() -> I, 23 | y: impl Fn() -> J, 24 | x_len: usize, 25 | y_len: usize, 26 | ) -> (usize, crate::twodvec::TwoDVec, usize) 27 | where 28 | I: DoubleEndedIterator, 29 | J: DoubleEndedIterator, 30 | { 31 | let mut x_iter = x(); 32 | let mut y_iter = y(); 33 | let prefix_eq = x_iter 34 | .by_ref() 35 | .zip(y_iter.by_ref()) 36 | .take_while(|(x, y)| x.same(y)) 37 | .count(); 38 | // Only check the suffix if we did not consume the entirety of either of the iterators 39 | // (If one of them are consumed, we would double count elements) 40 | let check_suffix = x_len.min(y_len) != prefix_eq; 41 | let suffix_eq = if check_suffix { 42 | x_iter 43 | .rev() 44 | .zip(y_iter.rev()) 45 | .take_while(|(x, y)| x.same(y)) 46 | .count() 47 | } else { 48 | 0 49 | }; 50 | 51 | let width = x_len.saturating_sub(prefix_eq + suffix_eq) + 1; 52 | let height = y_len.saturating_sub(prefix_eq + suffix_eq) + 1; 53 | 54 | let mut c = crate::twodvec::TwoDVec::new(0, width, height); 55 | 56 | for (i, x) in x().skip(prefix_eq).take(width - 1).enumerate() { 57 | for (j, y) in y().skip(prefix_eq).take(height - 1).enumerate() { 58 | c[j + 1][i + 1] = if x.same(&y) { 59 | c[j][i] + 1 60 | } else { 61 | c[j][i + 1].max(c[j + 1][i]) 62 | }; 63 | } 64 | } 65 | 66 | (prefix_eq, c, suffix_eq) 67 | } 68 | 69 | fn lcs_base( 70 | c: crate::twodvec::TwoDVec, 71 | mut x: itertools::PutBack>, 72 | mut y: itertools::PutBack>, 73 | ) -> impl Iterator> { 74 | let mut i = c.width() - 1; 75 | let mut j = c.height() - 1; 76 | 77 | std::iter::from_fn(move || { 78 | let current_x = x.next(); 79 | let current_y = y.next(); 80 | 81 | let left = j.checked_sub(1).map(|j_minus| c[j_minus][i]); 82 | let above = i.checked_sub(1).map(|i_minus| c[j][i_minus]); 83 | 84 | if current_x.is_some() 85 | && current_y.is_some() 86 | && current_x 87 | .as_ref() 88 | .unwrap() 89 | .same(current_y.as_ref().unwrap()) 90 | { 91 | i = i - 1; 92 | j = j - 1; 93 | 94 | match (current_x, current_y) { 95 | (Some(current_x), Some(current_y)) => Some(Edit::Same(current_x, current_y)), 96 | _ => unreachable!(), 97 | } 98 | } else if current_y.is_some() && (current_x.is_none() || left >= above) { 99 | current_x.map(|c| x.put_back(c)); 100 | j = j - 1; 101 | current_y.map(|value| Edit::Insert(value)) 102 | } else if current_x.is_some() && (current_y.is_none() || left < above) { 103 | current_y.map(|c| y.put_back(c)); 104 | i = i - 1; 105 | current_x.map(|value| Edit::Remove(value)) 106 | } else { 107 | None 108 | } 109 | }) 110 | .collect::>() 111 | .into_iter() 112 | .rev() 113 | } 114 | 115 | pub(crate) fn lcs< 116 | 'a, 117 | T: Same, 118 | I: DoubleEndedIterator, 119 | J: DoubleEndedIterator, 120 | >( 121 | x: impl Fn() -> I, 122 | y: impl Fn() -> J, 123 | x_len: usize, 124 | y_len: usize, 125 | ) -> impl Iterator> { 126 | let (prefix_eq, c, suffix_eq) = c_matrix(&x, &y, x_len, y_len); 127 | 128 | x().zip(y()) 129 | .take(prefix_eq) 130 | .map(|(x, y)| Edit::Same(x, y)) 131 | .chain(lcs_base( 132 | c, 133 | itertools::put_back( 134 | x().rev() 135 | .skip(suffix_eq) 136 | .take(x_len.saturating_sub(prefix_eq + suffix_eq)), 137 | ), 138 | itertools::put_back( 139 | y().rev() 140 | .skip(suffix_eq) 141 | .take(y_len.saturating_sub(prefix_eq + suffix_eq)), 142 | ), 143 | )) 144 | .chain( 145 | x().skip(x_len - suffix_eq) 146 | .zip(y().skip(y_len - suffix_eq)) 147 | .map(|(x, y)| Edit::Same(x, y)), 148 | ) 149 | } 150 | 151 | // FIXME move out from lcs 152 | pub(crate) fn lcs_post_change<'a, T: Same + Diffable<'a> + ?Sized + 'a>( 153 | result: impl Iterator>, 154 | ) -> impl Iterator>::Diff>> { 155 | result.map(|edit| match edit { 156 | Edit::Same(left, right) => match left.diff(right) { 157 | edit::Edit::Copy(t) => edit::collection::Edit::Copy(t), 158 | edit::Edit::Change(diff) => edit::collection::Edit::Change(diff), 159 | }, 160 | Edit::Insert(value) => edit::collection::Edit::Insert(value), 161 | Edit::Remove(value) => edit::collection::Edit::Remove(value), 162 | }) 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use super::*; 168 | 169 | #[test] 170 | fn characters() { 171 | let left = "XMJYAUZ"; 172 | let right = "MZJAWXU"; 173 | 174 | let s = lcs( 175 | || left.chars(), 176 | || right.chars(), 177 | left.chars().count(), 178 | right.chars().count(), 179 | ); 180 | 181 | assert_eq!( 182 | s.collect::>(), 183 | vec![ 184 | Edit::Remove('X'), 185 | Edit::Same('M', 'M'), 186 | Edit::Insert('Z'), 187 | Edit::Same('J', 'J'), 188 | Edit::Remove('Y'), 189 | Edit::Same('A', 'A'), 190 | Edit::Insert('W'), 191 | Edit::Insert('X'), 192 | Edit::Same('U', 'U'), 193 | Edit::Remove('Z') 194 | ] 195 | ); 196 | } 197 | 198 | #[test] 199 | fn words() { 200 | let left = "The quick brown fox jumps over the lazy dog"; 201 | let right = "The quick brown dog leaps over the lazy cat"; 202 | 203 | let s = lcs( 204 | || left.split_whitespace(), 205 | || right.split_whitespace(), 206 | left.split_whitespace().count(), 207 | right.split_whitespace().count(), 208 | ); 209 | 210 | assert_eq!( 211 | s.collect::>(), 212 | vec![ 213 | Edit::Same("The", "The"), 214 | Edit::Same("quick", "quick"), 215 | Edit::Same("brown", "brown"), 216 | Edit::Remove("fox"), 217 | Edit::Remove("jumps"), 218 | Edit::Insert("dog"), 219 | Edit::Insert("leaps"), 220 | Edit::Same("over", "over"), 221 | Edit::Same("the", "the"), 222 | Edit::Same("lazy", "lazy"), 223 | Edit::Remove("dog"), 224 | Edit::Insert("cat") 225 | ] 226 | ); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /diffus/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod diffable_impls; 2 | pub mod edit; 3 | mod lcs; 4 | pub mod same; 5 | mod twodvec; 6 | 7 | pub trait Diffable<'a> { 8 | type Diff: 'a; 9 | 10 | fn diff(&'a self, other: &'a Self) -> edit::Edit<'a, Self>; 11 | } 12 | 13 | pub trait Same { 14 | fn same(&self, other: &Self) -> bool; 15 | } 16 | 17 | #[cfg(feature = "derive")] 18 | #[doc(hidden)] 19 | pub use diffus_derive::*; 20 | -------------------------------------------------------------------------------- /diffus/src/same.rs: -------------------------------------------------------------------------------- 1 | use crate::Same; 2 | 3 | impl Same for Option { 4 | fn same(&self, other: &Self) -> bool { 5 | match (self, other) { 6 | (Some(a), Some(b)) => a.same(b), 7 | (None, None) => true, 8 | _ => false, 9 | } 10 | } 11 | } 12 | 13 | macro_rules! same_for_eq { 14 | ($($typ:ty),*) => { 15 | $( 16 | impl Same for $typ { 17 | fn same(&self, other: &Self) -> bool { 18 | self == other 19 | } 20 | } 21 | )* 22 | } 23 | } 24 | 25 | same_for_eq! { i64, i32, i16, i8, u64, u32, u16, u8, char, str, bool, isize, usize, () } 26 | 27 | macro_rules! same_for_float { 28 | ($($typ:ty),*) => { 29 | $( 30 | impl Same for $typ { 31 | fn same(&self, other: &Self) -> bool { 32 | self.to_ne_bytes() == other.to_ne_bytes() 33 | } 34 | } 35 | )* 36 | } 37 | } 38 | 39 | same_for_float! { f32, f64 } 40 | 41 | #[cfg(feature = "snake_case-impl")] 42 | same_for_eq! { snake_case::SnakeCase } 43 | 44 | #[cfg(feature = "uuid-impl")] 45 | same_for_eq! { uuid::Uuid } 46 | 47 | impl Same for &T { 48 | fn same(&self, other: &Self) -> bool { 49 | (*self).same(*other) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /diffus/src/twodvec.rs: -------------------------------------------------------------------------------- 1 | pub(crate) struct TwoDVec { 2 | storage: Vec, 3 | width: usize, 4 | } 5 | 6 | impl TwoDVec { 7 | pub fn new(initial: T, width: usize, height: usize) -> Self { 8 | Self { 9 | storage: vec![initial; width * height], 10 | width, 11 | } 12 | } 13 | } 14 | 15 | impl TwoDVec { 16 | pub fn height(&self) -> usize { 17 | self.storage.len() / self.width 18 | } 19 | pub fn width(&self) -> usize { 20 | self.width 21 | } 22 | } 23 | 24 | impl std::ops::Index for TwoDVec { 25 | type Output = [T]; 26 | 27 | fn index(&self, index: usize) -> &Self::Output { 28 | &self.storage.as_slice()[self.width * index..][..self.width] 29 | } 30 | } 31 | 32 | impl std::ops::IndexMut for TwoDVec { 33 | fn index_mut(&mut self, index: usize) -> &mut Self::Output { 34 | &mut self.storage.as_mut_slice()[self.width * index..][..self.width] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # ./publish.sh 0.13.37 3 | set -o errexit -o nounset -o pipefail -o xtrace 4 | 5 | VERSION="$1" 6 | 7 | command -v jq > /dev/null 8 | 9 | cargo_publish () { 10 | ( 11 | cd $1 12 | 13 | cargo package 14 | echo "Files added:" 15 | cargo package --list 16 | 17 | read -r -p "Looks good to publish to crates.io? " response 18 | case "$response" in 19 | [yY][eE][sS]|[yY]) 20 | cargo publish 21 | ;; 22 | *) 23 | echo "Aborted" 24 | exit 5 25 | ;; 26 | esac 27 | ) 28 | } 29 | 30 | ( 31 | cd "$( dirname "${BASH_SOURCE[0]}" )" 32 | 33 | git fetch 34 | test -z "$(git status --porcelain)" || (echo "Dirty repo"; exit 2) 35 | test -z "$(git diff origin/master)" || (echo "Not up to date with origin/master"; exit 3) 36 | 37 | ./test.sh 38 | 39 | git fetch --tags 40 | git tag -l | sed '/^'"${VERSION}"'$/{q2}' > /dev/null \ 41 | || (echo "${VERSION} already exists"; exit 4) 42 | 43 | find . \ 44 | -iname Cargo.toml \ 45 | -not -path "./target/*" \ 46 | -exec sed -i 's/^version = .*$/version = "'"${VERSION}"'"/g' '{}' \; \ 47 | -exec sed -i 's/^\(diffus-derive = { version = "\)\(=[0-9]*\.[0-9]*\.[0-9]*\)\(".*\)$/\1='"${VERSION}"'\3/g' '{}' \; \ 48 | -exec git add '{}' \; 49 | 50 | git diff origin/master 51 | 52 | read -r -p "Deploying ${VERSION}, are you sure? [y/N]? " response 53 | case "$response" in 54 | [yY][eE][sS]|[yY]) 55 | git commit -m"Version ${VERSION}" 56 | git tag "${VERSION}" 57 | git push origin "${VERSION}" 58 | git push origin master 59 | cargo_publish diffus-derive 60 | until cargo_publish diffus; do 61 | printf '.' 62 | sleep 1 63 | done 64 | ;; 65 | *) 66 | git checkout . 67 | echo "Aborted" 68 | ;; 69 | esac 70 | ) 71 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cargo test 3 | cargo test --all-features 4 | 5 | cargo fmt -- --check 6 | --------------------------------------------------------------------------------