├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── examples ├── Example1.kt ├── Example2.kt ├── Example3.kt ├── Example4.kt ├── Example5.kt ├── Example6.kt └── Example7.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── kotlin │ └── net │ │ └── andreinc │ │ └── mapneat │ │ ├── config │ │ └── JsonPathConfiguration.kt │ │ ├── dsl │ │ └── MapNeat.kt │ │ ├── exceptions │ │ └── Exceptions.kt │ │ ├── experimental │ │ └── scripting │ │ │ └── KotlinScriptRunner.kt │ │ ├── model │ │ ├── MapNeatObjectMap.kt │ │ └── MapNeatSource.kt │ │ └── operation │ │ ├── Assign.kt │ │ ├── Copy.kt │ │ ├── Delete.kt │ │ ├── Move.kt │ │ ├── Shift.kt │ │ └── abstract │ │ ├── MappingOperation.kt │ │ └── Operation.kt └── resources │ └── log4j2.xml └── test ├── kotlin └── net │ └── andreinc │ └── mapneat │ ├── MapNeatTest.kt │ ├── operation │ ├── assign │ │ └── AssignTests.kt │ ├── copy │ │ └── CopyTests.kt │ ├── delete │ │ └── DeleteTests.kt │ ├── move │ │ └── MoveTests.kt │ └── shift │ │ └── ShiftTests.kt │ └── other │ ├── config │ └── JsonPathConfigProvidedTest.kt │ ├── parent │ └── ParentTests.kt │ ├── root │ └── RootTests.kt │ └── xmlsource │ └── XmlSourceTests.kt └── resources ├── assign ├── hierarchy │ ├── source.json │ └── target.json ├── jsoninjson │ ├── source.json │ └── target.json ├── lambda │ ├── source.json │ └── target.json ├── re_assign │ ├── source.json │ └── target.json ├── root │ ├── source.json │ └── target.json └── simple │ ├── source.json │ └── target.json ├── config ├── source.json └── target.json ├── copy └── simple │ ├── source.json │ └── target.json ├── delete └── simple │ ├── source.json │ └── target.json ├── move └── simple │ ├── source.json │ └── target.json ├── parent └── simple │ ├── source.json │ └── target.json ├── root ├── source.json └── target.json ├── shift ├── lenient │ ├── source.json │ └── target.json └── simple │ ├── source.json │ └── target.json └── xmlsource └── simple ├── source.xml └── target.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "03:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: org.jetbrains.kotlin:kotlin-maven-plugin 11 | versions: 12 | - 1.4.21-2 13 | - dependency-name: org.jetbrains.kotlin:kotlin-reflect 14 | versions: 15 | - 1.4.21-2 16 | - dependency-name: org.jetbrains.kotlin:kotlin-scripting-jvm 17 | versions: 18 | - 1.4.21-2 19 | - dependency-name: org.jetbrains.kotlin:kotlin-scripting-jvm-host 20 | versions: 21 | - 1.4.21-2 22 | - dependency-name: org.jetbrains.kotlin:kotlin-stdlib 23 | versions: 24 | - 1.4.21-2 25 | - dependency-name: com.fasterxml.jackson.core:jackson-databind 26 | versions: 27 | - 2.12.1 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /target/ 3 | /mapneat.iml 4 | 5 | logs/ 6 | 7 | ### Gradle ### 8 | .gradle 9 | **/build/ 10 | !src/**/build/ 11 | 12 | # Ignore Gradle GUI config 13 | gradle-app.setting 14 | 15 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 16 | !gradle-wrapper.jar 17 | 18 | # Cache of project 19 | .gradletasknamecache 20 | 21 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 22 | # gradle/wrapper/gradle-wrapper.properties 23 | 24 | ### Gradle Patch ### 25 | **/build/ 26 | 27 | # End of https://www.toptal.com/developers/gitignore/api/gradle -------------------------------------------------------------------------------- /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 [2020] [Andrei Ciobanu] 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 | **MapNeat** is a JVM library written in Kotlin that provides an easy to use DSL (*Domain Specific Language*) for transforming JSON to JSON, XML to JSON, POJO to JSON in a declarative way. 2 | 3 | No intermediary POJOs are needed. Given's Kotlin high-interoperability **MapNeat** can be used in a Java project without any particular hassle. Check the documentation for examples on how to do that. 4 | 5 | Under the hood **MapNeat** is using: 6 | * [jackson](https://github.com/FasterXML/jackson) and [json-path](https://github.com/json-path/JsonPath) for JSON querying and processing; 7 | * [JSON In Java](https://github.com/stleary/JSON-java) for converting from XML to JSON; 8 | * [JSONAssert](http://jsonassert.skyscreamer.org/) for making JSON assertions (testing purposes). 9 | 10 | Blog articles: 11 | * [Hello world, mapneat!](https://www.andreinc.net/2021/01/31/hello-world-mapneat) 12 | * [XML to JSON using mapneat](https://www.andreinc.net/2021/02/01/xml-to-json-using-mapneat) 13 | 14 | # Table of contents 15 | 16 | * [Getting started](#getting-started) 17 | * [How it works](#how-it-works) 18 | * [A typical transformation](#a-typical-transformation) 19 | * [Operations](#operations) 20 | * [Assign](#assign-) 21 | * [Shift](#shift-) 22 | * [Copy](#copy-) 23 | * [Move](#move-) 24 | * [Delete](#delete--) 25 | * [Using Mapneat from Java](#using-mapneat-from-java) 26 | * [Logging](#logging) 27 | * [Contributions and Roadmap](#contributing-and-roadmap) 28 | 29 | # Getting Started 30 | 31 | For newver version (>=0.9.5): 32 | 33 | ```xml 34 | 35 | net.andreinc 36 | mapneat 37 | 1.0.0 38 | 39 | ``` 40 | 41 | ``` 42 | implementation 'net.andreinc:mapneat:1.0.0` 43 | ``` 44 | 45 | > For security issues, please update to `1.0.0`. 46 | 47 | # How it works 48 | 49 | The library will transform any JSON, XML or POJO into another JSON, without the need of intermediary POJO classes. 50 | 51 | Every operation applied to the source JSON (the input) is declarative in nature, and involves significantly less code than writing everything by hand. 52 | 53 | Normally, a transformation has the following structure: 54 | 55 | ```kotlin 56 | val jsonValue : String = "..." 57 | val transformedJson = json(fromJson(jsonValue)) { 58 | /* operation1 */ 59 | /* operation2 */ 60 | /* operation3 */ 61 | /* conditional block */ 62 | /* operation4 */ 63 | }.getPrettyString() // Transformed output 64 | ``` 65 | 66 | If the source is XML, `fromXML(xmlValue: String)`can be used. In this case the `xmlValue` is automatically converted to JSON using [JSON In Java](https://github.com/stleary/JSON-java). 67 | 68 | If the source is a POJO, `fromObject(object)` can be used. In this case the `object` is automatically converted to JSON using jackson. 69 | 70 | # A typical transformation 71 | 72 | A typical transformation looks like this: 73 | 74 | JSON1: 75 | ```json 76 | { 77 | "id": 380557, 78 | "first_name": "Gary", 79 | "last_name": "Young", 80 | "photo": "http://srcimg.com/100/150", 81 | "married": false, 82 | "visits" : [ 83 | { 84 | "country" : "Romania", 85 | "date" : "2020-10-10" 86 | }, 87 | { 88 | "country" : "Romania", 89 | "date" : "2019-07-21" 90 | }, 91 | { 92 | "country" : "Italy", 93 | "date" : "2019-12-21" 94 | }, 95 | { 96 | "country" : "France", 97 | "date" : "2019-02-21" 98 | } 99 | ] 100 | } 101 | ``` 102 | 103 | JSON2: 104 | ```json 105 | { 106 | "citizenship" : [ "Romanian", "French" ] 107 | } 108 | ``` 109 | 110 | We write the **MapNeat** transformation like: 111 | 112 | ```kotlin 113 | fun main() { 114 | // JSON1 and JSON2 are both String variables 115 | val transform = json(fromJson(JSON1)) { 116 | 117 | "person.id" /= 100 118 | "person.firstName" *= "$.first_name" 119 | "person.lastName" *= "$.last_name" 120 | 121 | // We can using a nested json assignment instead of using the "." notation 122 | "person.meta" /= json { 123 | "information1" /= "ABC" 124 | "information2" /= "ABC2" 125 | } 126 | 127 | // We can assign a value from a lambda expression 128 | "person.maritalStatus" /= { 129 | if(sourceCtx().read("$.married")) 130 | "married" 131 | else 132 | "unmarried" 133 | } 134 | 135 | "person.visited" *= { 136 | // We select only the country name from the visits array 137 | expression = "$.visits[*].country" 138 | processor = { countries -> 139 | // We don't allow duplications so we create a Set 140 | (countries as List).toMutableSet() 141 | } 142 | } 143 | 144 | // We add a new country using the "[+]" notation 145 | "person.visited[+]" /= "Ireland" 146 | 147 | // We merge another array into the visited[] array 148 | "person.visited[++]" /= mutableListOf("Israel", "Japan") 149 | 150 | // We look into a secondary json source - JSON2 151 | // Assigning the citizenship array to a temporary path (person._tmp.array) 152 | "person._tmp" /= json(fromJson(JSON2)) { 153 | "array" *= "$.citizenship" 154 | } 155 | 156 | // We copy the content of temporary array into the path were we want to keep it 157 | "person._tmp.array" % "person.citizenships" 158 | 159 | // We remove the temporary path 160 | - "person._tmp" 161 | 162 | // We rename "citizenships" to "citizenship" because we don't like typos 163 | "person.citizenships" %= "person.citizenship" 164 | } 165 | 166 | println(transform) 167 | } 168 | ``` 169 | 170 | After all the operations are performed step by step, the output looks like this: 171 | 172 | ```json 173 | { 174 | "person" : { 175 | "id" : 100, 176 | "firstName" : "Gary", 177 | "lastName" : "Young", 178 | "meta" : { 179 | "information1" : "ABC", 180 | "information2" : "ABC2" 181 | }, 182 | "maritalStatus" : "unmarried", 183 | "visited" : [ "Romania", "Italy", "France", "Ireland", "Israel", "Japan" ], 184 | "citizenship" : [ "Romanian", "French" ] 185 | } 186 | } 187 | ``` 188 | 189 | # Operations 190 | 191 | In the previous example you might wonder what the operators `/=`, `*=`, `%`, `%=`, `-` are doing. 192 | 193 | Those are actually shortcuts methods for the operations we are performing: 194 | 195 | | Operator | Operation | Description | 196 | | :-------- | :-------- | :-------- | 197 | | `/=` | `assign` | Assigns a given constant or a value computed in lambda expression to a certain path in the target JSON (the result). | 198 | | `*=` | `shift` | Shifts a portion from the source JSON based on a JSON Path expression. | 199 | | `%` | `copy` | Copies a path from the target JSON to another another path. | 200 | | `%=` | `move` | Moves a path from the target JSON to another path. | 201 | | `-` | `delete` | Deletes a path from the target JSON. | 202 | 203 | Additionally, the paths from the target JSON can be "decorated" with "array notation": 204 | 205 | | Array Notation | Description | 206 | | :------- | :------- | 207 | | `path[]` | A `new` array will be created through the `assign` and `shift` operations. | 208 | | `path[+]` | An `append` will be performed through the `assign` and `shift` operations. | 209 | | `path[++]` | A `merge` will be performed through the `assign` and `shift` operations. | 210 | 211 | If you prefer, instead of using the operators you can use their equivalent methods. 212 | 213 | For example: 214 | 215 | ``` 216 | "person.name" /= "Andrei" 217 | ``` 218 | 219 | Can be written as: 220 | 221 | ``` 222 | "person.name" assign "Andrei" 223 | ``` 224 | 225 | Or 226 | 227 | ``` 228 | "person.name" *= "$.user.full_name" 229 | ``` 230 | 231 | Can be written as: 232 | 233 | ``` 234 | "person.name" shift "$.user.full_name" 235 | ``` 236 | 237 | Personally, I prefer the operator notation (`/=`, `*=`, etc.), but some people consider the methods (`assign`, `shift`) more readable. 238 | 239 | For the rest of the examples the operator notation will be used. 240 | 241 | ## Assign (`/=`) 242 | 243 | The **Assign** Operation is used to assign a value to a path in the resulting JSON (target). 244 | 245 | The value can be a constant object, or a lambda (`()-> Any`). 246 | 247 | Example: 248 | 249 | ```kotlin 250 | package net.andreinc.mapneat.examples 251 | 252 | import net.andreinc.mapneat.dsl.json 253 | 254 | const val A_SRC_1 = """ 255 | { 256 | "id": 380557, 257 | "first_name": "Gary", 258 | "last_name": "Young" 259 | } 260 | """ 261 | 262 | const val A_SRC_2 = """ 263 | { 264 | "photo": "http://srcimg.com/100/150", 265 | "married": false 266 | } 267 | """ 268 | 269 | fun main() { 270 | val transformed = json(A_SRC_1) { 271 | // Assigning a constant 272 | "user.user_name" /= "neo2020" 273 | 274 | // Assigning value from a lambda expression 275 | "user.first_name" /= { sourceCtx().read("$.first_name") } 276 | 277 | // Assigning value from another JSON source 278 | "more_info" /= json(A_SRC_2) { 279 | "married" /= { sourceCtx().read("$.married") } 280 | } 281 | 282 | // Assigning an inner JSON with the same source as the parent 283 | "more_info2" /= json { 284 | "last_name" /= { sourceCtx().read("$.last_name") } 285 | } 286 | } 287 | println(transformed) 288 | } 289 | ``` 290 | 291 | Output: 292 | ```json 293 | { 294 | "user" : { 295 | "user_name" : "neo2020", 296 | "first_name" : "Gary" 297 | }, 298 | "more_info" : { 299 | "married" : false 300 | }, 301 | "more_info2" : { 302 | "last_name" : "Young" 303 | } 304 | } 305 | ``` 306 | 307 | In the lambda method we pass to the `/=` operation we have access to: 308 | * `sourceCtx()` which represents the `ReadContext` of the source. We can use this to read JSON Paths just like in the example above; 309 | * `targetCtx()` which represents the `ReacContext` of the target. This is calculated each time we call the method. So, it contains only the changes that were made up until that point. In most cases this shouldn't be called. 310 | 311 | In case we are using an inner JSON structure, we also have reference to the parent source and target contexts: 312 | * `parent.sourceCtx()` 313 | * `parent.targetCtx()` 314 | 315 | `parent()` returns a nullable value, so it needs to be used adding `!!` (double bang). 316 | 317 | ``` 318 | ... { 319 | "something" /= "Something Value" 320 | "person" /= json { 321 | "innerSomething" /= { parent()!!.targetCtx().read("$.something") } 322 | } 323 | } 324 | ``` 325 | 326 | For more information about `ReadContext`s please check [json-path](https://github.com/json-path/JsonPath)'s documentation. 327 | 328 | The **Assign** operation can also be used in conjunction with left-side array notations (`[]`, `[+]`, `[++]`): 329 | 330 | ```kotlin 331 | fun main() { 332 | val transformed = json("{}") { 333 | println("Simple array creation:") 334 | "a" /= 1 335 | "b" /= 1 336 | println(this) 337 | 338 | println("Adds a new value in the array:") 339 | "a[+]" /= 2 340 | "b[+]" /= true 341 | println(this) 342 | 343 | println("Merge in an existing array:") 344 | "b[++]" /= arrayOf("a", "b", "c") 345 | println(this) 346 | } 347 | } 348 | ``` 349 | 350 | Output: 351 | ``` 352 | Simple array creation: 353 | { 354 | "a" : 1, 355 | "b" : 1 356 | } 357 | Adds a new value in the array 358 | { 359 | "a" : [ 1, 2 ], 360 | "b" : [ 1, true ] 361 | } 362 | Merge in an existing array: 363 | { 364 | "a" : [ 1, 2 ], 365 | "b" : [ 1, true, "a", "b", "c" ] 366 | } 367 | ``` 368 | 369 | ## Shift (`*=`) 370 | 371 | The **Shift** operation is very similar to the *Assign* operation, but it provides an easier way to query the source JSON using [json-path](https://github.com/json-path/JsonPath). 372 | 373 | Example: 374 | 375 | ```kotlin 376 | package net.andreinc.mapneat.examples 377 | 378 | import net.andreinc.mapneat.dsl.json 379 | import java.time.LocalDate 380 | import java.time.format.DateTimeFormatter 381 | 382 | val JSON_VAL = """ 383 | { 384 | "id": 380557, 385 | "first_name": "Gary", 386 | "last_name": "Young", 387 | "photo": "http://srcimg.com/100/150", 388 | "married": false, 389 | "visits" : [ 390 | { 391 | "country" : "Romania", 392 | "date" : "2020-10-10" 393 | }, 394 | { 395 | "country" : "Romania", 396 | "date" : "2019-07-21" 397 | }, 398 | { 399 | "country" : "Italy", 400 | "date" : "2019-12-21" 401 | }, 402 | { 403 | "country" : "France", 404 | "date" : "2019-02-21" 405 | } 406 | ] 407 | } 408 | """ 409 | 410 | fun main() { 411 | val transformed = json(JSON_VAL) { 412 | "user.name.first" *= "$.first_name" 413 | // We use an additional processor to capitalise the last Name 414 | "user.name.last" *= { 415 | expression = "$.last_name" 416 | processor = { (it as String).toUpperCase() } 417 | } 418 | // We add the photo directly into an array 419 | "user.photos[]" *= "$.photo" 420 | // We don't allow duplicates 421 | "user.visits.countries" *= { 422 | expression = "$.visits[*].country" 423 | processor = { (it as MutableList).toSet().toMutableList() } 424 | } 425 | // We keep only the last visit 426 | "user.visits.lastVisit" *= { 427 | expression = "$.visits[*].date" 428 | processor = { 429 | (it as MutableList) 430 | .stream() 431 | .map { LocalDate.parse(it, DateTimeFormatter.ISO_DATE) } 432 | .max(LocalDate::compareTo) 433 | .get() 434 | .toString() 435 | } 436 | } 437 | } 438 | 439 | println(transformed) 440 | } 441 | ``` 442 | 443 | Output: 444 | 445 | ```json 446 | { 447 | "user" : { 448 | "name" : { 449 | "first" : "Gary", 450 | "last" : "YOUNG" 451 | }, 452 | "photos" : [ "http://srcimg.com/100/150" ], 453 | "visits" : { 454 | "countries" : [ "Romania", "Italy", "France" ], 455 | "lastVisit" : "2020-10-10" 456 | } 457 | } 458 | } 459 | ``` 460 | 461 | As you can see in the above example, each expression can be accompanied with an additional processor method that allows developers to refine the results provided by the JSON path expression. 462 | 463 | Similar to the **Assign** lambdas, `sourceCtx()`, `targetCtx()`, `parent!!.sourceCtx()`, `parent!!.targetCtx()` are also available to the method context and can be used. 464 | 465 | If you want to `Shift` all the source JSON into the target you can use the following transformation: 466 | 467 | ``` 468 | "" *= "$ 469 | ``` 470 | 471 | Or call the `copySourceToTarget()` method directly. 472 | 473 | In case a field is optional, and you don't want automatically fail the mapping, you can use the `leniency` property: 474 | 475 | ```kotlin 476 | "books" *= { 477 | expression = "$.store.broken.path" 478 | lenient = true 479 | } 480 | ``` 481 | 482 | ## Copy (`%`) 483 | 484 | The **Copy** Operation moves a certain path from the target JSON to another path in the target JSON. 485 | 486 | Example: 487 | 488 | ```kotlin 489 | package net.andreinc.mapneat.examples 490 | 491 | import net.andreinc.mapneat.dsl.json 492 | 493 | fun main() { 494 | val transformed = json("{}") { 495 | "some.long.path" /= mutableListOf("A, B, C") 496 | "some.long.path" % "copy" 497 | println(this) 498 | } 499 | } 500 | ``` 501 | 502 | Output: 503 | 504 | ```json 505 | { 506 | "some" : { 507 | "long" : { 508 | "path" : [ "A, B, C" ] 509 | } 510 | }, 511 | "copy" : [ "A, B, C" ] 512 | } 513 | ``` 514 | 515 | ## Move (`%=`) 516 | 517 | The **Move** operation moves a certain path from the target JSON to a new path in the target JSON. 518 | 519 | Example: 520 | 521 | ```kotlin 522 | package net.andreinc.mapneat.examples 523 | 524 | import net.andreinc.mapneat.dsl.json 525 | 526 | fun main() { 527 | json("{}") { 528 | "array" /= intArrayOf(1,2,3) 529 | "array" %= "a.b.c.d" 530 | println(this) 531 | } 532 | } 533 | ``` 534 | 535 | Output: 536 | 537 | ```json 538 | { 539 | "a" : { 540 | "b" : { 541 | "c" : { 542 | "d" : [ 1, 2, 3 ] 543 | } 544 | } 545 | } 546 | } 547 | ``` 548 | 549 | ## Delete (`-`) 550 | 551 | The **Delete** operation deletes a certain path from the target JSON. 552 | 553 | Example: 554 | 555 | ```kotlin 556 | package net.andreinc.mapneat.examples 557 | 558 | import net.andreinc.mapneat.dsl.json 559 | 560 | fun main() { 561 | json("{}") { 562 | "a.b.c" /= mutableListOf(1,2,3,4,true) 563 | "a.b.d" /= "a" 564 | // deletes the array from "a.b.c" 565 | - "a.b.c" 566 | println(this) 567 | } 568 | } 569 | ``` 570 | 571 | Output: 572 | 573 | ```json 574 | { 575 | "a" : { 576 | "b" : { 577 | "d" : "a" 578 | } 579 | } 580 | } 581 | ``` 582 | 583 | # Using **MapNeat** from Java 584 | 585 | Given Kotlin's high level of interoperability with Java, **MapNeat** can be used in any Java application. 586 | 587 | The DSL file should remain kotlin, but it can be called from any Java program, as simple as: 588 | 589 | ```kotlin 590 | @file : JvmName("Sample") 591 | 592 | package kotlinPrograms 593 | 594 | import net.andreinc.mapneat.dsl.json 595 | 596 | fun personTransform(input: String) : String { 597 | return json(input) { 598 | "person.name" /= "Andrei" 599 | "person.age" /= 13 600 | }.getPrettyString() 601 | } 602 | ``` 603 | 604 | The java file: 605 | 606 | ```java 607 | import static kotlinPrograms.Sample.personTransform; 608 | 609 | public class Main { 610 | public static void main(String[] args) { 611 | // personTransform(String) is the method from Kotlin 612 | String person = personTransform("{}"); 613 | System.out.println(person); 614 | } 615 | } 616 | ``` 617 | 618 | PS: Configuring the Java application to be Kotlin-enabled it's quite simple, usually IntelliJ is doing this automatically without amy developer intervention. 619 | 620 | # Logging 621 | 622 | The library uses log4j2 for logging purposes. 623 | 624 | Each transformation gets logged by default to `SYSTEM_OUT`and to `logs/mapneat.log`. 625 | 626 | For tracing and debugging purposes transformations have two IDs (id, parentId - if inner JSONs are used). 627 | 628 | E.g.: 629 | 630 | ``` 631 | 19:05:06.204 [main] INFO net.andreinc.mapneat.dsl.MapNeat - Transformation(id=a739ba94-dedd-4d5b-bd09-03b30693a1ae, parentId=null) INPUT = { 632 | "books" : [ 633 | { 634 | "title" : "Cool dog", 635 | "author" : "Mike Smith" 636 | }, 637 | { 638 | "title": "Feeble Cat", 639 | "author": "John Cibble" 640 | }, 641 | { 642 | "title": "Morning Horse", 643 | "author": "Kohn Gotcha" 644 | } 645 | ], 646 | "address" : { 647 | "country" : "RO", 648 | "street_number": 123, 649 | "city": "Bucharest" 650 | } 651 | } 652 | 19:05:06.209 [main] INFO net.andreinc.mapneat.dsl.MapNeat - Transformation(id=a739ba94-dedd-4d5b-bd09-03b30693a1ae, parentId=a739ba94-dedd-4d5b-bd09-03b30693a1ae) INPUT = INHERITED 653 | 19:05:06.244 [main] INFO net.andreinc.mapneat.operation.Assign - (transformationId=a739ba94-dedd-4d5b-bd09-03b30693a1ae) "fullName" ASSIGN(/=) "John Cibble" 654 | 19:05:06.246 [main] INFO net.andreinc.mapneat.operation.Assign - (transformationId=a739ba94-dedd-4d5b-bd09-03b30693a1ae) "firstName" ASSIGN(/=) "John" 655 | 19:05:06.246 [main] INFO net.andreinc.mapneat.operation.Assign - (transformationId=a739ba94-dedd-4d5b-bd09-03b30693a1ae) "lastName" ASSIGN(/=) "Cibble" 656 | 19:05:06.248 [main] INFO net.andreinc.mapneat.operation.Delete - (transformationId=a739ba94-dedd-4d5b-bd09-03b30693a1ae) DELETE(-) "fullName" 657 | ``` 658 | 659 | `(transformationId=a739ba94-dedd-4d5b-bd09-03b30693a1ae)` => represents the id 660 | `Transformation(id=a739ba94-dedd-4d5b-bd09-03b30693a1ae, parentId=a739ba94-dedd-4d5b-bd09-03b30693a1ae) INPUT = INHERITED` => marks the parentId 661 | 662 | # Contributing and Roadmap 663 | 664 | The highlevel roadmap for the library at this moment is: 665 | 1. Make mapneat a command-line tool 666 | 2. Create a mapneat-server to serve transformation sync / async 667 | 668 | Anyone if free to contribute. You know how github works:). 669 | 670 | 671 | ---------------- 672 | 673 | For more code examples, please check: https://github.com/nomemory/mapneat-examples 674 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.4.21' 3 | } 4 | 5 | apply plugin: 'java' 6 | apply plugin: 'idea' 7 | apply plugin: 'maven' 8 | apply plugin: 'signing' 9 | 10 | group = "net.andreinc" 11 | version = "1.0.0" 12 | archivesBaseName = "mapneat" 13 | 14 | sourceCompatibility = 1.8 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | dependencies { 21 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 22 | implementation "org.jetbrains.kotlin:kotlin-scripting-jvm-host" 23 | implementation "org.jetbrains.kotlin:kotlin-scripting-jvm" 24 | implementation "org.jetbrains.kotlin:kotlin-reflect" 25 | implementation "com.jayway.jsonpath:json-path:2.5.0" 26 | implementation "org.json:json:20201115" 27 | implementation "com.fasterxml.jackson.core:jackson-databind:2.12.6.1" 28 | implementation "org.apache.logging.log4j:log4j-api-kotlin:1.0.0" 29 | implementation "org.apache.logging.log4j:log4j-api:2.15.0" 30 | implementation "org.apache.logging.log4j:log4j-core:2.15.0" 31 | 32 | testImplementation "org.junit.jupiter:junit-jupiter-engine:5.7.1" 33 | testImplementation "org.skyscreamer:jsonassert:1.5.0" 34 | } 35 | 36 | test { 37 | useJUnitPlatform() 38 | } 39 | 40 | task javadocJar(type: Jar) { 41 | classifier = 'javadoc' 42 | from javadoc 43 | } 44 | 45 | task sourcesJar(type: Jar) { 46 | classifier = 'sources' 47 | from sourceSets.main.allSource 48 | } 49 | 50 | artifacts { 51 | archives javadocJar, sourcesJar 52 | } 53 | 54 | signing { 55 | sign configurations.archives 56 | } 57 | 58 | uploadArchives { 59 | repositories { 60 | mavenDeployer { 61 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 62 | 63 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2") { 64 | authentication(userName: ossrhUsername, password: ossrhPassword) 65 | } 66 | 67 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots") { 68 | authentication(userName: ossrhUsername, password: ossrhPassword) 69 | } 70 | 71 | pom.project { 72 | name 'mapneat' 73 | packaging 'jar' 74 | description "JSON to JSON and XML to JSON transformation library" 75 | url 'https://github.com/nomemory/mapneat' 76 | 77 | scm { 78 | connection 'https://github.com/nomemory/mapneat' 79 | developerConnection 'https://github.com/nomemory/mapneat' 80 | url 'https://github.com/nomemory/mapneat' 81 | } 82 | 83 | licenses { 84 | license { 85 | name 'The Apache License, Version 2.0' 86 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 87 | } 88 | } 89 | 90 | developers { 91 | developer { 92 | id 'nomemory' 93 | name 'nomemory' 94 | email 'gnomemory@yahoo.com' 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /examples/Example1.kt: -------------------------------------------------------------------------------- 1 | import net.andreinc.mapneat.dsl.json 2 | import net.andreinc.mapneat.model.MapNeatSource.Companion.fromJson 3 | 4 | val JSON1 = """ 5 | { 6 | "id": 380557, 7 | "first_name": "Gary", 8 | "last_name": "Young", 9 | "photo": "http://srcimg.com/100/150", 10 | "married": false, 11 | "visits" : [ 12 | { 13 | "country" : "Romania", 14 | "date" : "2020-10-10" 15 | }, 16 | { 17 | "country" : "Romania", 18 | "date" : "2019-07-21" 19 | }, 20 | { 21 | "country" : "Italy", 22 | "date" : "2019-12-21" 23 | }, 24 | { 25 | "country" : "France", 26 | "date" : "2019-02-21" 27 | } 28 | ] 29 | } 30 | """.trimIndent() 31 | 32 | val JSON2 = """ 33 | { 34 | "citizenship" : [ "Romanian", "French" ] 35 | } 36 | """.trimIndent() 37 | 38 | fun main() { 39 | val transform = json(fromJson(JSON1)) { 40 | 41 | "person.id" /= 100 42 | "person.firstName" *= "$.first_name" 43 | "person.lastName" *= "$.last_name" 44 | 45 | // We can using a nested assignment instead of using the "." notationa 46 | "person.meta" /= json { 47 | "information1" /= "ABC" 48 | "information2" /= "ABC2" 49 | } 50 | 51 | "person.maritalStatus" /= { 52 | if(sourceCtx().read("$.married")) "married" else "unmarried" 53 | } 54 | 55 | "person.visited" *= { 56 | // We select only the country name 57 | expression = "$.visits[*].country" 58 | processor = { countries -> 59 | // We don't allow duplications so we create a Set 60 | (countries as List).toMutableSet() 61 | } 62 | } 63 | 64 | // We add a new country using the "[+]" notation 65 | "person.visited[+]" /= "Ireland" 66 | 67 | // We merge another array into the visited[] array 68 | "person.visited[++]" /= mutableListOf("Israel", "Japan") 69 | 70 | // We look into a secondary json source - JSON2 71 | // Assigning the citizenship array to a temporary path (person._tmp) 72 | "person._tmp" /= json(fromJson(JSON2)) { 73 | "array" *= "$.citizenship" 74 | } 75 | 76 | // We copy the temporary array into the path were we want to keep it 77 | "person._tmp.array" % "person.citizenships" 78 | 79 | // We remove the temporary path 80 | 81 | - "person._tmp" 82 | // We rename "citizenships" to "citizenship" 83 | "person.citizenships" %= "person.citizenship" 84 | } 85 | 86 | println(transform) 87 | } -------------------------------------------------------------------------------- /examples/Example2.kt: -------------------------------------------------------------------------------- 1 | import net.andreinc.mapneat.dsl.json 2 | 3 | const val A_SRC_1 = """ 4 | { 5 | "id": 380557, 6 | "first_name": "Gary", 7 | "last_name": "Young" 8 | } 9 | """ 10 | 11 | const val A_SRC_2 = """ 12 | { 13 | "photo": "http://srcimg.com/100/150", 14 | "married": false 15 | } 16 | """ 17 | 18 | fun main() { 19 | val transformed = json(A_SRC_1) { 20 | // Assigning a constant 21 | "user.user_name" /= "neo2020" 22 | 23 | // Assigning value from a lambda expression 24 | "user.first_name" /= { sourceCtx().read("$.first_name") } 25 | 26 | // Assigning value from another JSON source 27 | "more_info" /= json(A_SRC_2) { 28 | "married" /= { sourceCtx().read("$.married") } 29 | } 30 | 31 | // Assigning an inner JSON with the same source as the parent 32 | "more_info2" /= json { 33 | "last_name" /= { sourceCtx().read("$.last_name") } 34 | } 35 | } 36 | println(transformed) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /examples/Example3.kt: -------------------------------------------------------------------------------- 1 | import net.andreinc.mapneat.dsl.json 2 | 3 | fun main() { 4 | val transformed = json("{}") { 5 | println("Simple array creation:") 6 | "a" /= 1 7 | "b" /= 1 8 | println(this) 9 | 10 | println("Adds a new value in the array:") 11 | "a[+]" /= 2 12 | "b[+]" /= true 13 | println(this) 14 | 15 | println("Merge in an existing array:") 16 | "b[++]" /= arrayOf("a", "b", "c") 17 | println(this) 18 | } 19 | } -------------------------------------------------------------------------------- /examples/Example4.kt: -------------------------------------------------------------------------------- 1 | import net.andreinc.mapneat.dsl.json 2 | import java.time.LocalDate 3 | import java.time.format.DateTimeFormatter 4 | 5 | val JSON_VAL = """ 6 | { 7 | "id": 380557, 8 | "first_name": "Gary", 9 | "last_name": "Young", 10 | "photo": "http://srcimg.com/100/150", 11 | "married": false, 12 | "visits" : [ 13 | { 14 | "country" : "Romania", 15 | "date" : "2020-10-10" 16 | }, 17 | { 18 | "country" : "Romania", 19 | "date" : "2019-07-21" 20 | }, 21 | { 22 | "country" : "Italy", 23 | "date" : "2019-12-21" 24 | }, 25 | { 26 | "country" : "France", 27 | "date" : "2019-02-21" 28 | } 29 | ] 30 | } 31 | """ 32 | 33 | fun main() { 34 | val transformed = json(JSON_VAL) { 35 | "user.name.first" *= "$.first_name" 36 | "user.name.last" *= { 37 | expression = "$.last_name" 38 | processor = { (it as String).toUpperCase() } 39 | } 40 | "user.photos[]" *= "$.photo" 41 | "user.visits.countries" *= { 42 | expression = "$.visits[*].country" 43 | processor = { (it as MutableList).toSet().toMutableList() } 44 | } 45 | "user.visits.lastVisit" *= { 46 | expression = "$.visits[*].date" 47 | processor = { 48 | (it as MutableList) 49 | .stream() 50 | .map { LocalDate.parse(it, DateTimeFormatter.ISO_DATE) } 51 | .max(LocalDate::compareTo) 52 | .get() 53 | .toString() 54 | } 55 | } 56 | } 57 | 58 | println(transformed) 59 | } -------------------------------------------------------------------------------- /examples/Example5.kt: -------------------------------------------------------------------------------- 1 | import net.andreinc.mapneat.dsl.json 2 | 3 | fun main() { 4 | val transformed = json("{}") { 5 | "some.long.path" /= mutableListOf("A, B, C") 6 | "some.long.path" % "copy" 7 | 8 | println(this) 9 | } 10 | } -------------------------------------------------------------------------------- /examples/Example6.kt: -------------------------------------------------------------------------------- 1 | import net.andreinc.mapneat.dsl.json 2 | 3 | fun main() { 4 | json("{}") { 5 | "array" /= intArrayOf(1,2,3) 6 | "array" %= "a.b.c.d" 7 | println(this) 8 | } 9 | } -------------------------------------------------------------------------------- /examples/Example7.kt: -------------------------------------------------------------------------------- 1 | import net.andreinc.mapneat.dsl.json 2 | 3 | fun main() { 4 | json("{}") { 5 | "a.b.c" /= mutableListOf(1,2,3,4,true) 6 | "a.b.d" /= "a" 7 | - "a.b.c" 8 | println(this) 9 | } 10 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomemory/mapneat/2a387364c90740c1c03d4e2017623062d7c8a1af/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mapneat' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/config/JsonPathConfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.config 2 | 3 | import com.jayway.jsonpath.Configuration 4 | import com.jayway.jsonpath.spi.json.JacksonJsonProvider 5 | 6 | /** 7 | * MapNeat is using for the moment the JacksonJSONProvider as the json provider for the library. 8 | * 9 | * Changing the implementation to something else (e.g.): gson will change the behavior of the 10 | * Shift Operation and everything using JsonPath functionality 11 | * 12 | */ 13 | object JsonPathConfiguration { 14 | val mapNeatConfiguration : Configuration = Configuration 15 | .builder() 16 | .jsonProvider(JacksonJsonProvider()) 17 | .build() 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/dsl/MapNeat.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.dsl 2 | 3 | import com.jayway.jsonpath.Configuration 4 | import net.andreinc.mapneat.config.JsonPathConfiguration 5 | import net.andreinc.mapneat.model.MapNeatObjectMap 6 | import net.andreinc.mapneat.model.MapNeatSource 7 | import net.andreinc.mapneat.operation.* 8 | import org.apache.logging.log4j.kotlin.Logging 9 | import java.util.* 10 | import kotlin.math.exp 11 | 12 | /** 13 | * This is the starting point of the DSL. 14 | * 15 | * The MapNeat class extends MapNeatObjectMap which holds internal representation of a JSON as a MutableMap 16 | */ 17 | class MapNeat(val inputJson: String, val parentObject: MapNeat? = null, val transformationId : String = UUID.randomUUID().toString(), private val jsonPathConfig: Configuration = JsonPathConfiguration.mapNeatConfiguration) : MapNeatObjectMap(inputJson, jsonPathConfig), Logging { 18 | 19 | fun hasParent() : Boolean { 20 | return parentObject != null 21 | } 22 | 23 | /** 24 | * This represents a reference of a possible parent JSON. 25 | */ 26 | fun parent() : MapNeat? { 27 | return this.parentObject 28 | } 29 | 30 | init { 31 | // All the operations associated with a transformation will be logged using the same ID 32 | // for an easier tracking inside the logs 33 | val printInput = if (inputJson == parent()?.inputJson) "INHERITED" else inputJson 34 | logger.info { "Transformation(id=$transformationId, parentId=${parentObject?.transformationId}) INPUT = $printInput"} 35 | } 36 | 37 | private constructor(source: MapNeatSource) : this(source.getStringContent()) 38 | 39 | infix operator fun String.divAssign(constantValue: Any) { 40 | Assign(sourceCtx, targetMap, transformationId).apply { 41 | fullFieldPath = this@divAssign 42 | value = constantValue 43 | }.doOperation() 44 | } 45 | 46 | infix operator fun String.divAssign(acc: AssignOperationMethod) { 47 | Assign(sourceCtx, targetMap, transformationId).apply { 48 | fullFieldPath = this@divAssign 49 | method = acc 50 | }.doOperation() 51 | } 52 | 53 | infix fun String.assign(constantValue: Any) { 54 | Assign(sourceCtx, targetMap, transformationId).apply { 55 | fullFieldPath = this@assign 56 | value = constantValue 57 | }.doOperation() 58 | } 59 | 60 | infix fun String.assign(acc: AssignOperationMethod) { 61 | Assign(sourceCtx, targetMap, transformationId).apply { 62 | fullFieldPath = this@assign 63 | method = acc 64 | }.doOperation() 65 | } 66 | 67 | // Shift transformation DSL methods 68 | 69 | infix operator fun String.timesAssign(value: String) { 70 | Shift(sourceCtx, targetMap, transformationId).apply { 71 | jsonPath = JsonPathQuery().apply { 72 | expression = value 73 | } 74 | fullFieldPath = this@timesAssign 75 | }.doOperation() 76 | } 77 | 78 | infix operator fun String.timesAssign(value: JsonPathQuery.() -> Unit) { 79 | Shift(sourceCtx, targetMap, transformationId).apply { 80 | jsonPath = JsonPathQuery().apply(value) 81 | fullFieldPath = this@timesAssign 82 | }.doOperation() 83 | } 84 | 85 | infix fun String.shift(value: String) { 86 | Shift(sourceCtx, targetMap, transformationId).apply { 87 | jsonPath = JsonPathQuery().apply { 88 | expression = value 89 | } 90 | fullFieldPath = this@shift 91 | }.doOperation() 92 | } 93 | 94 | infix fun String.shift(value: JsonPathQuery.() -> Unit) { 95 | Shift(sourceCtx, targetMap, transformationId).apply { 96 | jsonPath = JsonPathQuery().apply(value) 97 | fullFieldPath = this@shift 98 | }.doOperation() 99 | } 100 | 101 | // Delete transformation DSL methods 102 | 103 | fun delete(value: String) { 104 | Delete(sourceCtx, targetMap, transformationId) 105 | .apply { 106 | fullFieldPath = value 107 | }.doOperation() 108 | } 109 | 110 | operator fun String.unaryMinus() { 111 | Delete(sourceCtx, targetMap, transformationId) 112 | .apply { 113 | fullFieldPath = this@unaryMinus 114 | } 115 | .doOperation() 116 | } 117 | 118 | // Move transformation DSL methods 119 | 120 | infix operator fun String.remAssign(value : String) { 121 | Move(sourceCtx, targetMap, transformationId) 122 | .apply { 123 | this.fullFieldPath = this@remAssign 124 | this.newField = value 125 | } 126 | .doOperation() 127 | } 128 | 129 | infix fun String.move(value: String) { 130 | Move(sourceCtx, targetMap, transformationId) 131 | .apply { 132 | this.fullFieldPath = this@move 133 | this.newField = value 134 | } 135 | .doOperation() 136 | } 137 | 138 | // Copy transformation DSL methods 139 | 140 | infix fun String.copy(value: String) { 141 | Copy(sourceCtx, targetMap, transformationId).apply{ 142 | fullFieldPath = this@copy 143 | destination = value 144 | }.doOperation() 145 | } 146 | 147 | infix operator fun String.rem(value: String) { 148 | Copy(sourceCtx, targetMap, transformationId).apply { 149 | fullFieldPath = this@rem 150 | destination = value 151 | }.doOperation() 152 | } 153 | 154 | fun copySourceToTarget() { 155 | Shift(sourceCtx, targetMap, transformationId).apply { 156 | jsonPath = JsonPathQuery().apply { 157 | expression = "$" 158 | } 159 | fullFieldPath = "" 160 | }.doOperation() 161 | } 162 | 163 | override fun toString(): String { 164 | return getPrettyString() 165 | } 166 | 167 | fun json(init: MapNeat.() -> Unit) : Map { 168 | return MapNeat(super.source, this, transformationId, jsonPathConfig) 169 | .apply(init) 170 | .getObjectMap() 171 | } 172 | 173 | fun json(json: String, init: MapNeat.() -> Unit) : Map { 174 | return MapNeat(json, this, transformationId, jsonPathConfig) 175 | .apply(init) 176 | .getObjectMap() 177 | } 178 | 179 | fun json(source: MapNeatSource, init: MapNeat.() -> Unit): Map { 180 | return MapNeat(source.content, this, transformationId, jsonPathConfig) 181 | .apply(init) 182 | .getObjectMap() 183 | } 184 | 185 | } 186 | 187 | inline fun json(json: String, init: MapNeat.() -> Unit) : MapNeat { 188 | return MapNeat(json) 189 | .apply(init) 190 | } 191 | 192 | inline fun json(json: String, jsonPathConfig: Configuration, init: MapNeat.() -> Unit) : MapNeat { 193 | return MapNeat(json, jsonPathConfig = jsonPathConfig) 194 | .apply(init) 195 | } 196 | 197 | inline fun json(source: MapNeatSource, init: MapNeat.() -> Unit) : MapNeat { 198 | return MapNeat(source.content) 199 | .apply(init) 200 | } 201 | 202 | inline fun json(source: MapNeatSource, jsonPathConfig: Configuration, init: MapNeat.() -> Unit) : MapNeat { 203 | return MapNeat(source.content, jsonPathConfig = jsonPathConfig) 204 | .apply(init) 205 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/exceptions/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.exceptions 2 | 3 | import java.lang.RuntimeException 4 | 5 | abstract class MapNeatException(message: String) : RuntimeException(message) 6 | 7 | class FieldAlreadyExistsAndNotAnObject(fieldName: String, fullPath: List) : 8 | MapNeatException("Cannot create path '${fullPath.joinToString("/")}/${fieldName}'. Field already exists and it's not an object.") 9 | 10 | class JsonPathNotInitialized(target: String) : 11 | MapNeatException("Cannot execute operation, the jsonPath object is not initialised for the '${target}'") 12 | 13 | class AssignOperationNotInitialized(fieldName: String) : 14 | MapNeatException("Assign Operation is not initialized properly for field: '${fieldName}'. Either 'method' or 'value' needs to be initialized first.") 15 | 16 | class MoveOperationNotInitialized(fieldName: String) : 17 | MapNeatException("Move Operation is not initialized properly for field: '${fieldName}'. The target 'newField' needs to be initialised first.") 18 | 19 | class CopyOperationNotInitialized(fieldName: String) : 20 | MapNeatException("Copy Operation is not initialized properly for field: '${fieldName}. The target 'destination' needs to be initialised first.") 21 | 22 | class OperationFieldIsNotInitialized : 23 | MapNeatException("Operation is not initialized properly. 'field' needs to be initialized.") 24 | 25 | class CannotMergeNonIterableElement(fieldName: String) : 26 | MapNeatException("Cannot merge non-iterable value in '${fieldName}'.") 27 | -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/experimental/scripting/KotlinScriptRunner.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.experimental.scripting 2 | 3 | import net.andreinc.mapneat.experimental.scripting.KotlinScriptRunner.evalAsString 4 | import org.apache.logging.log4j.kotlin.Logging 5 | import kotlin.reflect.KClass 6 | import kotlin.script.experimental.api.* 7 | import kotlin.script.experimental.host.toScriptSource 8 | import kotlin.script.experimental.jvm.dependenciesFromCurrentContext 9 | import kotlin.script.experimental.jvm.jvm 10 | import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost 11 | 12 | data class ProvidedProperty(val name: String, val type: KClass<*>, val value: Any?) { 13 | constructor(name: String, type: Class<*>, value: Any?) : this(name, type.kotlin, value) 14 | } 15 | 16 | /** 17 | * Experimental feature, run kotlin transformations from .kts scripts 18 | */ 19 | object KotlinScriptRunner : Logging { 20 | 21 | fun eval(sourceCode: SourceCode, props: List): ResultWithDiagnostics { 22 | 23 | val compileConfig = ScriptCompilationConfiguration { 24 | jvm { 25 | dependenciesFromCurrentContext(wholeClasspath = true) 26 | defaultImports("net.andreinc.mapneat.dsl.*") 27 | } 28 | providedProperties(*(props.map { it.name to KotlinType(it.type) }.toTypedArray())) 29 | } 30 | val evaluationConfig = ScriptEvaluationConfiguration { 31 | providedProperties(*(props.map { it.name to it.value }.toTypedArray())) 32 | } 33 | 34 | return BasicJvmScriptingHost().eval(sourceCode, compileConfig, evaluationConfig) 35 | } 36 | 37 | fun evalAsString(sourceCode: SourceCode, props: List) : Any? { 38 | return eval(sourceCode, props).valueOrThrow().returnValue 39 | } 40 | } 41 | 42 | //fun main() { 43 | // 44 | // val script = """ 45 | // "ABC" + aValue 46 | // """ 47 | // 48 | // val props1 = listOf( 49 | // ProvidedProperty("aValue", Int::class, 3) 50 | // ) 51 | // 52 | // val props2 = listOf( 53 | // ProvidedProperty("aValue", Int::class, 4) 54 | // ) 55 | // 56 | // repeat(1) { 57 | // println(evalAsString(script.toScriptSource(), props2)) 58 | // } 59 | //} -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/model/MapNeatObjectMap.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.model 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.jayway.jsonpath.Configuration 5 | import com.jayway.jsonpath.JsonPath 6 | import com.jayway.jsonpath.ReadContext 7 | import net.andreinc.mapneat.config.JsonPathConfiguration.mapNeatConfiguration 8 | 9 | open class MapNeatObjectMap (val source: String, private val jsonPathConfig : Configuration = mapNeatConfiguration) { 10 | 11 | protected val targetMap = LinkedHashMap() 12 | protected val sourceCtx: ReadContext = JsonPath.using(jsonPathConfig).parse(source) 13 | 14 | fun sourceCtx() : ReadContext { 15 | return this.sourceCtx 16 | } 17 | 18 | fun targetCtx() : ReadContext { 19 | return JsonPath.using(mapNeatConfiguration).parse(targetMap) 20 | } 21 | 22 | fun getObjectMap() : Map { 23 | return targetMap 24 | } 25 | 26 | fun getPrettyString() : String { 27 | return ObjectMapper() 28 | .writerWithDefaultPrettyPrinter() 29 | .writeValueAsString(targetMap) 30 | } 31 | 32 | fun getString() : String { 33 | return ObjectMapper().writeValueAsString(targetMap) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/model/MapNeatSource.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.model 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.json.XML 6 | 7 | class MapNeatSource { 8 | 9 | lateinit var content : String 10 | 11 | companion object { 12 | 13 | fun fromXml(xmlValue: String) : MapNeatSource { 14 | return MapNeatSource().apply { 15 | this.content = XML.toJSONObject(xmlValue).toString() 16 | } 17 | } 18 | 19 | fun fromJson(jsonValue: String) : MapNeatSource { 20 | return MapNeatSource().apply { 21 | this.content = jsonValue 22 | } 23 | } 24 | 25 | fun fromObject(obj: Any) : MapNeatSource { 26 | val map = ObjectMapper().convertValue(obj, Map::class.java) 27 | val json = ObjectMapper().writer().writeValueAsString(map) 28 | return fromJson(json) 29 | } 30 | } 31 | 32 | fun getStringContent() : String { 33 | return this.content 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/operation/Assign.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | 5 | import net.andreinc.mapneat.exceptions.AssignOperationNotInitialized 6 | import net.andreinc.mapneat.model.MapNeatObjectMap 7 | import net.andreinc.mapneat.operation.abstract.MappingOperation 8 | import net.andreinc.mapneat.operation.abstract.Operation 9 | import org.apache.logging.log4j.kotlin.Logging 10 | 11 | typealias AssignOperationMethod = () -> Any 12 | 13 | class Assign(sourceCtx: ReadContext, targetMapRef: MutableMap, transformationId : String) : 14 | Operation(sourceCtx, targetMapRef, transformationId), 15 | MappingOperation, 16 | Logging { 17 | 18 | var value : Any? = null 19 | lateinit var method: AssignOperationMethod 20 | 21 | override fun doOperation() { 22 | onSelectedField { current, fieldContext -> 23 | doMappingOperation(current, fieldContext) 24 | val content = writer.writeValueAsString(current[fieldContext.name]) 25 | logger.info { "(transformationId=${transformationId}) \"${fullFieldPath}\" ASSIGN(/=) $content" } 26 | } 27 | } 28 | 29 | override fun getMappedValue(): Any? { 30 | return if (value != null) { 31 | if (value!! is MapNeatObjectMap) { 32 | (value!! as MapNeatObjectMap).getObjectMap() 33 | } else { 34 | value 35 | } 36 | } else if (this::method.isInitialized) { 37 | try { 38 | method() 39 | } catch (ex: NullPointerException) { 40 | null 41 | } 42 | } else if (value == null) { 43 | null 44 | } 45 | else { 46 | throw AssignOperationNotInitialized(fullFieldPath) 47 | } 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/operation/Copy.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | import net.andreinc.mapneat.exceptions.CopyOperationNotInitialized 5 | import net.andreinc.mapneat.operation.abstract.Operation 6 | import net.andreinc.mapneat.operation.abstract.StructuralOperation 7 | import org.apache.logging.log4j.kotlin.Logging 8 | 9 | class Copy(sourceCtx: ReadContext, targetMapRef: MutableMap, transformationId : String) : 10 | Operation(sourceCtx, targetMapRef, transformationId), 11 | StructuralOperation, 12 | Logging { 13 | 14 | lateinit var destination: String 15 | 16 | override fun doOperation() { 17 | 18 | if (!this::destination.isInitialized) { 19 | throw CopyOperationNotInitialized(fullFieldPath) 20 | } 21 | 22 | onSelectedField { current, fieldContext -> 23 | val toBeCopied = current.getOrDefault(fieldContext.name, "") 24 | onField(destination) { newCurrent, newFieldContext -> 25 | newCurrent[newFieldContext.name] = toBeCopied 26 | logger.info { "(transformationId=$transformationId) \"${destination}\" COPY(%) ${writer.writeValueAsString(toBeCopied)}"} 27 | } 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/operation/Delete.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | import net.andreinc.mapneat.operation.abstract.Operation 5 | import net.andreinc.mapneat.operation.abstract.StructuralOperation 6 | import org.apache.logging.log4j.kotlin.Logging 7 | 8 | /** 9 | * A transformation that deletes a certain field and all of it's children 10 | */ 11 | class Delete(sourceCtx: ReadContext, mapReference: MutableMap, transformationId : String) : 12 | Operation(sourceCtx, mapReference, transformationId), 13 | StructuralOperation, 14 | Logging { 15 | override fun doOperation() { 16 | onSelectedField { current, fieldContext -> 17 | current.remove(fieldContext.name) 18 | logger.info { "(transformationId=$transformationId) DELETE(-) \"${fullFieldPath}\""} 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/operation/Move.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | import net.andreinc.mapneat.exceptions.MoveOperationNotInitialized 5 | import net.andreinc.mapneat.operation.abstract.Operation 6 | import net.andreinc.mapneat.operation.abstract.StructuralOperation 7 | import org.apache.logging.log4j.kotlin.Logging 8 | 9 | /** 10 | * A transformation that moves a branch from one place to another inside the target output 11 | * It can be also used to rename fields 12 | */ 13 | class Move(sourceCtx: ReadContext, targetMapRef: MutableMap, transformationId : String) : 14 | Operation(sourceCtx, targetMapRef, transformationId), 15 | StructuralOperation, 16 | Logging { 17 | 18 | lateinit var newField : String 19 | 20 | override fun doOperation() { 21 | 22 | if (!this::newField.isInitialized) { 23 | throw MoveOperationNotInitialized(fullFieldPath) 24 | } 25 | 26 | onSelectedField { current, fieldContext -> 27 | if (current.containsKey(fieldContext.name)) { 28 | 29 | val toBeMoved = current.getOrDefault(fieldContext.name, "") 30 | 31 | Assign(sourceCtx(), targetMapRef, transformationId).apply { 32 | this.fullFieldPath = newField 33 | this.value = toBeMoved 34 | }.doOperation() 35 | 36 | Delete(sourceCtx(), targetMapRef, transformationId).apply { 37 | this.fullFieldPath = super.fullFieldPath 38 | }.doOperation() 39 | 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/operation/Shift.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | import net.andreinc.mapneat.exceptions.JsonPathNotInitialized 5 | import net.andreinc.mapneat.operation.abstract.MappingOperation 6 | import net.andreinc.mapneat.operation.abstract.Operation 7 | import org.apache.logging.log4j.kotlin.Logging 8 | import java.lang.Exception 9 | 10 | class Shift(sourceCtx: ReadContext, targetMapRef: MutableMap, transformationId : String) : 11 | Operation(sourceCtx, targetMapRef, transformationId), 12 | MappingOperation, 13 | Logging { 14 | 15 | lateinit var jsonPath : JsonPathQuery 16 | 17 | override fun doOperation() { 18 | onSelectedField { current, fieldContext -> 19 | doMappingOperation(current, fieldContext) 20 | logger.info { "(transformationId=$transformationId) \"${fullFieldPath}\" SHIFT(*=) ${writer.writeValueAsString(current[fieldContext.name])}" } 21 | } 22 | } 23 | 24 | override fun getMappedValue(): Any? { 25 | if (!this::jsonPath.isInitialized) 26 | throw JsonPathNotInitialized(fullFieldPath) 27 | val result = if (!this.jsonPath.lenient) { 28 | sourceCtx().read(jsonPath.expression) 29 | } else { 30 | try { 31 | sourceCtx().read(jsonPath.expression) 32 | } catch (e: Exception) { 33 | "" 34 | } 35 | } 36 | return if (result != null) 37 | this.jsonPath.processor(result) 38 | else null 39 | } 40 | } 41 | 42 | class JsonPathQuery { 43 | lateinit var expression : String 44 | var lenient : Boolean = false 45 | var processor : (input: Any) -> Any = { it } 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/operation/abstract/MappingOperation.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation.abstract 2 | 3 | import net.andreinc.mapneat.exceptions.CannotMergeNonIterableElement 4 | 5 | enum class ArrayChange (val isAffecting: Boolean) { 6 | NEW(true), // some.field[] 7 | APPEND(true), // some.field[+] 8 | MERGE(true), // some.field[++] 9 | NONE(false) // some.field 10 | } 11 | 12 | interface MappingOperation { 13 | 14 | fun getMappedValue(): Any? 15 | 16 | fun doMappingOperation(current: MutableMap, fieldContext: FieldContext) { 17 | MappingAction(current, fieldContext, getMappedValue()) 18 | .doAction() 19 | } 20 | } 21 | 22 | @Suppress("UNCHECKED_CAST") 23 | class MappingAction (val current: MutableMap, val fieldContext: FieldContext, val mappedValue: Any?) { 24 | 25 | private var field: String = fieldContext.name 26 | private var arrayChange: ArrayChange = fieldContext.arrayChange 27 | 28 | private fun createField() : MappingAction { 29 | if (!current.containsKey(field)) { 30 | if (arrayChange.isAffecting) { 31 | current[field] = mutableListOf() 32 | } 33 | } 34 | return this 35 | } 36 | 37 | // Goes to the field and transforms any potential Iterable values into "friendly" mutable lists. 38 | // I have to do this, in order to prevent error-prone Assign Operations with immutable iterables. 39 | private fun iterableToMutableList() : MappingAction { 40 | 41 | if (current[field] is Iterable<*> && current[field] !is MutableList<*>) { 42 | val result = mutableListOf() 43 | result.addAll(current[field] as Iterable) 44 | current[field] = result 45 | } 46 | 47 | when(current[field]) { 48 | is Array<*> -> current[field] = (current[field] as Array).toMutableList() 49 | is ByteArray -> current[field] = (current[field] as ByteArray).toMutableList() 50 | is CharArray -> current[field] = (current[field] as CharArray).toMutableList() 51 | is ShortArray -> current[field] = (current[field] as ShortArray).toMutableList() 52 | is IntArray -> current[field] = (current[field] as IntArray).toMutableList() 53 | is LongArray -> current[field] = (current[field] as LongArray).toMutableList() 54 | is DoubleArray -> current[field] = (current[field] as DoubleArray).toMutableList() 55 | is FloatArray -> current[field] = (current[field] as FloatArray).toMutableList() 56 | is BooleanArray -> current[field] = (current[field] as BooleanArray).toMutableList() 57 | } 58 | 59 | return this 60 | } 61 | 62 | // Transforms current one in a mutable list if needed 63 | private fun currentAsMutableList() : MutableList { 64 | if (!current.containsKey(field)) { 65 | // If the current path doesn't exist, create an empty MutableList 66 | current[field] = mutableListOf() 67 | } 68 | else { 69 | if (current[field] !is Iterable<*> && 70 | current[field] !is Array<*> && 71 | current[field] !is ByteArray && 72 | current[field] !is ShortArray && 73 | current[field] !is IntArray && 74 | current[field] !is IntArray && 75 | current[field] !is LongArray && 76 | current[field] !is DoubleArray && 77 | current[field] !is FloatArray && 78 | current[field] !is BooleanArray) { 79 | // If there's not an iterable, but a single field, we create a 80 | // one element mutable list 81 | current[field] = mutableListOf(current[field] as Any) 82 | } 83 | } 84 | return current[field] as MutableList 85 | } 86 | 87 | // Maps a certain value on the selected field from the Field Context 88 | private fun mapValue() : MappingAction { 89 | when(arrayChange) { 90 | ArrayChange.NONE -> { 91 | if (field == "" && mappedValue is Map<*, *>) { 92 | (mappedValue as Map).forEach { 93 | current[it.key] = it.value 94 | } 95 | } 96 | else { 97 | current[field] = mappedValue 98 | } 99 | } 100 | ArrayChange.NEW -> current[field] = mutableListOf(mappedValue) 101 | ArrayChange.APPEND -> currentAsMutableList().add(mappedValue) 102 | ArrayChange.MERGE -> { 103 | when(mappedValue) { 104 | // If we need to merge an iterable, we just merge the values in a 105 | // the new mutable list we've created 106 | is Array<*> -> currentAsMutableList().addAll(mappedValue as Array) 107 | is ByteArray -> currentAsMutableList().addAll((mappedValue).toList()) 108 | is CharArray -> currentAsMutableList().addAll(mappedValue.toList()) 109 | is ShortArray -> currentAsMutableList().addAll(mappedValue.toList()) 110 | is IntArray -> currentAsMutableList().addAll((mappedValue).toList()) 111 | is LongArray -> currentAsMutableList().addAll(mappedValue.toList()) 112 | is DoubleArray -> currentAsMutableList().addAll(mappedValue.toList()) 113 | is FloatArray -> currentAsMutableList().addAll(mappedValue.toList()) 114 | is BooleanArray -> currentAsMutableList().addAll(mappedValue.toList()) 115 | is Iterable<*> -> currentAsMutableList().addAll(mappedValue as Iterable) 116 | else -> throw CannotMergeNonIterableElement(field) 117 | } 118 | } 119 | } 120 | return this 121 | } 122 | 123 | fun doAction() { 124 | this.createField() 125 | .iterableToMutableList() 126 | .mapValue() 127 | } 128 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/mapneat/operation/abstract/Operation.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation.abstract 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.databind.ObjectWriter 5 | import com.jayway.jsonpath.JsonPath 6 | import com.jayway.jsonpath.ReadContext 7 | import net.andreinc.mapneat.exceptions.FieldAlreadyExistsAndNotAnObject 8 | import net.andreinc.mapneat.exceptions.OperationFieldIsNotInitialized 9 | import java.lang.ClassCastException 10 | import java.util.* 11 | import kotlin.collections.LinkedHashMap 12 | 13 | class FieldContext(val path: List, val name: String, val arrayChange: ArrayChange) 14 | typealias FieldAction = (MutableMap, FieldContext) -> Unit 15 | 16 | abstract class Operation(private val sourceCtx: ReadContext, val targetMapRef: MutableMap, val transformationId : String = UUID.randomUUID().toString()) { 17 | 18 | companion object { 19 | // Mainly used for logging purposes 20 | val writer : ObjectWriter = ObjectMapper() 21 | .writer() 22 | .withDefaultPrettyPrinter() 23 | } 24 | 25 | // The "left-side" field that is affected by the operation 26 | lateinit var fullFieldPath : String 27 | 28 | // The method that is actually performing the operation 29 | abstract fun doOperation() 30 | 31 | // The Source Context (The JSON where for example we apply JSON Paths) 32 | fun sourceCtx(): ReadContext { 33 | return this.sourceCtx 34 | } 35 | 36 | // The Target Context 37 | fun targetCtx(): ReadContext { 38 | return JsonPath.parse(targetMapRef) 39 | } 40 | 41 | fun onField(fullFieldPath: String, action: FieldAction) { 42 | val fieldContext = getFieldContext(fullFieldPath) 43 | val fullPath = fieldContext.path 44 | val fieldName = fieldContext.name 45 | var current = targetMapRef 46 | 47 | for (e in fullPath) { 48 | try { 49 | if (!current.containsKey(e)) { 50 | current[e] = LinkedHashMap() 51 | } 52 | current = current[e] as MutableMap 53 | } catch (ex: ClassCastException) { 54 | throw FieldAlreadyExistsAndNotAnObject(fieldName, fullPath) 55 | } 56 | } 57 | 58 | action(current, fieldContext) 59 | } 60 | 61 | fun onSelectedField(action: FieldAction) { 62 | 63 | if (!this::fullFieldPath.isInitialized) { 64 | throw OperationFieldIsNotInitialized() 65 | } 66 | 67 | onField(fullFieldPath, action) 68 | } 69 | 70 | //TODO escape "., [, +, ]" 71 | private fun getFieldContext(fieldRawValue: String): FieldContext { 72 | 73 | val elements = fieldRawValue.split(".") 74 | val path = elements.dropLast(1) 75 | val name: String 76 | val arrayChange: ArrayChange 77 | 78 | when { 79 | elements.last().endsWith("[]") -> { 80 | name = elements.last().dropLast(2) 81 | arrayChange = ArrayChange.NEW 82 | } 83 | elements.last().endsWith("[+]") -> { 84 | name = elements.last().dropLast(3) 85 | arrayChange = ArrayChange.APPEND 86 | } 87 | elements.last().endsWith("[++]") -> { 88 | name = elements.last().dropLast(4) 89 | arrayChange = ArrayChange.MERGE 90 | } 91 | else -> { 92 | name = elements.last() 93 | arrayChange = ArrayChange.NONE 94 | } 95 | } 96 | 97 | return FieldContext(path, name, arrayChange) 98 | } 99 | } 100 | 101 | interface StructuralOperation -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/MapNeatTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.databind.ObjectWriter 5 | import com.jayway.jsonpath.Configuration 6 | import net.andreinc.mapneat.config.JsonPathConfiguration 7 | import net.andreinc.mapneat.dsl.MapNeat 8 | import net.andreinc.mapneat.model.MapNeatSource.Companion.fromJson 9 | import net.andreinc.mapneat.model.MapNeatSource.Companion.fromXml 10 | import org.skyscreamer.jsonassert.JSONAssert 11 | 12 | open class MapNeatTest(private val source: String, private val expected: String, private val config: Configuration, private val init: MapNeat.() -> Unit) { 13 | 14 | private val writer : ObjectWriter = ObjectMapper().writerWithDefaultPrettyPrinter() 15 | 16 | companion object { 17 | 18 | const val SOURCE_XML : String = "/source.xml" 19 | const val SOURCE_JSON : String = "/source.json" 20 | const val EXPECTED_JSON : String = "/target.json" 21 | 22 | fun testFromDirectory(dirName: String, config: Configuration = JsonPathConfiguration.mapNeatConfiguration, xmlSource : Boolean = false, dslInit: MapNeat.() -> Unit) : MapNeatTest { 23 | val sourceContent = readFile(dirName + if (xmlSource) SOURCE_XML else SOURCE_JSON) 24 | val source = (if (xmlSource) fromXml(sourceContent) else fromJson(sourceContent)).content 25 | val expected = readFile(dirName + EXPECTED_JSON) 26 | return MapNeatTest(source, expected, config, dslInit) 27 | } 28 | 29 | private fun readFile(fileName: String) : String { 30 | return javaClass.classLoader?.getResource(fileName)!!.readText() 31 | } 32 | } 33 | 34 | private fun compareExpectedWithActual() { 35 | val actualObject = MapNeat(source, jsonPathConfig = config).apply(init).getObjectMap() 36 | val actualJson = writer.writeValueAsString(actualObject) 37 | JSONAssert.assertEquals(actualJson, expected, true) 38 | } 39 | 40 | fun doTest() { 41 | compareExpectedWithActual() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/operation/assign/AssignTests.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation.assign 2 | 3 | import net.andreinc.mapneat.MapNeatTest.Companion.testFromDirectory 4 | import org.junit.jupiter.api.Test 5 | 6 | class AssignTests { 7 | @Test 8 | fun `Constant assignments are working correctly`() { 9 | testFromDirectory("assign/simple") { 10 | "name" /= "Andrei" 11 | "age" /= 15 12 | "isDisabled" /= true 13 | "array1" /= mutableListOf(1, 2, 3) 14 | "array2" /= arrayOf(1, 2, 3) 15 | "array3" /= setOf("a", "b", "c") 16 | }.doTest() 17 | } 18 | 19 | @Test 20 | fun `Constant assignments are working correctly (assign method)`() { 21 | testFromDirectory("assign/simple") { 22 | "name" assign "Andrei" 23 | "age" assign 15 24 | "isDisabled" assign true 25 | "array1" assign mutableListOf(1, 2, 3) 26 | "array2" assign arrayOf(1, 2, 3) 27 | "array3" assign setOf("a", "b", "c") 28 | }.doTest() 29 | } 30 | 31 | @Test 32 | fun `Constant assignments and re-assignments are working correctly`() { 33 | testFromDirectory("assign/re_assign") { 34 | "name" /= "Agent" 35 | "name" /= "Smith" 36 | "name" /= "Neo" 37 | "oneTime" /= 1 38 | "oneTime" /= 2 39 | "oneTime" /= "3" 40 | }.doTest() 41 | } 42 | 43 | @Test 44 | fun `Constant assignments and re-assignments are working correctly (assign method)`() { 45 | testFromDirectory("assign/re_assign") { 46 | "name" assign "Agent" 47 | "name" assign "Smith" 48 | "name" assign "Neo" 49 | "oneTime" assign 1 50 | "oneTime" assign 2 51 | "oneTime" assign "3" 52 | }.doTest() 53 | } 54 | 55 | @Test 56 | fun `Constant assignments deep hierarchy are working correctly`() { 57 | testFromDirectory("assign/hierarchy") { 58 | "a.b.c.d.e.f.g.h" /= "10" 59 | "a.b.c.d.e.f.g.i" /= "20" 60 | "a.b.c.d.e.f.g.j" /= "20" 61 | "a.b.c.d.e.f.g.k" /= "20" 62 | }.doTest() 63 | } 64 | 65 | @Test 66 | fun `Constant assignments deep hierarchy are working correctly (assign method)`() { 67 | testFromDirectory("assign/hierarchy") { 68 | "a.b.c.d.e.f.g.h" assign "10" 69 | "a.b.c.d.e.f.g.i" assign "20" 70 | "a.b.c.d.e.f.g.j" assign "20" 71 | "a.b.c.d.e.f.g.k" assign "20" 72 | }.doTest() 73 | } 74 | 75 | @Test 76 | fun `Lambda assignments are working correctly`() { 77 | testFromDirectory("assign/lambda") { 78 | "some1" /= { sourceCtx().read("$.someValue") } 79 | "some2" /= { sourceCtx().read("$.some.val1") } 80 | "some3" /= { sourceCtx().read("$.some.val2") } 81 | "some4" /= { 82 | mutableListOf( 83 | targetCtx().read("$.some1"), 84 | sourceCtx().read("$.some.val1"), 85 | sourceCtx().read("$.some.val2") 86 | ) 87 | } 88 | }.doTest() 89 | } 90 | 91 | @Test 92 | fun `Lambda assignments are working correctly (assign method)`() { 93 | testFromDirectory("assign/lambda") { 94 | "some1" assign { sourceCtx().read("$.someValue") } 95 | "some2" assign { sourceCtx().read("$.some.val1") } 96 | "some3" assign { sourceCtx().read("$.some.val2") } 97 | "some4" assign { 98 | mutableListOf( 99 | targetCtx().read("$.some1"), 100 | sourceCtx().read("$.some.val1"), 101 | sourceCtx().read("$.some.val2") 102 | ) 103 | } 104 | }.doTest() 105 | } 106 | 107 | @Test 108 | fun `Assignments are working correctly with inner JSONs and lambdas`() { 109 | testFromDirectory("assign/jsoninjson") { 110 | "author" /= json { 111 | "fullName" /= { sourceCtx().read("$.books[1].author") } 112 | "firstName" /= { targetCtx().read("$.fullName").split(" ")[0] } 113 | "lastName" /= { targetCtx().read("$.fullName").split(" ")[1] } 114 | - "fullName" 115 | "book" /= { sourceCtx().read("$.books[1].title") } 116 | "address" /= json { 117 | "country" /= "UK" 118 | } 119 | } 120 | }.doTest() 121 | } 122 | 123 | @Test 124 | fun `Assignments are working correctly with inner JSONs and lambdas (assign method)`() { 125 | testFromDirectory("assign/jsoninjson") { 126 | "author" assign json { 127 | "fullName" assign { sourceCtx().read("$.books[1].author") } 128 | "firstName" assign { targetCtx().read("$.fullName").split(" ")[0] } 129 | "lastName" assign { targetCtx().read("$.fullName").split(" ")[1] } 130 | - "fullName" 131 | "book" assign { sourceCtx().read("$.books[1].title") } 132 | "address" assign json { 133 | "country" /= "UK" 134 | } 135 | } 136 | }.doTest() 137 | } 138 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/operation/copy/CopyTests.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation.copy 2 | 3 | import net.andreinc.mapneat.MapNeatTest 4 | import org.junit.jupiter.api.Test 5 | 6 | class CopyTests { 7 | @Test 8 | fun `Copying hierarchies is working properly`() { 9 | MapNeatTest.testFromDirectory("copy/simple") { 10 | "store" *= "$.store" 11 | "store.book" % "store.books" 12 | }.doTest() 13 | } 14 | 15 | @Test 16 | fun `Copying hierarchies is working properly (copy method)`() { 17 | MapNeatTest.testFromDirectory("copy/simple") { 18 | "store" *= "$.store" 19 | "store.book" copy "store.books" 20 | }.doTest() 21 | } 22 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/operation/delete/DeleteTests.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation.delete 2 | 3 | import net.andreinc.mapneat.MapNeatTest.Companion.testFromDirectory 4 | import org.junit.jupiter.api.Test 5 | 6 | class DeleteTests { 7 | 8 | @Test 9 | fun `Deleting fields are working properly`() { 10 | testFromDirectory("delete/simple") { 11 | "books" *= "$.books" 12 | "address" *= "$.address" 13 | - "address" 14 | }.doTest() 15 | } 16 | 17 | @Test 18 | fun `Deleting fields are working properly (delete method)`() { 19 | testFromDirectory("delete/simple") { 20 | "books" *= "$.books" 21 | "address" *= "$.address" 22 | delete("address") 23 | }.doTest() 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/operation/move/MoveTests.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation.move 2 | 3 | import net.andreinc.mapneat.MapNeatTest.Companion.testFromDirectory 4 | import org.junit.jupiter.api.Test 5 | 6 | class MoveTests { 7 | 8 | @Test 9 | fun `Moving hierarchies is working properly`() { 10 | testFromDirectory("move/simple") { 11 | "store" *= "$.store" 12 | "store.book" %= "store.books" 13 | }.doTest() 14 | } 15 | 16 | @Test 17 | fun `Moving hierarchies is working properly (move method)`() { 18 | testFromDirectory("move/simple") { 19 | "store" *= "$.store" 20 | "store.book" move "store.books" 21 | }.doTest() 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/operation/shift/ShiftTests.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.operation.shift 2 | 3 | import net.andreinc.mapneat.MapNeatTest.Companion.testFromDirectory 4 | import org.junit.jupiter.api.Test 5 | 6 | class ShiftTests { 7 | 8 | @Test 9 | fun `Simple shifts are working properly` () { 10 | testFromDirectory("shift/simple") { 11 | "books" *= "$.store.book[*].title" 12 | "books[+]" /= "Game of Thrones" 13 | "genre" *= { 14 | expression = "$.store.book[*].category" 15 | processor = { books -> 16 | (books as MutableList).toSet().toMutableList() 17 | } 18 | } 19 | }.doTest() 20 | } 21 | 22 | @Test 23 | fun `Simple shifts are working properly (shift method)` () { 24 | testFromDirectory("shift/simple") { 25 | "books" shift "$.store.book[*].title" 26 | "books[+]" /= "Game of Thrones" 27 | "genre" shift { 28 | expression = "$.store.book[*].category" 29 | processor = { books -> 30 | (books as MutableList).toSet().toMutableList() 31 | } 32 | } 33 | }.doTest() 34 | } 35 | 36 | @Test 37 | fun `Simple shifts with leniency are working properly (shift)` () { 38 | testFromDirectory("shift/lenient") { 39 | "books" *= { 40 | expression = "$.store.broken.path" 41 | lenient = true 42 | } 43 | "books[++]" *= { 44 | expression = "$.store.book[*].title" 45 | } 46 | println(this) 47 | }.doTest() 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/other/config/JsonPathConfigProvidedTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.other.config 2 | 3 | import com.jayway.jsonpath.Option 4 | import net.andreinc.mapneat.MapNeatTest.Companion.testFromDirectory 5 | import net.andreinc.mapneat.config.JsonPathConfiguration 6 | import org.junit.jupiter.api.Test 7 | 8 | class JsonPathConfigProvidedTest { 9 | @Test 10 | fun `Test provided configuration supresses exceptions`() { 11 | val config = JsonPathConfiguration.mapNeatConfiguration.addOptions(Option.SUPPRESS_EXCEPTIONS) 12 | 13 | testFromDirectory("config", config) { 14 | "existing" *= "$.something" 15 | "notExisting" *= "$.something1" 16 | "notExistingToRemove" *= "$.something1" 17 | "notExisting1" /= { sourceCtx().read("$.something1") } 18 | "notExisting1" % "notExisting2" 19 | "notExisting2" %= "notExisting3.subPath" 20 | - "something1" 21 | - "notExistingToRemove" 22 | }.doTest() 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/other/parent/ParentTests.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.other.parent 2 | 3 | import net.andreinc.mapneat.MapNeatTest.Companion.testFromDirectory 4 | import net.andreinc.mapneat.dsl.json 5 | import org.junit.jupiter.api.Assertions 6 | import org.junit.jupiter.api.Assertions.assertTrue 7 | import org.junit.jupiter.api.Test 8 | 9 | class ParentTests { 10 | @Test 11 | fun `Test if parent reference is working correctly in inner json reference`() { 12 | testFromDirectory("parent/simple") { 13 | "something" /= "Something Value" 14 | "person" /= json { 15 | "something" /= { parent()!!.targetCtx().read("$.something") } 16 | } 17 | }.doTest() 18 | } 19 | 20 | @Test 21 | fun `Test if hasParent() returns a correct value` () { 22 | json("{}") { 23 | "value" /= json("{}") { 24 | assertTrue(hasParent()) 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/other/root/RootTests.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.other.root 2 | 3 | import net.andreinc.mapneat.MapNeatTest 4 | import org.junit.jupiter.api.Test 5 | 6 | class RootTests { 7 | @Test 8 | fun `Test if the source is copied correctly to target`() { 9 | MapNeatTest.testFromDirectory("root") { 10 | copySourceToTarget() 11 | }.doTest() 12 | } 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/mapneat/other/xmlsource/XmlSourceTests.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.mapneat.other.xmlsource 2 | 3 | import net.andreinc.mapneat.MapNeatTest.Companion.testFromDirectory 4 | import org.junit.jupiter.api.Test 5 | 6 | class XmlSourceTests { 7 | 8 | @Test 9 | fun `Simple test to check the XML source works correctly`() { 10 | testFromDirectory("xmlsource/simple", xmlSource = true) { 11 | "feeling" *= "$.root.somehow.howHungry" 12 | "feeling[+]" *= "$.root.somehow.content" 13 | }.doTest() 14 | } 15 | } -------------------------------------------------------------------------------- /src/test/resources/assign/hierarchy/source.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /src/test/resources/assign/hierarchy/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "a" : { 3 | "b" : { 4 | "c" : { 5 | "d" : { 6 | "e" : { 7 | "f" : { 8 | "g" : { 9 | "h" : "10", 10 | "i" : "20", 11 | "j" : "20", 12 | "k" : "20" 13 | } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/resources/assign/jsoninjson/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "books" : [ 3 | { 4 | "title" : "Cool dog", 5 | "author" : "Mike Smith" 6 | }, 7 | { 8 | "title": "Feeble Cat", 9 | "author": "John Cibble" 10 | }, 11 | { 12 | "title": "Morning Horse", 13 | "author": "Kohn Gotcha" 14 | } 15 | ], 16 | "address" : { 17 | "country" : "RO", 18 | "street_number": 123, 19 | "city": "Bucharest" 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/resources/assign/jsoninjson/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : { 3 | "firstName" : "John", 4 | "lastName" : "Cibble", 5 | "book" : "Feeble Cat", 6 | "address" : { 7 | "country" : "UK" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/test/resources/assign/lambda/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "someValue" : "X", 3 | "some" : { 4 | "val1" : 1, 5 | "val2" : 2 6 | } 7 | } -------------------------------------------------------------------------------- /src/test/resources/assign/lambda/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "some1" : "X", 3 | "some2" : 1, 4 | "some3" : 2, 5 | "some4" : [ "X", 1, 2 ] 6 | } -------------------------------------------------------------------------------- /src/test/resources/assign/re_assign/source.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /src/test/resources/assign/re_assign/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Neo", 3 | "oneTime" : "3" 4 | } -------------------------------------------------------------------------------- /src/test/resources/assign/root/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "books" : [ 3 | { 4 | "title" : "Cool dog", 5 | "author" : "Mike Smith" 6 | }, 7 | { 8 | "title": "Feeble Cat", 9 | "author": "John Cibble" 10 | }, 11 | { 12 | "title": "Morning Horse", 13 | "author": "Kohn Gotcha" 14 | } 15 | ], 16 | "address" : { 17 | "country" : "RO", 18 | "street_number": 123, 19 | "city": "Bucharest" 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/resources/assign/root/target.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomemory/mapneat/2a387364c90740c1c03d4e2017623062d7c8a1af/src/test/resources/assign/root/target.json -------------------------------------------------------------------------------- /src/test/resources/assign/simple/source.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /src/test/resources/assign/simple/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Andrei", 3 | "age" : 15, 4 | "isDisabled" : true, 5 | "array1" : [1, 2, 3], 6 | "array2" : [1, 2, 3], 7 | "array3" : ["a", "b", "c"] 8 | } -------------------------------------------------------------------------------- /src/test/resources/config/source.json: -------------------------------------------------------------------------------- 1 | {"something": "test"} -------------------------------------------------------------------------------- /src/test/resources/config/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "existing" : "test", 3 | "notExisting" : null, 4 | "notExisting1" : null, 5 | "notExisting3" : { 6 | "subPath" : null 7 | } 8 | } -------------------------------------------------------------------------------- /src/test/resources/copy/simple/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "book": [ 4 | { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | ], 31 | "bicycle": { 32 | "color": "red", 33 | "price": 19.95 34 | } 35 | }, 36 | "expensive": 10 37 | } -------------------------------------------------------------------------------- /src/test/resources/copy/simple/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "store" : { 3 | "book" : [ { 4 | "category" : "reference", 5 | "author" : "Nigel Rees", 6 | "title" : "Sayings of the Century", 7 | "price" : 8.95 8 | }, { 9 | "category" : "fiction", 10 | "author" : "Evelyn Waugh", 11 | "title" : "Sword of Honour", 12 | "price" : 12.99 13 | }, { 14 | "category" : "fiction", 15 | "author" : "Herman Melville", 16 | "title" : "Moby Dick", 17 | "isbn" : "0-553-21311-3", 18 | "price" : 8.99 19 | }, { 20 | "category" : "fiction", 21 | "author" : "J. R. R. Tolkien", 22 | "title" : "The Lord of the Rings", 23 | "isbn" : "0-395-19395-8", 24 | "price" : 22.99 25 | } ], 26 | "bicycle" : { 27 | "color" : "red", 28 | "price" : 19.95 29 | }, 30 | "books" : [ { 31 | "category" : "reference", 32 | "author" : "Nigel Rees", 33 | "title" : "Sayings of the Century", 34 | "price" : 8.95 35 | }, { 36 | "category" : "fiction", 37 | "author" : "Evelyn Waugh", 38 | "title" : "Sword of Honour", 39 | "price" : 12.99 40 | }, { 41 | "category" : "fiction", 42 | "author" : "Herman Melville", 43 | "title" : "Moby Dick", 44 | "isbn" : "0-553-21311-3", 45 | "price" : 8.99 46 | }, { 47 | "category" : "fiction", 48 | "author" : "J. R. R. Tolkien", 49 | "title" : "The Lord of the Rings", 50 | "isbn" : "0-395-19395-8", 51 | "price" : 22.99 52 | } ] 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/resources/delete/simple/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "books" : [ 3 | { 4 | "title" : "Cool dog", 5 | "author" : "Mike Smith" 6 | }, 7 | { 8 | "title": "Feeble Cat", 9 | "author": "John Cibble" 10 | }, 11 | { 12 | "title": "Morning Horse", 13 | "author": "Kohn Gotcha" 14 | } 15 | ], 16 | "address" : { 17 | "country" : "RO", 18 | "street_number": 123, 19 | "city": "Bucharest" 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/resources/delete/simple/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "books" : [ 3 | { 4 | "title" : "Cool dog", 5 | "author" : "Mike Smith" 6 | }, 7 | { 8 | "title": "Feeble Cat", 9 | "author": "John Cibble" 10 | }, 11 | { 12 | "title": "Morning Horse", 13 | "author": "Kohn Gotcha" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /src/test/resources/move/simple/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "book": [ 4 | { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | ], 31 | "bicycle": { 32 | "color": "red", 33 | "price": 19.95 34 | } 35 | }, 36 | "expensive": 10 37 | } -------------------------------------------------------------------------------- /src/test/resources/move/simple/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "store" : { 3 | "bicycle" : { 4 | "color" : "red", 5 | "price" : 19.95 6 | }, 7 | "books" : [ { 8 | "category" : "reference", 9 | "author" : "Nigel Rees", 10 | "title" : "Sayings of the Century", 11 | "price" : 8.95 12 | }, { 13 | "category" : "fiction", 14 | "author" : "Evelyn Waugh", 15 | "title" : "Sword of Honour", 16 | "price" : 12.99 17 | }, { 18 | "category" : "fiction", 19 | "author" : "Herman Melville", 20 | "title" : "Moby Dick", 21 | "isbn" : "0-553-21311-3", 22 | "price" : 8.99 23 | }, { 24 | "category" : "fiction", 25 | "author" : "J. R. R. Tolkien", 26 | "title" : "The Lord of the Rings", 27 | "isbn" : "0-395-19395-8", 28 | "price" : 22.99 29 | } ] 30 | } 31 | } -------------------------------------------------------------------------------- /src/test/resources/parent/simple/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 380557, 3 | "first_name": "Gary", 4 | "last_name": "Young", 5 | "photo": "http://srcimg.com/100/150", 6 | "married": false, 7 | "visits" : [ 8 | { 9 | "country" : "Romania", 10 | "date" : "2020-10-10" 11 | }, 12 | { 13 | "country" : "Romania", 14 | "date" : "2019-07-21" 15 | }, 16 | { 17 | "country" : "Italy", 18 | "date" : "2019-12-21" 19 | }, 20 | { 21 | "country" : "France", 22 | "date" : "2019-02-21" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /src/test/resources/parent/simple/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "something" : "Something Value", 3 | "person" : { 4 | "something" : "Something Value" 5 | } 6 | } -------------------------------------------------------------------------------- /src/test/resources/root/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 380557, 3 | "first_name": "Gary", 4 | "last_name": "Young", 5 | "photo": "http://srcimg.com/100/150", 6 | "married": false, 7 | "visits" : [ 8 | { 9 | "country" : "Romania", 10 | "date" : "2020-10-10" 11 | }, 12 | { 13 | "country" : "Romania", 14 | "date" : "2019-07-21" 15 | }, 16 | { 17 | "country" : "Italy", 18 | "date" : "2019-12-21" 19 | }, 20 | { 21 | "country" : "France", 22 | "date" : "2019-02-21" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /src/test/resources/root/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 380557, 3 | "first_name": "Gary", 4 | "last_name": "Young", 5 | "photo": "http://srcimg.com/100/150", 6 | "married": false, 7 | "visits" : [ 8 | { 9 | "country" : "Romania", 10 | "date" : "2020-10-10" 11 | }, 12 | { 13 | "country" : "Romania", 14 | "date" : "2019-07-21" 15 | }, 16 | { 17 | "country" : "Italy", 18 | "date" : "2019-12-21" 19 | }, 20 | { 21 | "country" : "France", 22 | "date" : "2019-02-21" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /src/test/resources/shift/lenient/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "book": [ 4 | { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | ], 31 | "bicycle": { 32 | "color": "red", 33 | "price": 19.95 34 | } 35 | }, 36 | "expensive": 10 37 | } -------------------------------------------------------------------------------- /src/test/resources/shift/lenient/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "books" : [ "", "Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings" ] 3 | } -------------------------------------------------------------------------------- /src/test/resources/shift/simple/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "book": [ 4 | { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | ], 31 | "bicycle": { 32 | "color": "red", 33 | "price": 19.95 34 | } 35 | }, 36 | "expensive": 10 37 | } -------------------------------------------------------------------------------- /src/test/resources/shift/simple/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "books" : [ "Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings", "Game of Thrones" ], 3 | "genre" : [ "reference", "fiction" ] 4 | } -------------------------------------------------------------------------------- /src/test/resources/xmlsource/simple/source.xml: -------------------------------------------------------------------------------- 1 | 2 | hungry 3 | save 4 | 5 | 6 | 541273007 7 | worried 8 | brought 9 | -1410733687.2053316 10 | -2037906708 11 | kind 12 | 13 | -1595143432 14 | reader 15 | stared 16 | -178038838 17 | -846607343.7045817 18 | 19 | meal 20 | product 21 | lack 22 | -------------------------------------------------------------------------------- /src/test/resources/xmlsource/simple/target.json: -------------------------------------------------------------------------------- 1 | { 2 | "feeling" : [ "very", "hungry" ] 3 | } 4 | --------------------------------------------------------------------------------