├── .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 |
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 |
--------------------------------------------------------------------------------