├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── data └── dynamic │ └── dyanmic-example │ └── usersList.json ├── examples ├── json-response-dynamic-mockneat.kts ├── json-response-simple.kts ├── plain-text-responses.kts ├── rest-crud-example.kts └── static │ └── test.txt ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── src ├── main │ ├── kotlin │ │ ├── Main.kt │ │ ├── MainContext.kt │ │ └── net │ │ │ └── andreinc │ │ │ └── serverneat │ │ │ ├── CliApp.kt │ │ │ ├── KotlinScriptRunner.kt │ │ │ ├── logging │ │ │ └── AnsiScapeConfig.kt │ │ │ ├── mockneat │ │ │ └── extension │ │ │ │ └── ObjectMap.kt │ │ │ └── server │ │ │ ├── RouteDSL.kt │ │ │ ├── RouteResponseDSL.kt │ │ │ └── ServerDSL.kt │ └── resources │ │ ├── META-INF │ │ ├── MANIFEST.MF │ │ └── services │ │ │ └── javax.script.ScriptEngineFactory │ │ └── log4j.properties └── test │ ├── kotlin │ └── net │ │ └── andreinc │ │ └── serverneat │ │ ├── abstraction │ │ └── RouteResponseAbstractTest.kt │ │ ├── responses │ │ ├── RouteDynamicHeadersTest.kt │ │ ├── RouteResponseFileDownloadTest.kt │ │ ├── RouteResponseFileTest.kt │ │ ├── RouteResponsePlainTextTest.kt │ │ ├── RouteResponseResourceTest.kt │ │ └── RouteResponseSimpleJsonTest.kt │ │ └── utils │ │ └── FileUtils.kt │ └── resources │ └── api-data │ ├── delete-response.txt │ ├── get-response.txt │ ├── patch-response.txt │ ├── post-response.txt │ └── put-response.txt └── userList.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/java,gradle,eclipse,intellij 3 | 4 | ### Java ### 5 | *.class 6 | 7 | # Mobile Tools for Java (J2ME) 8 | .mtj.tmp/ 9 | 10 | # Package Files # 11 | *.jar 12 | *.war 13 | *.ear 14 | 15 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 16 | hs_err_pid* 17 | 18 | 19 | ### Gradle ### 20 | .gradle 21 | gradle/ 22 | build/ 23 | 24 | # Ignore Gradle GUI config 25 | gradle-app.setting 26 | 27 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 28 | !gradle-wrapper.jar 29 | 30 | # Cache of project 31 | .gradletasknamecache 32 | 33 | 34 | ### Eclipse ### 35 | *.pydevproject 36 | .metadata 37 | bin/ 38 | tmp/ 39 | *.tmp 40 | *.bak 41 | *.swp 42 | *~.nib 43 | local.properties 44 | .settings/ 45 | .loadpath 46 | 47 | # Eclipse Core 48 | .project 49 | 50 | # External tool builders 51 | .externalToolBuilders/ 52 | 53 | # Locally stored "Eclipse launch configurations" 54 | *.launch 55 | 56 | # CDT-specific 57 | .cproject 58 | 59 | # JDT-specific (Eclipse Java Development Tools) 60 | .classpath 61 | 62 | # Java annotation processor (APT) 63 | .factorypath 64 | 65 | # PDT-specific 66 | .buildpath 67 | 68 | # sbteclipse plugin 69 | .target 70 | 71 | # TeXlipse plugin 72 | .texlipse 73 | 74 | # STS (Spring Tool Suite) 75 | .springBeans 76 | 77 | 78 | ### Intellij ### 79 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 80 | 81 | *.iml 82 | 83 | ## Directory-based project format: 84 | .idea/ 85 | # if you remove the above rule, at least ignore the following: 86 | 87 | # User-specific stuff: 88 | # .idea/workspace.xml 89 | # .idea/tasks.xml 90 | # .idea/dictionaries 91 | # .idea/shelf 92 | 93 | # Sensitive or high-churn files: 94 | # .idea/dataSources.ids 95 | # .idea/dataSources.xml 96 | # .idea/sqlDataSources.xml 97 | # .idea/dynamic.xml 98 | # .idea/uiDesigner.xml 99 | 100 | # Gradle: 101 | # .idea/gradle.xml 102 | # .idea/libraries 103 | 104 | # Mongo Explorer plugin: 105 | # .idea/mongoSettings.xml 106 | 107 | ## File-based project format: 108 | *.ipr 109 | *.iws 110 | 111 | ## Plugin-specific files: 112 | 113 | # IntelliJ 114 | /out/ 115 | 116 | # mpeltonen/sbt-idea plugin 117 | .idea_modules/ 118 | 119 | # JIRA plugin 120 | atlassian-ide-plugin.xml 121 | 122 | # Crashlytics plugin (for Android Studio and IntelliJ) 123 | com_crashlytics_export_strings.xml 124 | crashlytics.properties 125 | crashlytics-build.properties 126 | fabric.properties 127 | 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ServerNeat 2 | 3 | ## Introduction 4 | 5 | *ServerNeat* is (~not another~) a Kotlin Web Server for mocking and stubbing Rest APIs. 6 | 7 | It provides and easy to use DSL and seamless integration with [MockNeat](http://www.mockneat.com) for generating dynamic json responses. 8 | 9 | *ServerNeat* can be used as standalone application, capable of loading, compiling and evaluating `kts` scripts, or as a Kotlin/Java library. 10 | 11 | ## Building the stand-alone mock server 12 | 13 | By executing the `application` task, a a (fat) stand-alone jar is created in the `build\libs` folder: 14 | 15 | Gradle: 16 | ```groovy 17 | gradle application 18 | ``` 19 | 20 | Running the server: 21 | 22 | ``` 23 | java -jar serverneat-all-1.0-SNAPSHOT.jar -f 24 | ``` 25 | 26 | Note: in the `examples\` folder there are a few `.kts` scripts that can be used for testing. 27 | 28 | ## Features 29 | 30 | The server supports the following types of responses: 31 | 32 | | ResponseType | Description | 33 | | ------------ | ----------- | 34 | | PlainText | Responds to the HTTP by returning simple `String` values as the response body. | 35 | | File Content | Responds to the request reading the content of a file and returning it as `String`. It can be useful when you want to separate the response bodies from the configuration. | 36 | | Resource Content | Similar to `File Content`, except the files are read as Resources from the `resources` folder. It's particularly useful when you don't run **serverneat** as standalone application but you use it as a "library". | 37 | | File Download | Useful when you want to emulate a route that is allowing the user to download a file. | 38 | | JSON Content | Allows you to generate JSON in an instant using a nice DSL and www.mockneat.com integration. | 39 | 40 | ## Examples 41 | 42 | Check the `examples` folder. 43 | 44 | ### Plain text response - Hello world 45 | 46 | ```kotlin 47 | import net.andreinc.serverneat.server.server 48 | 49 | server { 50 | 51 | httpOptions { 52 | host = "localhost" 53 | port = 8081 54 | } 55 | 56 | globalHeaders { 57 | header("Content-Type", "application/text") 58 | } 59 | 60 | routes { 61 | get { 62 | path = "/plainText" 63 | response { 64 | header("plain", "text") 65 | statusCode = 200 66 | plainText { 67 | value = "Hello World!" 68 | } 69 | } 70 | } 71 | } 72 | 73 | 74 | }.start() 75 | ``` 76 | 77 | ### Json Response (Simple) 78 | 79 | ```kotlin 80 | import net.andreinc.serverneat.mockneat.extension.obj 81 | import net.andreinc.serverneat.server.server 82 | 83 | server { 84 | 85 | httpOptions { 86 | host = "localhost" 87 | port = 8081 88 | } 89 | 90 | globalHeaders { 91 | header("Content-Type", "application/json") 92 | } 93 | 94 | routes { 95 | get { 96 | path = "/user/100" 97 | response { 98 | header("plain", "text") // Adding a custom header to the response 99 | statusCode = 200 100 | json { 101 | value = obj { 102 | "firstName" const "Mike" 103 | "lastName" const "Smith" 104 | "someFiles" const arrayOf("file1.txt", "file2.txt") 105 | "anotherObject" value obj { 106 | "someData" const "someValue" 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | 115 | }.start() 116 | ``` 117 | 118 | PS: When creating an inner structure it's important to use `value` instead of `const`. 119 | 120 | Calling the service `curl localhost:8081/user/100`: 121 | 122 | ``` 123 | { 124 | "firstName": "Mike", 125 | "lastName": "Smith", 126 | "anotherObject": { 127 | "someData": {} 128 | }, 129 | "someFiles": [ 130 | "file1.txt", 131 | "file2.txt" 132 | ] 133 | } 134 | ``` 135 | 136 | PS: When creating an inner structure it's important to use `value` instead of `const`. 137 | 138 | ### Json Response (Dynamic - using [MockNeat](https://www.mockneat.com/)) 139 | 140 | The value object can be any [MockUnit](https://www.mockneat.com/tutorial/#everything-is-a-mockunitt). 141 | 142 | ```kotlin 143 | import net.andreinc.mockneat.unit.address.Cities.cities 144 | import net.andreinc.mockneat.unit.financial.CreditCards.creditCards 145 | import net.andreinc.mockneat.unit.time.LocalDates.localDates 146 | import net.andreinc.mockneat.unit.user.Genders.genders 147 | import net.andreinc.mockneat.unit.user.Names.names 148 | import net.andreinc.serverneat.mockneat.extension.obj 149 | import net.andreinc.serverneat.server.server 150 | 151 | server { 152 | 153 | httpOptions { 154 | host = "localhost" 155 | port = 8081 156 | } 157 | 158 | globalHeaders { 159 | header("Content-Type", "application/json") 160 | } 161 | 162 | routes { 163 | get { 164 | path = "/users" 165 | response { 166 | statusCode = 200 167 | json { 168 | persistent = true // generated data will be stored in the file "usersList.json" 169 | file = "dyanmic-example/usersList.json" 170 | value = obj { 171 | "users" value obj { 172 | "firstName" value names().first() 173 | "lastName" value names().last() 174 | "gender" value genders() 175 | "financialInformation" value obj { 176 | "creditCard1" value creditCards().visa() 177 | "creditCard2" value creditCards().amex() 178 | } 179 | "visits" value obj { 180 | "time" value localDates().thisYear() 181 | "city" value cities().capitalsEurope() 182 | }.list(5) 183 | }.list(50) 184 | } 185 | } 186 | } 187 | } 188 | } 189 | }.start() 190 | ``` 191 | 192 | And the reponse will be 193 | 194 | ```json 195 | { 196 | "users": [ 197 | { 198 | "firstName": "Tommie", 199 | "lastName": "Pecinousky", 200 | "visits": [ 201 | { 202 | "city": "Copenhagen", 203 | "time": { 204 | "year": 2020, 205 | "month": 1, 206 | "day": 7 207 | } 208 | }, 209 | { 210 | "city": "Monaco", 211 | "time": { 212 | "year": 2020, 213 | "month": 11, 214 | "day": 13 215 | } 216 | }, 217 | { 218 | "city": "Tallinn", 219 | "time": { 220 | "year": 2020, 221 | "month": 10, 222 | "day": 22 223 | } 224 | }, 225 | { 226 | "city": "Sarajevo", 227 | "time": { 228 | "year": 2020, 229 | "month": 3, 230 | "day": 7 231 | } 232 | }, 233 | { 234 | "city": "Athens", 235 | "time": { 236 | "year": 2020, 237 | "month": 12, 238 | "day": 15 239 | } 240 | } 241 | ], 242 | "financialInformation": { 243 | "creditCard2": "340529111074115", 244 | "creditCard1": "4647171048830798" 245 | }, 246 | "gender": "Female" 247 | }, 248 | { 249 | "firstName": "Sid", 250 | "lastName": "Falge", 251 | "visits": [ 252 | { 253 | "city": "Berlin", 254 | "time": { 255 | "year": 2020, 256 | "month": 2, 257 | "day": 22 258 | } 259 | }, 260 | { 261 | "city": "Brussels", 262 | "time": { 263 | "year": 2020, 264 | "month": 1, 265 | "day": 27 266 | } 267 | }, 268 | { 269 | "city": "Athens", 270 | "time": { 271 | "year": 2020, 272 | "month": 12, 273 | "day": 28 274 | } 275 | }, 276 | { 277 | "city": "San Marino", 278 | "time": { 279 | "year": 2020, 280 | "month": 10, 281 | "day": 23 282 | } 283 | }, 284 | { 285 | "city": "Stockholm", 286 | "time": { 287 | "year": 2020, 288 | "month": 5, 289 | "day": 12 290 | } 291 | } 292 | ], 293 | "financialInformation": { 294 | "creditCard2": "373802757435803", 295 | "creditCard1": "4332758675303071" 296 | }, 297 | "gender": "Male" 298 | } 299 | /// and so on 300 | ] 301 | } 302 | ``` 303 | 304 | *and son on for the rest of the users* 305 | 306 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.4.31' 3 | } 4 | 5 | task application(type: Jar) { 6 | manifest { 7 | attributes 'Main-Class': 'MainKt' 8 | } 9 | archiveBaseName = project.name + '-all' 10 | from { 11 | configurations.compile.collect { 12 | it.isDirectory() ? it : zipTree(it) 13 | } 14 | } 15 | with jar 16 | } 17 | 18 | group 'net.andreinc.serverneat' 19 | version '1.0-SNAPSHOT' 20 | 21 | repositories { 22 | mavenCentral() 23 | jcenter() 24 | } 25 | 26 | dependencies { 27 | 28 | compile 'info.picocli:picocli:4.6.1' 29 | 30 | // vertex 31 | compile "io.vertx:vertx-core:4.0.2" 32 | compile "io.vertx:vertx-web:4.0.2" 33 | compile "io.vertx:vertx-lang-kotlin:4.0.2" 34 | 35 | 36 | 37 | // mockneat 38 | compile 'net.andreinc.mockneat:mockneat:0.3.9' 39 | 40 | // logging 41 | compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.30' 42 | compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.30' 43 | compile 'io.github.microutils:kotlin-logging:2.0.6' 44 | compile 'net.andreinc.ansiscape:ansiscape:0.0.2' 45 | 46 | // json 47 | compile 'com.google.code.gson:gson:2.8.6' 48 | 49 | // kotlin scripting 50 | compile "org.jetbrains.kotlin:kotlin-script-runtime" 51 | compile "org.jetbrains.kotlin:kotlin-compiler-embeddable" 52 | compile "org.jetbrains.kotlin:kotlin-script-util" 53 | compile "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable" 54 | 55 | // kotlin lib 56 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 57 | 58 | // testing 59 | testCompile group: 'io.vertx', name: 'vertx-junit5-web-client', version: '3.9.7' 60 | testCompile 'org.junit.jupiter:junit-jupiter-api:5.7.1' 61 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' 62 | } 63 | 64 | test { 65 | useJUnitPlatform() 66 | } 67 | 68 | compileKotlin { 69 | kotlinOptions.jvmTarget = "1.8" 70 | } 71 | 72 | compileTestKotlin { 73 | kotlinOptions.jvmTarget = "1.8" 74 | } -------------------------------------------------------------------------------- /data/dynamic/dyanmic-example/usersList.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "firstName": "Tommie", 5 | "lastName": "Pecinousky", 6 | "visits": [ 7 | { 8 | "city": "Copenhagen", 9 | "time": { 10 | "year": 2020, 11 | "month": 1, 12 | "day": 7 13 | } 14 | }, 15 | { 16 | "city": "Monaco", 17 | "time": { 18 | "year": 2020, 19 | "month": 11, 20 | "day": 13 21 | } 22 | }, 23 | { 24 | "city": "Tallinn", 25 | "time": { 26 | "year": 2020, 27 | "month": 10, 28 | "day": 22 29 | } 30 | }, 31 | { 32 | "city": "Sarajevo", 33 | "time": { 34 | "year": 2020, 35 | "month": 3, 36 | "day": 7 37 | } 38 | }, 39 | { 40 | "city": "Athens", 41 | "time": { 42 | "year": 2020, 43 | "month": 12, 44 | "day": 15 45 | } 46 | } 47 | ], 48 | "financialInformation": { 49 | "creditCard2": "340529111074115", 50 | "creditCard1": "4647171048830798" 51 | }, 52 | "gender": "Female" 53 | }, 54 | { 55 | "firstName": "Sid", 56 | "lastName": "Falge", 57 | "visits": [ 58 | { 59 | "city": "Berlin", 60 | "time": { 61 | "year": 2020, 62 | "month": 2, 63 | "day": 22 64 | } 65 | }, 66 | { 67 | "city": "Brussels", 68 | "time": { 69 | "year": 2020, 70 | "month": 1, 71 | "day": 27 72 | } 73 | }, 74 | { 75 | "city": "Athens", 76 | "time": { 77 | "year": 2020, 78 | "month": 12, 79 | "day": 28 80 | } 81 | }, 82 | { 83 | "city": "San Marino", 84 | "time": { 85 | "year": 2020, 86 | "month": 10, 87 | "day": 23 88 | } 89 | }, 90 | { 91 | "city": "Stockholm", 92 | "time": { 93 | "year": 2020, 94 | "month": 5, 95 | "day": 12 96 | } 97 | } 98 | ], 99 | "financialInformation": { 100 | "creditCard2": "373802757435803", 101 | "creditCard1": "4332758675303071" 102 | }, 103 | "gender": "Male" 104 | }, 105 | { 106 | "firstName": "Bernardo", 107 | "lastName": "Vanderlaan", 108 | "visits": [ 109 | { 110 | "city": "Tirana", 111 | "time": { 112 | "year": 2020, 113 | "month": 5, 114 | "day": 1 115 | } 116 | }, 117 | { 118 | "city": "Athens", 119 | "time": { 120 | "year": 2020, 121 | "month": 1, 122 | "day": 31 123 | } 124 | }, 125 | { 126 | "city": "Vienna", 127 | "time": { 128 | "year": 2020, 129 | "month": 1, 130 | "day": 25 131 | } 132 | }, 133 | { 134 | "city": "Bucharest", 135 | "time": { 136 | "year": 2020, 137 | "month": 7, 138 | "day": 15 139 | } 140 | }, 141 | { 142 | "city": "Bucharest", 143 | "time": { 144 | "year": 2020, 145 | "month": 6, 146 | "day": 26 147 | } 148 | } 149 | ], 150 | "financialInformation": { 151 | "creditCard2": "377821958901575", 152 | "creditCard1": "4935299272973082" 153 | }, 154 | "gender": "Male" 155 | }, 156 | { 157 | "firstName": "Harley", 158 | "lastName": "Lablue", 159 | "visits": [ 160 | { 161 | "city": "Budapest", 162 | "time": { 163 | "year": 2020, 164 | "month": 11, 165 | "day": 27 166 | } 167 | }, 168 | { 169 | "city": "Tbilisi", 170 | "time": { 171 | "year": 2020, 172 | "month": 11, 173 | "day": 3 174 | } 175 | }, 176 | { 177 | "city": "Dublin", 178 | "time": { 179 | "year": 2020, 180 | "month": 1, 181 | "day": 3 182 | } 183 | }, 184 | { 185 | "city": "Rome", 186 | "time": { 187 | "year": 2020, 188 | "month": 2, 189 | "day": 29 190 | } 191 | }, 192 | { 193 | "city": "Stockholm", 194 | "time": { 195 | "year": 2020, 196 | "month": 6, 197 | "day": 28 198 | } 199 | } 200 | ], 201 | "financialInformation": { 202 | "creditCard2": "371378746877449", 203 | "creditCard1": "4066270632784197" 204 | }, 205 | "gender": "Male" 206 | }, 207 | { 208 | "firstName": "Errol", 209 | "lastName": "Southward", 210 | "visits": [ 211 | { 212 | "city": "Sofia", 213 | "time": { 214 | "year": 2020, 215 | "month": 12, 216 | "day": 29 217 | } 218 | }, 219 | { 220 | "city": "Yerevan", 221 | "time": { 222 | "year": 2020, 223 | "month": 7, 224 | "day": 5 225 | } 226 | }, 227 | { 228 | "city": "Vaduz", 229 | "time": { 230 | "year": 2020, 231 | "month": 4, 232 | "day": 7 233 | } 234 | }, 235 | { 236 | "city": "Rome", 237 | "time": { 238 | "year": 2020, 239 | "month": 8, 240 | "day": 20 241 | } 242 | }, 243 | { 244 | "city": "Baku", 245 | "time": { 246 | "year": 2020, 247 | "month": 12, 248 | "day": 2 249 | } 250 | } 251 | ], 252 | "financialInformation": { 253 | "creditCard2": "344904571395431", 254 | "creditCard1": "4499886948762598" 255 | }, 256 | "gender": "Male" 257 | }, 258 | { 259 | "firstName": "Valentine", 260 | "lastName": "Schiavo", 261 | "visits": [ 262 | { 263 | "city": "Prague", 264 | "time": { 265 | "year": 2020, 266 | "month": 1, 267 | "day": 1 268 | } 269 | }, 270 | { 271 | "city": "Tbilisi", 272 | "time": { 273 | "year": 2020, 274 | "month": 2, 275 | "day": 26 276 | } 277 | }, 278 | { 279 | "city": "Monaco", 280 | "time": { 281 | "year": 2020, 282 | "month": 11, 283 | "day": 10 284 | } 285 | }, 286 | { 287 | "city": "Baku", 288 | "time": { 289 | "year": 2020, 290 | "month": 8, 291 | "day": 20 292 | } 293 | }, 294 | { 295 | "city": "Minsk", 296 | "time": { 297 | "year": 2020, 298 | "month": 2, 299 | "day": 8 300 | } 301 | } 302 | ], 303 | "financialInformation": { 304 | "creditCard2": "343978585834149", 305 | "creditCard1": "4034132616637966" 306 | }, 307 | "gender": "Female" 308 | }, 309 | { 310 | "firstName": "Dewitt", 311 | "lastName": "Sickinger", 312 | "visits": [ 313 | { 314 | "city": "Tirana", 315 | "time": { 316 | "year": 2020, 317 | "month": 5, 318 | "day": 3 319 | } 320 | }, 321 | { 322 | "city": "Prague", 323 | "time": { 324 | "year": 2020, 325 | "month": 4, 326 | "day": 26 327 | } 328 | }, 329 | { 330 | "city": "Budapest", 331 | "time": { 332 | "year": 2020, 333 | "month": 6, 334 | "day": 9 335 | } 336 | }, 337 | { 338 | "city": "Madrid", 339 | "time": { 340 | "year": 2020, 341 | "month": 7, 342 | "day": 26 343 | } 344 | }, 345 | { 346 | "city": "Nicosia", 347 | "time": { 348 | "year": 2020, 349 | "month": 1, 350 | "day": 25 351 | } 352 | } 353 | ], 354 | "financialInformation": { 355 | "creditCard2": "371189357656711", 356 | "creditCard1": "4685318958497653" 357 | }, 358 | "gender": "Female" 359 | }, 360 | { 361 | "firstName": "Dana", 362 | "lastName": "Nuzum", 363 | "visits": [ 364 | { 365 | "city": "Berlin", 366 | "time": { 367 | "year": 2020, 368 | "month": 6, 369 | "day": 25 370 | } 371 | }, 372 | { 373 | "city": "Vilnius", 374 | "time": { 375 | "year": 2020, 376 | "month": 2, 377 | "day": 19 378 | } 379 | }, 380 | { 381 | "city": "Amsterdam", 382 | "time": { 383 | "year": 2020, 384 | "month": 5, 385 | "day": 28 386 | } 387 | }, 388 | { 389 | "city": "Vilnius", 390 | "time": { 391 | "year": 2020, 392 | "month": 12, 393 | "day": 18 394 | } 395 | }, 396 | { 397 | "city": "Rome", 398 | "time": { 399 | "year": 2020, 400 | "month": 1, 401 | "day": 22 402 | } 403 | } 404 | ], 405 | "financialInformation": { 406 | "creditCard2": "372320857952073", 407 | "creditCard1": "4878629855683436" 408 | }, 409 | "gender": "Female" 410 | }, 411 | { 412 | "firstName": "Sterling", 413 | "lastName": "Kleinke", 414 | "visits": [ 415 | { 416 | "city": "Bratislava", 417 | "time": { 418 | "year": 2020, 419 | "month": 4, 420 | "day": 3 421 | } 422 | }, 423 | { 424 | "city": "Valletta", 425 | "time": { 426 | "year": 2020, 427 | "month": 10, 428 | "day": 8 429 | } 430 | }, 431 | { 432 | "city": "Belgrade", 433 | "time": { 434 | "year": 2020, 435 | "month": 3, 436 | "day": 24 437 | } 438 | }, 439 | { 440 | "city": "Reykjavík", 441 | "time": { 442 | "year": 2020, 443 | "month": 12, 444 | "day": 21 445 | } 446 | }, 447 | { 448 | "city": "Budapest", 449 | "time": { 450 | "year": 2020, 451 | "month": 10, 452 | "day": 25 453 | } 454 | } 455 | ], 456 | "financialInformation": { 457 | "creditCard2": "348958057701377", 458 | "creditCard1": "4029881819781693" 459 | }, 460 | "gender": "Male" 461 | }, 462 | { 463 | "firstName": "Lonny", 464 | "lastName": "Baczewski", 465 | "visits": [ 466 | { 467 | "city": "Ljubljana", 468 | "time": { 469 | "year": 2020, 470 | "month": 11, 471 | "day": 15 472 | } 473 | }, 474 | { 475 | "city": "Vaduz", 476 | "time": { 477 | "year": 2020, 478 | "month": 3, 479 | "day": 31 480 | } 481 | }, 482 | { 483 | "city": "Monaco", 484 | "time": { 485 | "year": 2020, 486 | "month": 9, 487 | "day": 27 488 | } 489 | }, 490 | { 491 | "city": "Athens", 492 | "time": { 493 | "year": 2020, 494 | "month": 10, 495 | "day": 29 496 | } 497 | }, 498 | { 499 | "city": "Chisinau", 500 | "time": { 501 | "year": 2020, 502 | "month": 8, 503 | "day": 25 504 | } 505 | } 506 | ], 507 | "financialInformation": { 508 | "creditCard2": "374778231737189", 509 | "creditCard1": "4292937524882299" 510 | }, 511 | "gender": "Male" 512 | }, 513 | { 514 | "firstName": "Domenic", 515 | "lastName": "Chasser", 516 | "visits": [ 517 | { 518 | "city": "Astana", 519 | "time": { 520 | "year": 2020, 521 | "month": 10, 522 | "day": 11 523 | } 524 | }, 525 | { 526 | "city": "Riga", 527 | "time": { 528 | "year": 2020, 529 | "month": 3, 530 | "day": 23 531 | } 532 | }, 533 | { 534 | "city": "Zagreb", 535 | "time": { 536 | "year": 2020, 537 | "month": 8, 538 | "day": 4 539 | } 540 | }, 541 | { 542 | "city": "Sarajevo", 543 | "time": { 544 | "year": 2020, 545 | "month": 1, 546 | "day": 27 547 | } 548 | }, 549 | { 550 | "city": "Copenhagen", 551 | "time": { 552 | "year": 2020, 553 | "month": 2, 554 | "day": 17 555 | } 556 | } 557 | ], 558 | "financialInformation": { 559 | "creditCard2": "376268943857648", 560 | "creditCard1": "4299881907884063" 561 | }, 562 | "gender": "Female" 563 | }, 564 | { 565 | "firstName": "Raymond", 566 | "lastName": "Ocenasek", 567 | "visits": [ 568 | { 569 | "city": "Sofia", 570 | "time": { 571 | "year": 2020, 572 | "month": 3, 573 | "day": 27 574 | } 575 | }, 576 | { 577 | "city": "Prague", 578 | "time": { 579 | "year": 2020, 580 | "month": 5, 581 | "day": 31 582 | } 583 | }, 584 | { 585 | "city": "Sarajevo", 586 | "time": { 587 | "year": 2020, 588 | "month": 7, 589 | "day": 27 590 | } 591 | }, 592 | { 593 | "city": "Tbilisi", 594 | "time": { 595 | "year": 2020, 596 | "month": 4, 597 | "day": 9 598 | } 599 | }, 600 | { 601 | "city": "Reykjavík", 602 | "time": { 603 | "year": 2020, 604 | "month": 9, 605 | "day": 20 606 | } 607 | } 608 | ], 609 | "financialInformation": { 610 | "creditCard2": "343939202653767", 611 | "creditCard1": "4529305603200624" 612 | }, 613 | "gender": "Female" 614 | }, 615 | { 616 | "firstName": "Bill", 617 | "lastName": "Lawley", 618 | "visits": [ 619 | { 620 | "city": "Brussels", 621 | "time": { 622 | "year": 2020, 623 | "month": 9, 624 | "day": 20 625 | } 626 | }, 627 | { 628 | "city": "London", 629 | "time": { 630 | "year": 2020, 631 | "month": 3, 632 | "day": 28 633 | } 634 | }, 635 | { 636 | "city": "Andorra la Vella", 637 | "time": { 638 | "year": 2020, 639 | "month": 4, 640 | "day": 1 641 | } 642 | }, 643 | { 644 | "city": "Tbilisi", 645 | "time": { 646 | "year": 2020, 647 | "month": 9, 648 | "day": 6 649 | } 650 | }, 651 | { 652 | "city": "Bucharest", 653 | "time": { 654 | "year": 2020, 655 | "month": 6, 656 | "day": 1 657 | } 658 | } 659 | ], 660 | "financialInformation": { 661 | "creditCard2": "374615692823035", 662 | "creditCard1": "4565478389622587" 663 | }, 664 | "gender": "Male" 665 | }, 666 | { 667 | "firstName": "Leo", 668 | "lastName": "Villane", 669 | "visits": [ 670 | { 671 | "city": "Bucharest", 672 | "time": { 673 | "year": 2020, 674 | "month": 5, 675 | "day": 14 676 | } 677 | }, 678 | { 679 | "city": "Nicosia", 680 | "time": { 681 | "year": 2020, 682 | "month": 4, 683 | "day": 3 684 | } 685 | }, 686 | { 687 | "city": "Warsaw", 688 | "time": { 689 | "year": 2020, 690 | "month": 8, 691 | "day": 28 692 | } 693 | }, 694 | { 695 | "city": "Riga", 696 | "time": { 697 | "year": 2020, 698 | "month": 12, 699 | "day": 6 700 | } 701 | }, 702 | { 703 | "city": "Podgorica", 704 | "time": { 705 | "year": 2020, 706 | "month": 6, 707 | "day": 23 708 | } 709 | } 710 | ], 711 | "financialInformation": { 712 | "creditCard2": "370309121675893", 713 | "creditCard1": "4127400286782588" 714 | }, 715 | "gender": "Female" 716 | }, 717 | { 718 | "firstName": "Barrett", 719 | "lastName": "Hemanes", 720 | "visits": [ 721 | { 722 | "city": "London", 723 | "time": { 724 | "year": 2020, 725 | "month": 11, 726 | "day": 17 727 | } 728 | }, 729 | { 730 | "city": "Sarajevo", 731 | "time": { 732 | "year": 2020, 733 | "month": 11, 734 | "day": 9 735 | } 736 | }, 737 | { 738 | "city": "Monaco", 739 | "time": { 740 | "year": 2020, 741 | "month": 9, 742 | "day": 16 743 | } 744 | }, 745 | { 746 | "city": "Reykjavík", 747 | "time": { 748 | "year": 2020, 749 | "month": 3, 750 | "day": 10 751 | } 752 | }, 753 | { 754 | "city": "Sofia", 755 | "time": { 756 | "year": 2020, 757 | "month": 3, 758 | "day": 29 759 | } 760 | } 761 | ], 762 | "financialInformation": { 763 | "creditCard2": "347795966417989", 764 | "creditCard1": "4922741860153622" 765 | }, 766 | "gender": "Male" 767 | }, 768 | { 769 | "firstName": "Daniel", 770 | "lastName": "Bair", 771 | "visits": [ 772 | { 773 | "city": "Vatican City", 774 | "time": { 775 | "year": 2020, 776 | "month": 1, 777 | "day": 12 778 | } 779 | }, 780 | { 781 | "city": "Vaduz", 782 | "time": { 783 | "year": 2020, 784 | "month": 6, 785 | "day": 20 786 | } 787 | }, 788 | { 789 | "city": "Podgorica", 790 | "time": { 791 | "year": 2020, 792 | "month": 1, 793 | "day": 14 794 | } 795 | }, 796 | { 797 | "city": "Minsk", 798 | "time": { 799 | "year": 2020, 800 | "month": 5, 801 | "day": 13 802 | } 803 | }, 804 | { 805 | "city": "Berlin", 806 | "time": { 807 | "year": 2020, 808 | "month": 10, 809 | "day": 5 810 | } 811 | } 812 | ], 813 | "financialInformation": { 814 | "creditCard2": "346940459903993", 815 | "creditCard1": "4808924649999601" 816 | }, 817 | "gender": "Female" 818 | }, 819 | { 820 | "firstName": "Quincy", 821 | "lastName": "Inlow", 822 | "visits": [ 823 | { 824 | "city": "Chisinau", 825 | "time": { 826 | "year": 2020, 827 | "month": 8, 828 | "day": 26 829 | } 830 | }, 831 | { 832 | "city": "Paris", 833 | "time": { 834 | "year": 2020, 835 | "month": 5, 836 | "day": 2 837 | } 838 | }, 839 | { 840 | "city": "Andorra la Vella", 841 | "time": { 842 | "year": 2020, 843 | "month": 7, 844 | "day": 22 845 | } 846 | }, 847 | { 848 | "city": "Luxembourg", 849 | "time": { 850 | "year": 2020, 851 | "month": 12, 852 | "day": 25 853 | } 854 | }, 855 | { 856 | "city": "Amsterdam", 857 | "time": { 858 | "year": 2020, 859 | "month": 6, 860 | "day": 9 861 | } 862 | } 863 | ], 864 | "financialInformation": { 865 | "creditCard2": "375944061148041", 866 | "creditCard1": "4757233958748786" 867 | }, 868 | "gender": "Male" 869 | }, 870 | { 871 | "firstName": "Carlo", 872 | "lastName": "Browner", 873 | "visits": [ 874 | { 875 | "city": "Sarajevo", 876 | "time": { 877 | "year": 2020, 878 | "month": 5, 879 | "day": 10 880 | } 881 | }, 882 | { 883 | "city": "Helsinki", 884 | "time": { 885 | "year": 2020, 886 | "month": 7, 887 | "day": 3 888 | } 889 | }, 890 | { 891 | "city": "Skopje", 892 | "time": { 893 | "year": 2020, 894 | "month": 4, 895 | "day": 23 896 | } 897 | }, 898 | { 899 | "city": "Zagreb", 900 | "time": { 901 | "year": 2020, 902 | "month": 5, 903 | "day": 14 904 | } 905 | }, 906 | { 907 | "city": "Oslo", 908 | "time": { 909 | "year": 2020, 910 | "month": 5, 911 | "day": 27 912 | } 913 | } 914 | ], 915 | "financialInformation": { 916 | "creditCard2": "371416476821441", 917 | "creditCard1": "4571283785286512" 918 | }, 919 | "gender": "Female" 920 | }, 921 | { 922 | "firstName": "Lorenzo", 923 | "lastName": "Ginsel", 924 | "visits": [ 925 | { 926 | "city": "Belgrade", 927 | "time": { 928 | "year": 2020, 929 | "month": 10, 930 | "day": 23 931 | } 932 | }, 933 | { 934 | "city": "Dublin", 935 | "time": { 936 | "year": 2020, 937 | "month": 4, 938 | "day": 9 939 | } 940 | }, 941 | { 942 | "city": "Bratislava", 943 | "time": { 944 | "year": 2020, 945 | "month": 3, 946 | "day": 31 947 | } 948 | }, 949 | { 950 | "city": "Reykjavík", 951 | "time": { 952 | "year": 2020, 953 | "month": 10, 954 | "day": 4 955 | } 956 | }, 957 | { 958 | "city": "Amsterdam", 959 | "time": { 960 | "year": 2020, 961 | "month": 3, 962 | "day": 12 963 | } 964 | } 965 | ], 966 | "financialInformation": { 967 | "creditCard2": "342613433500672", 968 | "creditCard1": "4258799392586200" 969 | }, 970 | "gender": "Female" 971 | }, 972 | { 973 | "firstName": "Sid", 974 | "lastName": "Reder", 975 | "visits": [ 976 | { 977 | "city": "Rome", 978 | "time": { 979 | "year": 2020, 980 | "month": 3, 981 | "day": 6 982 | } 983 | }, 984 | { 985 | "city": "Stockholm", 986 | "time": { 987 | "year": 2020, 988 | "month": 11, 989 | "day": 17 990 | } 991 | }, 992 | { 993 | "city": "Berlin", 994 | "time": { 995 | "year": 2020, 996 | "month": 10, 997 | "day": 2 998 | } 999 | }, 1000 | { 1001 | "city": "Bern", 1002 | "time": { 1003 | "year": 2020, 1004 | "month": 4, 1005 | "day": 22 1006 | } 1007 | }, 1008 | { 1009 | "city": "Ljubljana", 1010 | "time": { 1011 | "year": 2020, 1012 | "month": 2, 1013 | "day": 22 1014 | } 1015 | } 1016 | ], 1017 | "financialInformation": { 1018 | "creditCard2": "371704050638251", 1019 | "creditCard1": "4747927629687298" 1020 | }, 1021 | "gender": "Male" 1022 | }, 1023 | { 1024 | "firstName": "Tracy", 1025 | "lastName": "Maltas", 1026 | "visits": [ 1027 | { 1028 | "city": "Oslo", 1029 | "time": { 1030 | "year": 2020, 1031 | "month": 7, 1032 | "day": 31 1033 | } 1034 | }, 1035 | { 1036 | "city": "Oslo", 1037 | "time": { 1038 | "year": 2020, 1039 | "month": 5, 1040 | "day": 29 1041 | } 1042 | }, 1043 | { 1044 | "city": "Moscow", 1045 | "time": { 1046 | "year": 2020, 1047 | "month": 3, 1048 | "day": 4 1049 | } 1050 | }, 1051 | { 1052 | "city": "Riga", 1053 | "time": { 1054 | "year": 2020, 1055 | "month": 1, 1056 | "day": 24 1057 | } 1058 | }, 1059 | { 1060 | "city": "Belgrade", 1061 | "time": { 1062 | "year": 2020, 1063 | "month": 11, 1064 | "day": 5 1065 | } 1066 | } 1067 | ], 1068 | "financialInformation": { 1069 | "creditCard2": "371832760679735", 1070 | "creditCard1": "4510599931573584" 1071 | }, 1072 | "gender": "Male" 1073 | }, 1074 | { 1075 | "firstName": "Winston", 1076 | "lastName": "Menez", 1077 | "visits": [ 1078 | { 1079 | "city": "Moscow", 1080 | "time": { 1081 | "year": 2020, 1082 | "month": 7, 1083 | "day": 24 1084 | } 1085 | }, 1086 | { 1087 | "city": "Vatican City", 1088 | "time": { 1089 | "year": 2020, 1090 | "month": 7, 1091 | "day": 30 1092 | } 1093 | }, 1094 | { 1095 | "city": "Madrid", 1096 | "time": { 1097 | "year": 2020, 1098 | "month": 6, 1099 | "day": 5 1100 | } 1101 | }, 1102 | { 1103 | "city": "Lisbon", 1104 | "time": { 1105 | "year": 2020, 1106 | "month": 4, 1107 | "day": 13 1108 | } 1109 | }, 1110 | { 1111 | "city": "Tbilisi", 1112 | "time": { 1113 | "year": 2020, 1114 | "month": 6, 1115 | "day": 4 1116 | } 1117 | } 1118 | ], 1119 | "financialInformation": { 1120 | "creditCard2": "375294842215309", 1121 | "creditCard1": "4303083238072435" 1122 | }, 1123 | "gender": "Male" 1124 | }, 1125 | { 1126 | "firstName": "Alex", 1127 | "lastName": "Lietzke", 1128 | "visits": [ 1129 | { 1130 | "city": "Andorra la Vella", 1131 | "time": { 1132 | "year": 2020, 1133 | "month": 10, 1134 | "day": 23 1135 | } 1136 | }, 1137 | { 1138 | "city": "Zagreb", 1139 | "time": { 1140 | "year": 2020, 1141 | "month": 11, 1142 | "day": 8 1143 | } 1144 | }, 1145 | { 1146 | "city": "Moscow", 1147 | "time": { 1148 | "year": 2020, 1149 | "month": 2, 1150 | "day": 1 1151 | } 1152 | }, 1153 | { 1154 | "city": "Monaco", 1155 | "time": { 1156 | "year": 2020, 1157 | "month": 8, 1158 | "day": 17 1159 | } 1160 | }, 1161 | { 1162 | "city": "Vaduz", 1163 | "time": { 1164 | "year": 2020, 1165 | "month": 10, 1166 | "day": 28 1167 | } 1168 | } 1169 | ], 1170 | "financialInformation": { 1171 | "creditCard2": "348147857312227", 1172 | "creditCard1": "4593768093640248" 1173 | }, 1174 | "gender": "Male" 1175 | }, 1176 | { 1177 | "firstName": "Tad", 1178 | "lastName": "Dalmata", 1179 | "visits": [ 1180 | { 1181 | "city": "Stockholm", 1182 | "time": { 1183 | "year": 2020, 1184 | "month": 10, 1185 | "day": 14 1186 | } 1187 | }, 1188 | { 1189 | "city": "Skopje", 1190 | "time": { 1191 | "year": 2020, 1192 | "month": 3, 1193 | "day": 4 1194 | } 1195 | }, 1196 | { 1197 | "city": "Tbilisi", 1198 | "time": { 1199 | "year": 2020, 1200 | "month": 11, 1201 | "day": 15 1202 | } 1203 | }, 1204 | { 1205 | "city": "Lisbon", 1206 | "time": { 1207 | "year": 2020, 1208 | "month": 5, 1209 | "day": 31 1210 | } 1211 | }, 1212 | { 1213 | "city": "Dublin", 1214 | "time": { 1215 | "year": 2020, 1216 | "month": 5, 1217 | "day": 1 1218 | } 1219 | } 1220 | ], 1221 | "financialInformation": { 1222 | "creditCard2": "348969802837846", 1223 | "creditCard1": "4304407364271694" 1224 | }, 1225 | "gender": "Male" 1226 | }, 1227 | { 1228 | "firstName": "Dee", 1229 | "lastName": "Lecrone", 1230 | "visits": [ 1231 | { 1232 | "city": "Tbilisi", 1233 | "time": { 1234 | "year": 2020, 1235 | "month": 11, 1236 | "day": 29 1237 | } 1238 | }, 1239 | { 1240 | "city": "Warsaw", 1241 | "time": { 1242 | "year": 2020, 1243 | "month": 7, 1244 | "day": 19 1245 | } 1246 | }, 1247 | { 1248 | "city": "Ljubljana", 1249 | "time": { 1250 | "year": 2020, 1251 | "month": 12, 1252 | "day": 15 1253 | } 1254 | }, 1255 | { 1256 | "city": "Madrid", 1257 | "time": { 1258 | "year": 2020, 1259 | "month": 3, 1260 | "day": 18 1261 | } 1262 | }, 1263 | { 1264 | "city": "Yerevan", 1265 | "time": { 1266 | "year": 2020, 1267 | "month": 5, 1268 | "day": 4 1269 | } 1270 | } 1271 | ], 1272 | "financialInformation": { 1273 | "creditCard2": "372060399690001", 1274 | "creditCard1": "4706433742189030" 1275 | }, 1276 | "gender": "Male" 1277 | }, 1278 | { 1279 | "firstName": "Damien", 1280 | "lastName": "Duverne", 1281 | "visits": [ 1282 | { 1283 | "city": "Vienna", 1284 | "time": { 1285 | "year": 2020, 1286 | "month": 4, 1287 | "day": 11 1288 | } 1289 | }, 1290 | { 1291 | "city": "Budapest", 1292 | "time": { 1293 | "year": 2020, 1294 | "month": 12, 1295 | "day": 3 1296 | } 1297 | }, 1298 | { 1299 | "city": "Skopje", 1300 | "time": { 1301 | "year": 2020, 1302 | "month": 3, 1303 | "day": 3 1304 | } 1305 | }, 1306 | { 1307 | "city": "Dublin", 1308 | "time": { 1309 | "year": 2020, 1310 | "month": 2, 1311 | "day": 11 1312 | } 1313 | }, 1314 | { 1315 | "city": "Luxembourg", 1316 | "time": { 1317 | "year": 2020, 1318 | "month": 4, 1319 | "day": 16 1320 | } 1321 | } 1322 | ], 1323 | "financialInformation": { 1324 | "creditCard2": "373025354892073", 1325 | "creditCard1": "4807881186245882" 1326 | }, 1327 | "gender": "Male" 1328 | }, 1329 | { 1330 | "firstName": "Eusebio", 1331 | "lastName": "Colmenero", 1332 | "visits": [ 1333 | { 1334 | "city": "Valletta", 1335 | "time": { 1336 | "year": 2020, 1337 | "month": 7, 1338 | "day": 26 1339 | } 1340 | }, 1341 | { 1342 | "city": "Kiev", 1343 | "time": { 1344 | "year": 2020, 1345 | "month": 3, 1346 | "day": 9 1347 | } 1348 | }, 1349 | { 1350 | "city": "Tallinn", 1351 | "time": { 1352 | "year": 2020, 1353 | "month": 11, 1354 | "day": 5 1355 | } 1356 | }, 1357 | { 1358 | "city": "Prague", 1359 | "time": { 1360 | "year": 2020, 1361 | "month": 5, 1362 | "day": 20 1363 | } 1364 | }, 1365 | { 1366 | "city": "Luxembourg", 1367 | "time": { 1368 | "year": 2020, 1369 | "month": 3, 1370 | "day": 14 1371 | } 1372 | } 1373 | ], 1374 | "financialInformation": { 1375 | "creditCard2": "348602262940418", 1376 | "creditCard1": "4114785996823358" 1377 | }, 1378 | "gender": "Female" 1379 | }, 1380 | { 1381 | "firstName": "Ezequiel", 1382 | "lastName": "Farb", 1383 | "visits": [ 1384 | { 1385 | "city": "Zagreb", 1386 | "time": { 1387 | "year": 2020, 1388 | "month": 12, 1389 | "day": 1 1390 | } 1391 | }, 1392 | { 1393 | "city": "Chisinau", 1394 | "time": { 1395 | "year": 2020, 1396 | "month": 4, 1397 | "day": 7 1398 | } 1399 | }, 1400 | { 1401 | "city": "Warsaw", 1402 | "time": { 1403 | "year": 2020, 1404 | "month": 6, 1405 | "day": 5 1406 | } 1407 | }, 1408 | { 1409 | "city": "Madrid", 1410 | "time": { 1411 | "year": 2020, 1412 | "month": 5, 1413 | "day": 15 1414 | } 1415 | }, 1416 | { 1417 | "city": "Minsk", 1418 | "time": { 1419 | "year": 2020, 1420 | "month": 6, 1421 | "day": 13 1422 | } 1423 | } 1424 | ], 1425 | "financialInformation": { 1426 | "creditCard2": "348763807543156", 1427 | "creditCard1": "4731749525545539" 1428 | }, 1429 | "gender": "Male" 1430 | }, 1431 | { 1432 | "firstName": "Colton", 1433 | "lastName": "Dimon", 1434 | "visits": [ 1435 | { 1436 | "city": "Moscow", 1437 | "time": { 1438 | "year": 2020, 1439 | "month": 6, 1440 | "day": 12 1441 | } 1442 | }, 1443 | { 1444 | "city": "Sarajevo", 1445 | "time": { 1446 | "year": 2020, 1447 | "month": 7, 1448 | "day": 1 1449 | } 1450 | }, 1451 | { 1452 | "city": "Tallinn", 1453 | "time": { 1454 | "year": 2020, 1455 | "month": 11, 1456 | "day": 2 1457 | } 1458 | }, 1459 | { 1460 | "city": "Rome", 1461 | "time": { 1462 | "year": 2020, 1463 | "month": 11, 1464 | "day": 19 1465 | } 1466 | }, 1467 | { 1468 | "city": "Reykjavík", 1469 | "time": { 1470 | "year": 2020, 1471 | "month": 3, 1472 | "day": 25 1473 | } 1474 | } 1475 | ], 1476 | "financialInformation": { 1477 | "creditCard2": "341659641569851", 1478 | "creditCard1": "4248941984191287" 1479 | }, 1480 | "gender": "Male" 1481 | }, 1482 | { 1483 | "firstName": "Peter", 1484 | "lastName": "Kahrer", 1485 | "visits": [ 1486 | { 1487 | "city": "Belgrade", 1488 | "time": { 1489 | "year": 2020, 1490 | "month": 9, 1491 | "day": 21 1492 | } 1493 | }, 1494 | { 1495 | "city": "Valletta", 1496 | "time": { 1497 | "year": 2020, 1498 | "month": 8, 1499 | "day": 16 1500 | } 1501 | }, 1502 | { 1503 | "city": "San Marino", 1504 | "time": { 1505 | "year": 2020, 1506 | "month": 3, 1507 | "day": 2 1508 | } 1509 | }, 1510 | { 1511 | "city": "Berlin", 1512 | "time": { 1513 | "year": 2020, 1514 | "month": 10, 1515 | "day": 29 1516 | } 1517 | }, 1518 | { 1519 | "city": "Bratislava", 1520 | "time": { 1521 | "year": 2020, 1522 | "month": 3, 1523 | "day": 3 1524 | } 1525 | } 1526 | ], 1527 | "financialInformation": { 1528 | "creditCard2": "340341926231025", 1529 | "creditCard1": "4155414070566961" 1530 | }, 1531 | "gender": "Female" 1532 | }, 1533 | { 1534 | "firstName": "Derick", 1535 | "lastName": "Erber", 1536 | "visits": [ 1537 | { 1538 | "city": "Budapest", 1539 | "time": { 1540 | "year": 2020, 1541 | "month": 11, 1542 | "day": 29 1543 | } 1544 | }, 1545 | { 1546 | "city": "Ankara", 1547 | "time": { 1548 | "year": 2020, 1549 | "month": 12, 1550 | "day": 26 1551 | } 1552 | }, 1553 | { 1554 | "city": "London", 1555 | "time": { 1556 | "year": 2020, 1557 | "month": 12, 1558 | "day": 6 1559 | } 1560 | }, 1561 | { 1562 | "city": "Chisinau", 1563 | "time": { 1564 | "year": 2020, 1565 | "month": 1, 1566 | "day": 17 1567 | } 1568 | }, 1569 | { 1570 | "city": "Lisbon", 1571 | "time": { 1572 | "year": 2020, 1573 | "month": 7, 1574 | "day": 30 1575 | } 1576 | } 1577 | ], 1578 | "financialInformation": { 1579 | "creditCard2": "345591868254535", 1580 | "creditCard1": "4982647713173377" 1581 | }, 1582 | "gender": "Male" 1583 | }, 1584 | { 1585 | "firstName": "Mickey", 1586 | "lastName": "Swanberg", 1587 | "visits": [ 1588 | { 1589 | "city": "Sofia", 1590 | "time": { 1591 | "year": 2020, 1592 | "month": 5, 1593 | "day": 17 1594 | } 1595 | }, 1596 | { 1597 | "city": "Paris", 1598 | "time": { 1599 | "year": 2020, 1600 | "month": 1, 1601 | "day": 24 1602 | } 1603 | }, 1604 | { 1605 | "city": "Prague", 1606 | "time": { 1607 | "year": 2020, 1608 | "month": 4, 1609 | "day": 5 1610 | } 1611 | }, 1612 | { 1613 | "city": "Minsk", 1614 | "time": { 1615 | "year": 2020, 1616 | "month": 3, 1617 | "day": 22 1618 | } 1619 | }, 1620 | { 1621 | "city": "Dublin", 1622 | "time": { 1623 | "year": 2020, 1624 | "month": 3, 1625 | "day": 26 1626 | } 1627 | } 1628 | ], 1629 | "financialInformation": { 1630 | "creditCard2": "349080406346280", 1631 | "creditCard1": "4969163345242900" 1632 | }, 1633 | "gender": "Male" 1634 | }, 1635 | { 1636 | "firstName": "Ronald", 1637 | "lastName": "Pillot", 1638 | "visits": [ 1639 | { 1640 | "city": "Skopje", 1641 | "time": { 1642 | "year": 2020, 1643 | "month": 12, 1644 | "day": 29 1645 | } 1646 | }, 1647 | { 1648 | "city": "Paris", 1649 | "time": { 1650 | "year": 2020, 1651 | "month": 2, 1652 | "day": 13 1653 | } 1654 | }, 1655 | { 1656 | "city": "Vatican City", 1657 | "time": { 1658 | "year": 2020, 1659 | "month": 8, 1660 | "day": 5 1661 | } 1662 | }, 1663 | { 1664 | "city": "Vatican City", 1665 | "time": { 1666 | "year": 2020, 1667 | "month": 7, 1668 | "day": 9 1669 | } 1670 | }, 1671 | { 1672 | "city": "Brussels", 1673 | "time": { 1674 | "year": 2020, 1675 | "month": 6, 1676 | "day": 19 1677 | } 1678 | } 1679 | ], 1680 | "financialInformation": { 1681 | "creditCard2": "343843781208763", 1682 | "creditCard1": "4065483470945389" 1683 | }, 1684 | "gender": "Male" 1685 | }, 1686 | { 1687 | "firstName": "Vito", 1688 | "lastName": "Seitz", 1689 | "visits": [ 1690 | { 1691 | "city": "Nicosia", 1692 | "time": { 1693 | "year": 2020, 1694 | "month": 1, 1695 | "day": 7 1696 | } 1697 | }, 1698 | { 1699 | "city": "Tirana", 1700 | "time": { 1701 | "year": 2020, 1702 | "month": 1, 1703 | "day": 10 1704 | } 1705 | }, 1706 | { 1707 | "city": "Sarajevo", 1708 | "time": { 1709 | "year": 2020, 1710 | "month": 4, 1711 | "day": 18 1712 | } 1713 | }, 1714 | { 1715 | "city": "Podgorica", 1716 | "time": { 1717 | "year": 2020, 1718 | "month": 6, 1719 | "day": 16 1720 | } 1721 | }, 1722 | { 1723 | "city": "Stockholm", 1724 | "time": { 1725 | "year": 2020, 1726 | "month": 3, 1727 | "day": 11 1728 | } 1729 | } 1730 | ], 1731 | "financialInformation": { 1732 | "creditCard2": "342545254767826", 1733 | "creditCard1": "4511644134222883" 1734 | }, 1735 | "gender": "Female" 1736 | }, 1737 | { 1738 | "firstName": "Deangelo", 1739 | "lastName": "Mesenbrink", 1740 | "visits": [ 1741 | { 1742 | "city": "Helsinki", 1743 | "time": { 1744 | "year": 2020, 1745 | "month": 12, 1746 | "day": 24 1747 | } 1748 | }, 1749 | { 1750 | "city": "Brussels", 1751 | "time": { 1752 | "year": 2020, 1753 | "month": 10, 1754 | "day": 16 1755 | } 1756 | }, 1757 | { 1758 | "city": "Moscow", 1759 | "time": { 1760 | "year": 2020, 1761 | "month": 7, 1762 | "day": 18 1763 | } 1764 | }, 1765 | { 1766 | "city": "Stockholm", 1767 | "time": { 1768 | "year": 2020, 1769 | "month": 11, 1770 | "day": 15 1771 | } 1772 | }, 1773 | { 1774 | "city": "Podgorica", 1775 | "time": { 1776 | "year": 2020, 1777 | "month": 1, 1778 | "day": 22 1779 | } 1780 | } 1781 | ], 1782 | "financialInformation": { 1783 | "creditCard2": "378119502759006", 1784 | "creditCard1": "4044839066939845" 1785 | }, 1786 | "gender": "Female" 1787 | }, 1788 | { 1789 | "firstName": "Valentin", 1790 | "lastName": "Tanouye", 1791 | "visits": [ 1792 | { 1793 | "city": "Vilnius", 1794 | "time": { 1795 | "year": 2020, 1796 | "month": 3, 1797 | "day": 1 1798 | } 1799 | }, 1800 | { 1801 | "city": "Chisinau", 1802 | "time": { 1803 | "year": 2020, 1804 | "month": 2, 1805 | "day": 11 1806 | } 1807 | }, 1808 | { 1809 | "city": "Riga", 1810 | "time": { 1811 | "year": 2020, 1812 | "month": 12, 1813 | "day": 3 1814 | } 1815 | }, 1816 | { 1817 | "city": "Tallinn", 1818 | "time": { 1819 | "year": 2020, 1820 | "month": 8, 1821 | "day": 9 1822 | } 1823 | }, 1824 | { 1825 | "city": "Belgrade", 1826 | "time": { 1827 | "year": 2020, 1828 | "month": 9, 1829 | "day": 6 1830 | } 1831 | } 1832 | ], 1833 | "financialInformation": { 1834 | "creditCard2": "378159782718504", 1835 | "creditCard1": "4335837195410934" 1836 | }, 1837 | "gender": "Male" 1838 | }, 1839 | { 1840 | "firstName": "Eli", 1841 | "lastName": "Statzer", 1842 | "visits": [ 1843 | { 1844 | "city": "Chisinau", 1845 | "time": { 1846 | "year": 2020, 1847 | "month": 9, 1848 | "day": 8 1849 | } 1850 | }, 1851 | { 1852 | "city": "Sarajevo", 1853 | "time": { 1854 | "year": 2020, 1855 | "month": 9, 1856 | "day": 6 1857 | } 1858 | }, 1859 | { 1860 | "city": "Madrid", 1861 | "time": { 1862 | "year": 2020, 1863 | "month": 3, 1864 | "day": 29 1865 | } 1866 | }, 1867 | { 1868 | "city": "Belgrade", 1869 | "time": { 1870 | "year": 2020, 1871 | "month": 10, 1872 | "day": 23 1873 | } 1874 | }, 1875 | { 1876 | "city": "Tbilisi", 1877 | "time": { 1878 | "year": 2020, 1879 | "month": 3, 1880 | "day": 20 1881 | } 1882 | } 1883 | ], 1884 | "financialInformation": { 1885 | "creditCard2": "370980340996102", 1886 | "creditCard1": "4862965883930268" 1887 | }, 1888 | "gender": "Male" 1889 | }, 1890 | { 1891 | "firstName": "Judson", 1892 | "lastName": "Winnie", 1893 | "visits": [ 1894 | { 1895 | "city": "Prague", 1896 | "time": { 1897 | "year": 2020, 1898 | "month": 5, 1899 | "day": 14 1900 | } 1901 | }, 1902 | { 1903 | "city": "Brussels", 1904 | "time": { 1905 | "year": 2020, 1906 | "month": 2, 1907 | "day": 26 1908 | } 1909 | }, 1910 | { 1911 | "city": "Dublin", 1912 | "time": { 1913 | "year": 2020, 1914 | "month": 12, 1915 | "day": 5 1916 | } 1917 | }, 1918 | { 1919 | "city": "Tallinn", 1920 | "time": { 1921 | "year": 2020, 1922 | "month": 11, 1923 | "day": 17 1924 | } 1925 | }, 1926 | { 1927 | "city": "Riga", 1928 | "time": { 1929 | "year": 2020, 1930 | "month": 8, 1931 | "day": 9 1932 | } 1933 | } 1934 | ], 1935 | "financialInformation": { 1936 | "creditCard2": "347308429975716", 1937 | "creditCard1": "4673751374773626" 1938 | }, 1939 | "gender": "Female" 1940 | }, 1941 | { 1942 | "firstName": "Shaun", 1943 | "lastName": "Sandage", 1944 | "visits": [ 1945 | { 1946 | "city": "Astana", 1947 | "time": { 1948 | "year": 2020, 1949 | "month": 3, 1950 | "day": 14 1951 | } 1952 | }, 1953 | { 1954 | "city": "Vaduz", 1955 | "time": { 1956 | "year": 2020, 1957 | "month": 3, 1958 | "day": 26 1959 | } 1960 | }, 1961 | { 1962 | "city": "Chisinau", 1963 | "time": { 1964 | "year": 2020, 1965 | "month": 10, 1966 | "day": 4 1967 | } 1968 | }, 1969 | { 1970 | "city": "Tallinn", 1971 | "time": { 1972 | "year": 2020, 1973 | "month": 4, 1974 | "day": 6 1975 | } 1976 | }, 1977 | { 1978 | "city": "Reykjavík", 1979 | "time": { 1980 | "year": 2020, 1981 | "month": 3, 1982 | "day": 21 1983 | } 1984 | } 1985 | ], 1986 | "financialInformation": { 1987 | "creditCard2": "376550148430788", 1988 | "creditCard1": "4196144774195020" 1989 | }, 1990 | "gender": "Male" 1991 | }, 1992 | { 1993 | "firstName": "Caleb", 1994 | "lastName": "Muhr", 1995 | "visits": [ 1996 | { 1997 | "city": "Budapest", 1998 | "time": { 1999 | "year": 2020, 2000 | "month": 2, 2001 | "day": 17 2002 | } 2003 | }, 2004 | { 2005 | "city": "Vilnius", 2006 | "time": { 2007 | "year": 2020, 2008 | "month": 1, 2009 | "day": 31 2010 | } 2011 | }, 2012 | { 2013 | "city": "Vaduz", 2014 | "time": { 2015 | "year": 2020, 2016 | "month": 9, 2017 | "day": 29 2018 | } 2019 | }, 2020 | { 2021 | "city": "Lisbon", 2022 | "time": { 2023 | "year": 2020, 2024 | "month": 8, 2025 | "day": 26 2026 | } 2027 | }, 2028 | { 2029 | "city": "Riga", 2030 | "time": { 2031 | "year": 2020, 2032 | "month": 3, 2033 | "day": 31 2034 | } 2035 | } 2036 | ], 2037 | "financialInformation": { 2038 | "creditCard2": "342613233199071", 2039 | "creditCard1": "4102771815954370" 2040 | }, 2041 | "gender": "Female" 2042 | }, 2043 | { 2044 | "firstName": "Stanton", 2045 | "lastName": "Snorton", 2046 | "visits": [ 2047 | { 2048 | "city": "Valletta", 2049 | "time": { 2050 | "year": 2020, 2051 | "month": 4, 2052 | "day": 21 2053 | } 2054 | }, 2055 | { 2056 | "city": "Sarajevo", 2057 | "time": { 2058 | "year": 2020, 2059 | "month": 3, 2060 | "day": 16 2061 | } 2062 | }, 2063 | { 2064 | "city": "Luxembourg", 2065 | "time": { 2066 | "year": 2020, 2067 | "month": 4, 2068 | "day": 5 2069 | } 2070 | }, 2071 | { 2072 | "city": "Sarajevo", 2073 | "time": { 2074 | "year": 2020, 2075 | "month": 3, 2076 | "day": 5 2077 | } 2078 | }, 2079 | { 2080 | "city": "Ankara", 2081 | "time": { 2082 | "year": 2020, 2083 | "month": 8, 2084 | "day": 19 2085 | } 2086 | } 2087 | ], 2088 | "financialInformation": { 2089 | "creditCard2": "377043275551724", 2090 | "creditCard1": "4748494087656983" 2091 | }, 2092 | "gender": "Male" 2093 | }, 2094 | { 2095 | "firstName": "Hugh", 2096 | "lastName": "Poeppelman", 2097 | "visits": [ 2098 | { 2099 | "city": "Helsinki", 2100 | "time": { 2101 | "year": 2020, 2102 | "month": 8, 2103 | "day": 15 2104 | } 2105 | }, 2106 | { 2107 | "city": "Amsterdam", 2108 | "time": { 2109 | "year": 2020, 2110 | "month": 10, 2111 | "day": 23 2112 | } 2113 | }, 2114 | { 2115 | "city": "Vaduz", 2116 | "time": { 2117 | "year": 2020, 2118 | "month": 3, 2119 | "day": 28 2120 | } 2121 | }, 2122 | { 2123 | "city": "Bucharest", 2124 | "time": { 2125 | "year": 2020, 2126 | "month": 3, 2127 | "day": 26 2128 | } 2129 | }, 2130 | { 2131 | "city": "Tirana", 2132 | "time": { 2133 | "year": 2020, 2134 | "month": 2, 2135 | "day": 10 2136 | } 2137 | } 2138 | ], 2139 | "financialInformation": { 2140 | "creditCard2": "341025382188923", 2141 | "creditCard1": "4580536827275093" 2142 | }, 2143 | "gender": "Female" 2144 | }, 2145 | { 2146 | "firstName": "Domenic", 2147 | "lastName": "Ophus", 2148 | "visits": [ 2149 | { 2150 | "city": "Luxembourg", 2151 | "time": { 2152 | "year": 2020, 2153 | "month": 12, 2154 | "day": 22 2155 | } 2156 | }, 2157 | { 2158 | "city": "Bern", 2159 | "time": { 2160 | "year": 2020, 2161 | "month": 6, 2162 | "day": 24 2163 | } 2164 | }, 2165 | { 2166 | "city": "Kiev", 2167 | "time": { 2168 | "year": 2020, 2169 | "month": 5, 2170 | "day": 9 2171 | } 2172 | }, 2173 | { 2174 | "city": "Ljubljana", 2175 | "time": { 2176 | "year": 2020, 2177 | "month": 7, 2178 | "day": 27 2179 | } 2180 | }, 2181 | { 2182 | "city": "San Marino", 2183 | "time": { 2184 | "year": 2020, 2185 | "month": 3, 2186 | "day": 6 2187 | } 2188 | } 2189 | ], 2190 | "financialInformation": { 2191 | "creditCard2": "346733753262434", 2192 | "creditCard1": "4609740799906738" 2193 | }, 2194 | "gender": "Female" 2195 | }, 2196 | { 2197 | "firstName": "Luther", 2198 | "lastName": "Schalow", 2199 | "visits": [ 2200 | { 2201 | "city": "Skopje", 2202 | "time": { 2203 | "year": 2020, 2204 | "month": 11, 2205 | "day": 11 2206 | } 2207 | }, 2208 | { 2209 | "city": "Ankara", 2210 | "time": { 2211 | "year": 2020, 2212 | "month": 10, 2213 | "day": 27 2214 | } 2215 | }, 2216 | { 2217 | "city": "Dublin", 2218 | "time": { 2219 | "year": 2020, 2220 | "month": 12, 2221 | "day": 26 2222 | } 2223 | }, 2224 | { 2225 | "city": "Chisinau", 2226 | "time": { 2227 | "year": 2020, 2228 | "month": 1, 2229 | "day": 29 2230 | } 2231 | }, 2232 | { 2233 | "city": "Reykjavík", 2234 | "time": { 2235 | "year": 2020, 2236 | "month": 2, 2237 | "day": 2 2238 | } 2239 | } 2240 | ], 2241 | "financialInformation": { 2242 | "creditCard2": "370163046583702", 2243 | "creditCard1": "4697425748888806" 2244 | }, 2245 | "gender": "Female" 2246 | }, 2247 | { 2248 | "firstName": "Marquis", 2249 | "lastName": "Connelley", 2250 | "visits": [ 2251 | { 2252 | "city": "Minsk", 2253 | "time": { 2254 | "year": 2020, 2255 | "month": 1, 2256 | "day": 21 2257 | } 2258 | }, 2259 | { 2260 | "city": "Luxembourg", 2261 | "time": { 2262 | "year": 2020, 2263 | "month": 8, 2264 | "day": 29 2265 | } 2266 | }, 2267 | { 2268 | "city": "Astana", 2269 | "time": { 2270 | "year": 2020, 2271 | "month": 8, 2272 | "day": 24 2273 | } 2274 | }, 2275 | { 2276 | "city": "Nicosia", 2277 | "time": { 2278 | "year": 2020, 2279 | "month": 7, 2280 | "day": 29 2281 | } 2282 | }, 2283 | { 2284 | "city": "Vatican City", 2285 | "time": { 2286 | "year": 2020, 2287 | "month": 7, 2288 | "day": 1 2289 | } 2290 | } 2291 | ], 2292 | "financialInformation": { 2293 | "creditCard2": "343874360867521", 2294 | "creditCard1": "4359238437264950" 2295 | }, 2296 | "gender": "Male" 2297 | }, 2298 | { 2299 | "firstName": "Frank", 2300 | "lastName": "Akhtar", 2301 | "visits": [ 2302 | { 2303 | "city": "Copenhagen", 2304 | "time": { 2305 | "year": 2020, 2306 | "month": 12, 2307 | "day": 5 2308 | } 2309 | }, 2310 | { 2311 | "city": "Brussels", 2312 | "time": { 2313 | "year": 2020, 2314 | "month": 3, 2315 | "day": 18 2316 | } 2317 | }, 2318 | { 2319 | "city": "Belgrade", 2320 | "time": { 2321 | "year": 2020, 2322 | "month": 5, 2323 | "day": 31 2324 | } 2325 | }, 2326 | { 2327 | "city": "Vaduz", 2328 | "time": { 2329 | "year": 2020, 2330 | "month": 9, 2331 | "day": 24 2332 | } 2333 | }, 2334 | { 2335 | "city": "Moscow", 2336 | "time": { 2337 | "year": 2020, 2338 | "month": 1, 2339 | "day": 5 2340 | } 2341 | } 2342 | ], 2343 | "financialInformation": { 2344 | "creditCard2": "376771728855620", 2345 | "creditCard1": "4223074807948169" 2346 | }, 2347 | "gender": "Female" 2348 | }, 2349 | { 2350 | "firstName": "Thanh", 2351 | "lastName": "Macadamia", 2352 | "visits": [ 2353 | { 2354 | "city": "Minsk", 2355 | "time": { 2356 | "year": 2020, 2357 | "month": 2, 2358 | "day": 8 2359 | } 2360 | }, 2361 | { 2362 | "city": "Andorra la Vella", 2363 | "time": { 2364 | "year": 2020, 2365 | "month": 4, 2366 | "day": 30 2367 | } 2368 | }, 2369 | { 2370 | "city": "Lisbon", 2371 | "time": { 2372 | "year": 2020, 2373 | "month": 4, 2374 | "day": 11 2375 | } 2376 | }, 2377 | { 2378 | "city": "Astana", 2379 | "time": { 2380 | "year": 2020, 2381 | "month": 10, 2382 | "day": 14 2383 | } 2384 | }, 2385 | { 2386 | "city": "Athens", 2387 | "time": { 2388 | "year": 2020, 2389 | "month": 8, 2390 | "day": 30 2391 | } 2392 | } 2393 | ], 2394 | "financialInformation": { 2395 | "creditCard2": "378322368292662", 2396 | "creditCard1": "4396592087768339" 2397 | }, 2398 | "gender": "Male" 2399 | }, 2400 | { 2401 | "firstName": "Wilbur", 2402 | "lastName": "Rensing", 2403 | "visits": [ 2404 | { 2405 | "city": "Oslo", 2406 | "time": { 2407 | "year": 2020, 2408 | "month": 6, 2409 | "day": 26 2410 | } 2411 | }, 2412 | { 2413 | "city": "Berlin", 2414 | "time": { 2415 | "year": 2020, 2416 | "month": 10, 2417 | "day": 3 2418 | } 2419 | }, 2420 | { 2421 | "city": "Skopje", 2422 | "time": { 2423 | "year": 2020, 2424 | "month": 1, 2425 | "day": 3 2426 | } 2427 | }, 2428 | { 2429 | "city": "Skopje", 2430 | "time": { 2431 | "year": 2020, 2432 | "month": 10, 2433 | "day": 7 2434 | } 2435 | }, 2436 | { 2437 | "city": "Tallinn", 2438 | "time": { 2439 | "year": 2020, 2440 | "month": 11, 2441 | "day": 19 2442 | } 2443 | } 2444 | ], 2445 | "financialInformation": { 2446 | "creditCard2": "346360286939344", 2447 | "creditCard1": "4807739489718338" 2448 | }, 2449 | "gender": "Female" 2450 | }, 2451 | { 2452 | "firstName": "Virgil", 2453 | "lastName": "Etzkorn", 2454 | "visits": [ 2455 | { 2456 | "city": "Bucharest", 2457 | "time": { 2458 | "year": 2020, 2459 | "month": 10, 2460 | "day": 4 2461 | } 2462 | }, 2463 | { 2464 | "city": "Sarajevo", 2465 | "time": { 2466 | "year": 2020, 2467 | "month": 2, 2468 | "day": 13 2469 | } 2470 | }, 2471 | { 2472 | "city": "Bucharest", 2473 | "time": { 2474 | "year": 2020, 2475 | "month": 7, 2476 | "day": 24 2477 | } 2478 | }, 2479 | { 2480 | "city": "Monaco", 2481 | "time": { 2482 | "year": 2020, 2483 | "month": 5, 2484 | "day": 12 2485 | } 2486 | }, 2487 | { 2488 | "city": "Yerevan", 2489 | "time": { 2490 | "year": 2020, 2491 | "month": 9, 2492 | "day": 13 2493 | } 2494 | } 2495 | ], 2496 | "financialInformation": { 2497 | "creditCard2": "343500637622296", 2498 | "creditCard1": "4103058343533440" 2499 | }, 2500 | "gender": "Male" 2501 | }, 2502 | { 2503 | "firstName": "Herbert", 2504 | "lastName": "Diserens", 2505 | "visits": [ 2506 | { 2507 | "city": "Minsk", 2508 | "time": { 2509 | "year": 2020, 2510 | "month": 6, 2511 | "day": 18 2512 | } 2513 | }, 2514 | { 2515 | "city": "Minsk", 2516 | "time": { 2517 | "year": 2020, 2518 | "month": 12, 2519 | "day": 24 2520 | } 2521 | }, 2522 | { 2523 | "city": "Vaduz", 2524 | "time": { 2525 | "year": 2020, 2526 | "month": 10, 2527 | "day": 26 2528 | } 2529 | }, 2530 | { 2531 | "city": "Baku", 2532 | "time": { 2533 | "year": 2020, 2534 | "month": 5, 2535 | "day": 16 2536 | } 2537 | }, 2538 | { 2539 | "city": "Zagreb", 2540 | "time": { 2541 | "year": 2020, 2542 | "month": 5, 2543 | "day": 22 2544 | } 2545 | } 2546 | ], 2547 | "financialInformation": { 2548 | "creditCard2": "374189671009321", 2549 | "creditCard1": "4915397428998260" 2550 | }, 2551 | "gender": "Male" 2552 | } 2553 | ] 2554 | } -------------------------------------------------------------------------------- /examples/json-response-dynamic-mockneat.kts: -------------------------------------------------------------------------------- 1 | import net.andreinc.mockneat.unit.address.Cities.cities 2 | import net.andreinc.mockneat.unit.financial.CreditCards.creditCards 3 | import net.andreinc.mockneat.unit.time.LocalDates.localDates 4 | import net.andreinc.mockneat.unit.user.Genders.genders 5 | import net.andreinc.mockneat.unit.user.Names.names 6 | import net.andreinc.serverneat.mockneat.extension.obj 7 | import net.andreinc.serverneat.server.server 8 | 9 | server { 10 | 11 | httpOptions { 12 | host = "localhost" 13 | port = 8081 14 | } 15 | 16 | globalHeaders { 17 | header("Content-Type", "application/json") 18 | } 19 | 20 | routes { 21 | get { 22 | path = "/users" 23 | response { 24 | statusCode = 200 25 | json { 26 | persistent = true // generated data will be stored in the file "usersList.json" 27 | file = "dyanmic-example/usersList.json" 28 | value = obj { 29 | "users" value obj { 30 | "firstName" value names().first() 31 | "lastName" value names().last() 32 | "gender" value genders() 33 | "financialInformation" value obj { 34 | "creditCard1" value creditCards().visa() 35 | "creditCard2" value creditCards().amex() 36 | } 37 | "visits" value obj { 38 | "time" value localDates().thisYear() 39 | "city" value cities().capitalsEurope() 40 | }.list(5) 41 | }.list(50) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | }.start() -------------------------------------------------------------------------------- /examples/json-response-simple.kts: -------------------------------------------------------------------------------- 1 | import net.andreinc.serverneat.mockneat.extension.obj 2 | import net.andreinc.serverneat.server.server 3 | 4 | server { 5 | 6 | httpOptions { 7 | host = "localhost" 8 | port = 8081 9 | } 10 | 11 | globalHeaders { 12 | header("Content-Type", "application/json") 13 | } 14 | 15 | routes { 16 | get { 17 | path = "/user/100" 18 | response { 19 | header("plain", "text") // Adding a custom header to the response 20 | statusCode = 200 21 | json { 22 | value = obj { 23 | "firstName" const "Mike" 24 | "lastName" const "Smith" 25 | "someFiles" const arrayOf("file1.txt", "file2.txt") 26 | "anotherObject" const obj { 27 | "someData" const "someValue" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | 36 | }.start() -------------------------------------------------------------------------------- /examples/plain-text-responses.kts: -------------------------------------------------------------------------------- 1 | import net.andreinc.serverneat.server.server 2 | 3 | server { 4 | 5 | httpOptions { 6 | host = "localhost" 7 | port = 8081 8 | } 9 | 10 | globalHeaders { 11 | header("Content-Type", "application/text") 12 | } 13 | 14 | routes { 15 | get { 16 | path = "/plainText" 17 | response { 18 | header("plain", "text") // Adding a custom header to the response 19 | statusCode = 200 20 | plainText { 21 | value = "Hello World!" 22 | } 23 | } 24 | } 25 | } 26 | 27 | 28 | }.start() -------------------------------------------------------------------------------- /examples/rest-crud-example.kts: -------------------------------------------------------------------------------- 1 | import net.andreinc.mockneat.unit.financial.CreditCards.creditCards 2 | import net.andreinc.mockneat.unit.user.Names.names 3 | import net.andreinc.serverneat.mockneat.extension.obj 4 | import net.andreinc.serverneat.server.server 5 | 6 | server { 7 | 8 | httpOptions { 9 | host = "localhost" 10 | port = 8081 11 | } 12 | 13 | globalHeaders { 14 | header("Content-Type", "application/json") 15 | } 16 | 17 | routes { 18 | 19 | get { 20 | path = "/user/list" 21 | response { 22 | statusCode = 200 23 | json { 24 | persistent = true 25 | file = "userList.json" 26 | value = obj { 27 | "users" value obj { 28 | "firstName" value names().first() 29 | "lastName" value names().last() 30 | "creditCards" value creditCards().list(10) 31 | }.list(10) 32 | } 33 | } 34 | } 35 | } 36 | 37 | get { 38 | path = "/user/1000" 39 | response { 40 | statusCode = 200 41 | header("head1", "value1") 42 | header("head2", "value2") 43 | json { 44 | persistent = true 45 | file = "userProfile.json" 46 | value = obj { 47 | "user" value obj { 48 | "firstName" const "Andrew" 49 | "lastName" const "Smith" 50 | "creditCards" value creditCards().list(15) 51 | "userId" const 1000 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | get { 59 | path = "/hello/world" 60 | response { 61 | header("hello", "world") 62 | file { 63 | file = "data/static/test.txt" 64 | } 65 | } 66 | } 67 | 68 | get { 69 | path = "/download" 70 | response { 71 | fileDownload { 72 | file = "data/static/test.txt" 73 | } 74 | } 75 | } 76 | 77 | head { 78 | path = "/user/sanity" 79 | response { 80 | statusCode = 200 81 | plainText { 82 | value = "Hello world" 83 | } 84 | } 85 | } 86 | } 87 | 88 | }.start() 89 | 90 | -------------------------------------------------------------------------------- /examples/static/test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomemory/serverneat/13e34a1e4cab05dade445f1ad5495cb3956e97fc/examples/static/test.txt -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'serverneat' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import mu.KotlinLogging 2 | import net.andreinc.serverneat.CliApp 3 | import net.andreinc.serverneat.logging.ansi 4 | import picocli.CommandLine 5 | 6 | private val logger = KotlinLogging.logger { } 7 | 8 | fun main(args: Array) { 9 | logger.info { ansi("Initialising {logoNeat ServerNeat}") } 10 | CommandLine(CliApp()).execute(*args) 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/MainContext.kt: -------------------------------------------------------------------------------- 1 | import java.nio.charset.Charset 2 | 3 | class MainContext { 4 | companion object { 5 | fun readResource(resource: String, charset: Charset = Charsets.UTF_8) : String { 6 | val resourceUrl = this::class.java.getResource(resource) 7 | return resourceUrl.readText(charset) 8 | } 9 | fun currentClassLoader() : ClassLoader { 10 | return Thread.currentThread().contextClassLoader 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/serverneat/CliApp.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat 2 | 3 | import picocli.CommandLine.Command 4 | import picocli.CommandLine.Option 5 | import java.util.concurrent.Callable 6 | 7 | @Command( 8 | name = "serverneat", 9 | version = ["1.0alpha"], 10 | mixinStandardHelpOptions = false 11 | ) 12 | class CliApp : Callable { 13 | 14 | @Option( 15 | required = true, 16 | names = ["-f", "--file"], 17 | paramLabel = "FILE", 18 | description = ["The relative path to the '.kts' script containing the server"] 19 | ) 20 | private lateinit var file : String 21 | 22 | @Option( 23 | required = false, 24 | names = [ "-e", "--evaluation-only" ], 25 | paramLabel = "EVALUATION_ONLY", 26 | description = [ "The .kts script is only evaluated, without compilation (can increase startup time)." ] 27 | ) 28 | private var evaluationOnly : Boolean = false 29 | 30 | override fun call(): Int { 31 | KotlinScriptRunner.evalFile(file, readAsResource = false, compileFirst = !evaluationOnly); 32 | return 0 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/serverneat/KotlinScriptRunner.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat 2 | 3 | import MainContext.Companion.currentClassLoader 4 | import MainContext.Companion.readResource 5 | import mu.KotlinLogging 6 | import net.andreinc.serverneat.logging.ansi 7 | import java.io.File 8 | import java.lang.IllegalStateException 9 | import javax.script.Compilable 10 | import javax.script.ScriptEngine 11 | import javax.script.ScriptEngineManager 12 | 13 | 14 | private val logger = KotlinLogging.logger { } 15 | 16 | object KotlinScriptRunner { 17 | 18 | private val kotlinEngine: ScriptEngine = 19 | ScriptEngineManager(currentClassLoader()) 20 | .getEngineByExtension("kts") 21 | 22 | 23 | fun eval(scriptContent: String, compileFirst: Boolean = true) { 24 | if (compileFirst) { 25 | when (kotlinEngine) { 26 | is Compilable -> { 27 | logger.info { "Compiling script content "} 28 | val compiledCode = kotlinEngine.compile(scriptContent) 29 | logger.info { "Running compiled script content "} 30 | compiledCode.eval() 31 | } 32 | else -> throw IllegalStateException("Kotlin is not an instance of Compilable. Cannot compile script.") 33 | } 34 | } 35 | else { 36 | logger.info { "Running script content (without compilation)"} 37 | kotlinEngine.eval(scriptContent) 38 | } 39 | } 40 | 41 | fun evalFile(file: String, readAsResource: Boolean, compileFirst: Boolean = true) { 42 | logger.info { ansi("Loading script: {file $file} ${if (readAsResource) "(as a resource)" else ""} with compilation flag set to {b true}") } 43 | val content : String = if (readAsResource) readResource(file) else File(file).readText() 44 | eval(content, compileFirst) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/serverneat/logging/AnsiScapeConfig.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.logging 2 | 3 | import mu.KotlinLogging 4 | import net.andreinc.ansiscape.AnsiClass 5 | import net.andreinc.ansiscape.AnsiScape 6 | import net.andreinc.ansiscape.AnsiScapeContext 7 | import net.andreinc.ansiscape.AnsiSequence.* 8 | 9 | private val logger = KotlinLogging.logger { } 10 | 11 | val ansiScapeCtx : AnsiScapeContext = 12 | AnsiScapeContext() 13 | .add(AnsiClass.withName("file").add(UNDERLINE, YELLOW)) 14 | .add(AnsiClass.withName("httpMethod").add(BOLD, GREEN)) 15 | .add(AnsiClass.withName("logo").add(UNDERLINE, BLUE)) 16 | .add(AnsiClass.withName("logoNeat").add(RED, BLUE_BG)) 17 | .add(AnsiClass.withName("path").add(UNDERLINE, BLUE)) 18 | 19 | 20 | val ansiScape : AnsiScape = AnsiScape(ansiScapeCtx) 21 | 22 | fun ansi(msg: String) : String { 23 | return ansiScape.format(msg) 24 | } 25 | 26 | fun main() { 27 | logger.info { ansi("{red Andrei}") } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/serverneat/mockneat/extension/ObjectMap.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.mockneat.extension 2 | 3 | import net.andreinc.mockneat.abstraction.MockUnit 4 | import net.andreinc.mockneat.unit.objects.Constant.constant 5 | import java.util.HashMap 6 | import java.util.LinkedHashMap 7 | import java.util.function.Supplier 8 | 9 | class ObjectMap : MockUnit { 10 | 11 | private val map = LinkedHashMap>() 12 | 13 | override fun supplier(): Supplier { 14 | return Supplier { traverseObject(this) } 15 | } 16 | 17 | infix fun String.value(unit: MockUnit<*>): ObjectMap { 18 | map[this@value] = unit 19 | return this@ObjectMap 20 | } 21 | 22 | infix fun String.const(value: Any) : ObjectMap { 23 | map[this@const] = constant(value) 24 | return this@ObjectMap 25 | } 26 | 27 | companion object { 28 | private fun traverseObject(ojMap: ObjectMap): Map { 29 | val map = ojMap.map 30 | val result = HashMap() 31 | for (key in map.keys) { 32 | val value = map[key] 33 | if (value is ObjectMap) { 34 | result[key] = traverseObject(value) 35 | } else { 36 | result[key] = value!!.get() 37 | } 38 | } 39 | return result 40 | } 41 | } 42 | } 43 | 44 | 45 | fun obj(init: ObjectMap.() -> Unit) : ObjectMap { 46 | val result = ObjectMap() 47 | result.apply(init) 48 | return result 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/serverneat/server/RouteDSL.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.server 2 | 3 | import io.vertx.core.http.HttpHeaders 4 | import io.vertx.core.http.HttpMethod 5 | import io.vertx.ext.web.Router 6 | import kotlinx.coroutines.GlobalScope 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.launch 9 | import mu.KotlinLogging 10 | import net.andreinc.mockneat.abstraction.MockUnit 11 | import net.andreinc.serverneat.logging.ansi 12 | import java.util.concurrent.atomic.AtomicLong 13 | 14 | private val logger = KotlinLogging.logger { } 15 | 16 | @ServerNeatDslMarker 17 | class Routes(private val router: Router, private val globalHeaders: MutableMap) { 18 | 19 | private val count: AtomicLong = AtomicLong(0) 20 | 21 | companion object { 22 | 23 | // A list of HTTP Methods without a response body 24 | private val noBodyHttpMethods : Set = 25 | setOf( 26 | HttpMethod.HEAD, 27 | HttpMethod.CONNECT, 28 | HttpMethod.OPTIONS, 29 | HttpMethod.TRACE 30 | ) 31 | 32 | private fun hasNoBody(httpMethod: HttpMethod) : Boolean { 33 | return httpMethod in noBodyHttpMethods 34 | } 35 | } 36 | 37 | private val routes : MutableList = mutableListOf() 38 | 39 | fun get(init: Route.() -> Unit) { 40 | createRoute(init, HttpMethod.GET) 41 | } 42 | fun head(init: Route.()-> Unit) { 43 | createRoute(init, HttpMethod.HEAD) 44 | } 45 | fun post(init: Route.() -> Unit) { 46 | createRoute(init, HttpMethod.POST) 47 | } 48 | fun put(init: Route.() -> Unit) { 49 | createRoute(init, HttpMethod.PUT) 50 | } 51 | fun delete(init: Route.() -> Unit) { 52 | createRoute(init, HttpMethod.DELETE) 53 | } 54 | 55 | fun connect(init: Route.() -> Unit) { 56 | createRoute(init, HttpMethod.CONNECT) 57 | } 58 | fun options(init: Route.() -> Unit) { 59 | createRoute(init, HttpMethod.OPTIONS) 60 | } 61 | fun trace(init: Route.() -> Unit) { 62 | createRoute(init, HttpMethod.TRACE) 63 | } 64 | fun patch(init: Route.() -> Unit) { 65 | createRoute(init, HttpMethod.PATCH) 66 | } 67 | 68 | private fun createRoute(init: Route.() -> Unit, httpMethod: HttpMethod) { 69 | 70 | val r = Route(httpMethod); 71 | r.apply(init) 72 | 73 | logger.info { ansi("Adding new ({httpMethod ${r.method}}) route path='{path ${r.path}}'") } 74 | routes.add(r) 75 | 76 | if (hasNoBody(r.method) && r.response.contentObject !is RouteResponseEmptyContent) { 77 | logger.warn { ansi("{red Found response for {httpMethod ${r.method}} route. Response will be ignored.}") } 78 | } 79 | 80 | router 81 | .route(r.path) 82 | .method(r.method) 83 | .handler{ ctx -> 84 | GlobalScope.launch { 85 | val response = ctx.response() 86 | 87 | // No need to set a delay if value is 0 88 | if (r.response.delay > 0) { 89 | delay(r.response.delay) 90 | } 91 | 92 | response.headers().addAll(globalHeaders) 93 | response.headers().addAll(r.response.customHeaders) 94 | response.statusCode = r.response.statusCode 95 | 96 | logger.info { ansi("({httpMethod ${r.method}}) request for path='{path ${r.path}}' (requestId={b ${count.addAndGet(1)}})") } 97 | 98 | // No need to return a body if it's HEAD 99 | if (hasNoBody(r.method) && r.response.contentObject !is RouteResponseEmptyContent) { 100 | response.end() 101 | } 102 | // If the route is responsible for returning a file to download 103 | else if (r.response.contentObject is RouteResponseFileDownload) { 104 | response 105 | .putHeader(HttpHeaders.CONTENT_TYPE, "text/plain") 106 | .putHeader("Content-Disposition", "attachment; filename=\"${r.response.content()}\"") 107 | .putHeader(HttpHeaders.TRANSFER_ENCODING, "chunked") 108 | .sendFile(r.response.content()) 109 | } 110 | else { 111 | response.end(r.response.content()) 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | @ServerNeatDslMarker 119 | class GlobalHeaders { 120 | val globalHeaders : MutableMap = mutableMapOf() 121 | 122 | fun header(name: String, value: String) { 123 | globalHeaders[name] = value 124 | } 125 | 126 | fun header(name: String, value: MockUnit<*>) { 127 | globalHeaders[name] = value.mapToString().get() 128 | } 129 | } 130 | 131 | @ServerNeatDslMarker 132 | class Route(val method: HttpMethod) { 133 | 134 | lateinit var path : String 135 | var response : RouteResponse = RouteResponse() 136 | 137 | fun response(init: RouteResponse.() -> Unit) { 138 | this.response.apply(init) 139 | } 140 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/serverneat/server/RouteResponseDSL.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.server 2 | 3 | import MainContext.Companion.readResource 4 | import com.google.gson.GsonBuilder 5 | import mu.KotlinLogging 6 | import net.andreinc.mockneat.abstraction.MockUnit 7 | import net.andreinc.serverneat.logging.ansi 8 | import net.andreinc.serverneat.mockneat.extension.ObjectMap 9 | import java.io.File 10 | import java.nio.charset.Charset 11 | import java.util.concurrent.ConcurrentHashMap 12 | import java.util.concurrent.ConcurrentMap 13 | import java.util.concurrent.locks.Lock 14 | import java.util.concurrent.locks.ReentrantLock 15 | import kotlin.concurrent.withLock 16 | 17 | private val logger = KotlinLogging.logger { } 18 | 19 | @ServerNeatDslMarker 20 | class RouteResponse { 21 | 22 | val customHeaders : MutableMap = mutableMapOf() 23 | 24 | var delay: Long = 0 25 | var statusCode: Int = 200 26 | 27 | var contentObject : RouteResponseContent = RouteResponseEmptyContent() 28 | private set 29 | 30 | fun header(name: String, value: String) { 31 | customHeaders[name] = value 32 | } 33 | 34 | fun header(name: String, value: MockUnit<*>) { 35 | customHeaders[name] = value.mapToString().get() 36 | } 37 | 38 | fun plainText(init: RouteResponsePlainText.() -> Unit) { 39 | this.contentObject = RouteResponsePlainText().apply(init); 40 | } 41 | 42 | fun file(init: RouteResponseFileContent.() -> Unit) { 43 | this.contentObject = RouteResponseFileContent().apply(init) 44 | } 45 | 46 | fun fileDownload(init: RouteResponseFileDownload.() -> Unit) { 47 | this.contentObject = RouteResponseFileDownload().apply(init) 48 | } 49 | 50 | fun resource(init: RouteResponseResourceContent.() -> Unit) { 51 | this.contentObject = RouteResponseResourceContent().apply(init) 52 | } 53 | 54 | fun json(init: RouteResponseJsonContent.() -> Unit) { 55 | this.contentObject = RouteResponseJsonContent().apply(init) 56 | } 57 | 58 | fun empty (init : RouteResponseEmptyContent.() -> Unit) {} 59 | 60 | fun empty() {} 61 | 62 | fun content() : String { 63 | return contentObject.content() 64 | } 65 | } 66 | 67 | @ServerNeatDslMarker 68 | abstract class RouteResponseContent { 69 | abstract fun content() : String 70 | } 71 | 72 | @ServerNeatDslMarker 73 | class RouteResponsePlainText : RouteResponseContent() { 74 | 75 | lateinit var value : String 76 | 77 | override fun content(): String { 78 | return value 79 | } 80 | } 81 | 82 | @ServerNeatDslMarker 83 | class RouteResponseFileDownload : RouteResponseContent() { 84 | 85 | override fun content(): String { 86 | return path; 87 | } 88 | 89 | lateinit var path : String 90 | } 91 | 92 | @ServerNeatDslMarker 93 | class RouteResponseFileContent : RouteResponseContent() { 94 | 95 | var charSet : Charset = Charsets.UTF_8 96 | lateinit var path : String 97 | private lateinit var internalContent : String 98 | 99 | override fun content(): String { 100 | if (!this::internalContent.isInitialized) { 101 | val fileOnDisk = File(path) 102 | if (!fileOnDisk.exists()) { 103 | throw IllegalStateException("File: ${fileOnDisk.absolutePath} doesn't exist. Please check the configuration and try again.") 104 | } 105 | internalContent = File(path).readText(charSet) 106 | logger.info { ansi("File : {file ${fileOnDisk.canonicalPath}} content was loaded from the disk.") } 107 | } 108 | return internalContent 109 | } 110 | } 111 | 112 | @ServerNeatDslMarker 113 | class RouteResponseResourceContent : RouteResponseContent() { 114 | 115 | lateinit var path : String 116 | var charSet : Charset = Charsets.UTF_8 117 | private lateinit var internalContent : String 118 | 119 | override fun content(): String { 120 | if (!this::internalContent.isInitialized) { 121 | internalContent = readResource(path, charSet) 122 | } 123 | return internalContent 124 | } 125 | 126 | } 127 | 128 | @ServerNeatDslMarker 129 | class RouteResponseJsonContent : RouteResponseContent() { 130 | 131 | companion object { 132 | private val GSON = GsonBuilder().setPrettyPrinting().create() 133 | private val lock : ReentrantLock = ReentrantLock() 134 | } 135 | 136 | var persistent : Boolean = false 137 | 138 | lateinit var value: ObjectMap 139 | lateinit var file: String 140 | 141 | private lateinit var internalValue : String 142 | 143 | override fun content(): String { 144 | if (!this::internalValue.isInitialized) { 145 | if (persistent) { 146 | val fileOnDisk = File("data/dynamic/$file") 147 | if (!fileOnDisk.exists()) { 148 | logger.info { ansi("File : {file $fileOnDisk} doesn't exist on the disk. Creating a new one.") } 149 | 150 | // If the file has a parent directory(ies) that doesn't exist on the disk 151 | // Create them first, before creating the file 152 | val parentDirectories = fileOnDisk.parentFile 153 | if (parentDirectories != null && !parentDirectories.exists()) { 154 | logger.info { ansi("File : {file $fileOnDisk} parent directory {file $parentDirectories} doesn't exist on the disk. Creating folder structure.") } 155 | parentDirectories.mkdirs() 156 | } 157 | 158 | // Writing contents 159 | val contentToWrite = GSON.toJson(value.get()) 160 | fileOnDisk.writeText(contentToWrite) 161 | logger.info { ansi("File : {file ${fileOnDisk.canonicalPath}} successfully created.") } 162 | } 163 | internalValue = fileOnDisk.readText() 164 | logger.info { ansi("File : {file ${fileOnDisk.canonicalPath}} content was loaded from the disk.") } 165 | } else { 166 | internalValue = GSON.toJson(value.get()) 167 | } 168 | } 169 | return internalValue 170 | } 171 | } 172 | 173 | @ServerNeatDslMarker 174 | class RouteResponseEmptyContent : RouteResponseContent() { 175 | override fun content(): String { 176 | return "" 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/kotlin/net/andreinc/serverneat/server/ServerDSL.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.server 2 | 3 | import io.vertx.core.Vertx 4 | import io.vertx.core.http.HttpServerOptions 5 | import io.vertx.ext.web.Router 6 | import mu.KotlinLogging 7 | import net.andreinc.serverneat.logging.ansi 8 | 9 | private val logger = KotlinLogging.logger { } 10 | 11 | /** 12 | * Main entry point for the Server DSL. 13 | */ 14 | fun server(initializer: Server.() -> Unit) : Server { 15 | return Server() 16 | .apply(initializer) 17 | } 18 | 19 | @DslMarker 20 | annotation class ServerNeatDslMarker 21 | 22 | @ServerNeatDslMarker 23 | class Server { 24 | 25 | init { 26 | logger.info { ansi("Preparing {logo Vert.x} server ") } 27 | } 28 | 29 | private val vertx: Vertx = Vertx.vertx() 30 | private val router: Router = Router.router(vertx) 31 | 32 | private val globalHeaders : GlobalHeaders = GlobalHeaders() 33 | private val routes: Routes = Routes(router, globalHeaders.globalHeaders) 34 | val httpOptions : HttpServerOptions = HttpServerOptions() 35 | 36 | fun httpOptions(init: HttpServerOptions.() -> Unit) { 37 | this.httpOptions.apply(init) 38 | 39 | } 40 | 41 | fun globalHeaders(init: GlobalHeaders.() -> Unit) { 42 | this.globalHeaders.apply(init) 43 | } 44 | 45 | fun routes(init: Routes.() -> Unit) { 46 | logger.info { ansi("Initialising {logo Vert.x} server router") } 47 | this.routes.apply(init) 48 | } 49 | 50 | fun start() { 51 | 52 | logger.info { ansi("Starting {logo Vert.x} server ") } 53 | 54 | vertx 55 | .createHttpServer(httpOptions) 56 | .requestHandler(router) 57 | .listen() 58 | 59 | logger.info { ansi("{logo Vert.x server} listening on port: {b ${httpOptions.port}}") } 60 | } 61 | 62 | fun stop() { 63 | logger.info { ansi("Stopping {logo Vert.x server}...")} 64 | 65 | this.vertx.close() 66 | 67 | logger.info { ansi("{logo Vert.x server} stopped.")} 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Main-Class: MainKt 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/javax.script.ScriptEngineFactory: -------------------------------------------------------------------------------- 1 | org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, STDOUT 2 | log4j.net.andreinc.serverneat.getLogger.deng=INFO 3 | log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender 4 | log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.STDOUT.layout.ConversionPattern=[%d{ISO8601}] [%5p ] %m%n -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/serverneat/abstraction/RouteResponseAbstractTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.abstraction 2 | 3 | import io.vertx.ext.web.client.WebClientOptions 4 | import io.vertx.junit5.VertxExtension 5 | import io.vertx.junit5.web.VertxWebClientExtension 6 | import io.vertx.junit5.web.WebClientOptionsInject 7 | import net.andreinc.serverneat.server.Server 8 | import org.junit.jupiter.api.AfterAll 9 | import org.junit.jupiter.api.BeforeAll 10 | import org.junit.jupiter.api.TestInstance 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | 13 | @TestInstance( 14 | TestInstance.Lifecycle.PER_CLASS 15 | ) 16 | @ExtendWith( 17 | VertxExtension::class, 18 | VertxWebClientExtension::class 19 | ) 20 | abstract class RouteResponseAbstractTest(val server: Server) { 21 | 22 | @WebClientOptionsInject 23 | @JvmField 24 | var webClientOptions: WebClientOptions = 25 | WebClientOptions().apply { 26 | defaultHost = server.httpOptions.host 27 | defaultPort = server.httpOptions.port 28 | } 29 | 30 | @BeforeAll 31 | fun startServer() { 32 | this.server.start() 33 | } 34 | 35 | @AfterAll 36 | fun stopServer() { 37 | this.server.stop() 38 | } 39 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/serverneat/responses/RouteDynamicHeadersTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.responses 2 | 3 | import io.vertx.core.http.HttpMethod 4 | import io.vertx.ext.web.client.WebClient 5 | import io.vertx.junit5.VertxTestContext 6 | import io.vertx.junit5.web.TestRequest 7 | import io.vertx.junit5.web.TestRequest.testRequest 8 | import net.andreinc.mockneat.unit.types.Ints.ints 9 | import net.andreinc.serverneat.abstraction.RouteResponseAbstractTest 10 | import net.andreinc.serverneat.server.Server 11 | import net.andreinc.serverneat.server.server 12 | import org.junit.jupiter.api.Assertions 13 | import org.junit.jupiter.api.Assertions.* 14 | import org.junit.jupiter.api.Test 15 | import java.lang.Integer.parseInt 16 | import java.util.function.Consumer 17 | 18 | private val server : Server = server { 19 | 20 | httpOptions { 21 | host = "localhost" 22 | port = 17880 23 | } 24 | 25 | globalHeaders { 26 | header("dynamicHeader", ints().range(1,10)) 27 | header("staticHeader", "static") 28 | } 29 | 30 | routes { 31 | get { 32 | path = "/dynamic/headers" 33 | response { 34 | header("dynamicLocalHeader", ints().range(10, 20)) 35 | header("staticLocalHeader", "static local") 36 | statusCode = 200 37 | empty {} 38 | } 39 | } 40 | } 41 | } 42 | 43 | class DynamicHeadersTest : RouteResponseAbstractTest(server) { 44 | 45 | @Test 46 | fun `Test to see if dynanamically generated global and response headers are correctly generated` (webClient: WebClient, testContext: VertxTestContext) { 47 | testRequest(webClient, HttpMethod.GET, "/dynamic/headers") 48 | .expect( 49 | Consumer { response -> 50 | val dynamicHeader = response.getHeader("dynamicHeader") 51 | val staticHeader = response.getHeader("staticHeader") 52 | val dynamicLocalHeader = response.getHeader("dynamicLocalHeader") 53 | val staticLocalHeader = response.getHeader("staticLocalHeader") 54 | 55 | assertNotNull(dynamicHeader) 56 | assertNotNull(staticHeader) 57 | assertNotNull(dynamicLocalHeader) 58 | assertNotNull(staticLocalHeader) 59 | 60 | assertEquals(staticHeader, "static") 61 | assertEquals(staticLocalHeader, "static local") 62 | 63 | assertTrue(parseInt(dynamicHeader) < 10) 64 | assertTrue(parseInt(dynamicHeader) >= 0) 65 | assertTrue(parseInt(dynamicLocalHeader) >= 10) 66 | assertTrue(parseInt(dynamicLocalHeader) < 20) 67 | } 68 | ) 69 | .send(testContext) 70 | } 71 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/serverneat/responses/RouteResponseFileDownloadTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.responses 2 | 3 | import io.vertx.core.buffer.Buffer 4 | import io.vertx.core.http.HttpMethod 5 | import io.vertx.ext.web.client.HttpResponse 6 | import io.vertx.ext.web.client.WebClient 7 | import io.vertx.junit5.VertxTestContext 8 | import io.vertx.junit5.web.TestRequest.* 9 | import net.andreinc.serverneat.abstraction.RouteResponseAbstractTest 10 | import net.andreinc.serverneat.server.Server 11 | import net.andreinc.serverneat.server.server 12 | import net.andreinc.serverneat.utils.temporaryFileWithContent 13 | import org.junit.jupiter.api.Assertions 14 | import org.junit.jupiter.api.Test 15 | import java.util.function.Consumer 16 | 17 | private val fileToDownload : String = 18 | temporaryFileWithContent("download-get", "Hello, Get!") 19 | 20 | private val currentServer : Server = server { 21 | 22 | httpOptions { 23 | host = "localhost" 24 | port = 17880 25 | } 26 | 27 | routes { 28 | get { 29 | path = "/file/download/get" 30 | response { 31 | header("/file/download/get", "get") 32 | fileDownload { 33 | path = fileToDownload 34 | } 35 | } 36 | } 37 | } 38 | 39 | } 40 | 41 | class RouteResponseFileDownloadTest : RouteResponseAbstractTest(currentServer) { 42 | @Test 43 | fun `Test if a GET route with File Download works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 44 | testRequest(webClient, HttpMethod.GET, "/file/download/get") 45 | .expect( 46 | statusCode(200), 47 | responseHeader("content-type", "text/plain"), 48 | responseHeader("Content-Disposition", "attachment; filename=\"$fileToDownload\""), 49 | responseHeader("transfer-encoding", "chunked"), 50 | Consumer { httpResponseBuffer : HttpResponse -> 51 | val fileContent = httpResponseBuffer.bodyAsString() 52 | Assertions.assertEquals(fileContent, "Hello, Get!") 53 | } 54 | ) 55 | .send(testContext) 56 | } 57 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/serverneat/responses/RouteResponseFileTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.responses 2 | 3 | import io.vertx.core.buffer.Buffer 4 | import io.vertx.core.http.HttpMethod 5 | import io.vertx.ext.web.client.WebClient 6 | import io.vertx.junit5.VertxTestContext 7 | import io.vertx.junit5.web.TestRequest.* 8 | import net.andreinc.serverneat.abstraction.RouteResponseAbstractTest 9 | import net.andreinc.serverneat.server.server 10 | import net.andreinc.serverneat.utils.temporaryFileWithContent 11 | import org.junit.jupiter.api.Test 12 | 13 | private val currentServer = server { 14 | 15 | httpOptions { 16 | host = "localhost" 17 | port = 17880 18 | } 19 | 20 | globalHeaders { 21 | header("Content-Type", "application/text") 22 | } 23 | 24 | routes { 25 | 26 | get { 27 | path = "/file/response/get" 28 | response { 29 | header("/file/response/get", "get") 30 | statusCode = 200 31 | file { 32 | path = temporaryFileWithContent("getFile", "Hello, Get!") 33 | } 34 | } 35 | } 36 | 37 | post { 38 | path = "/file/response/post" 39 | response { 40 | header("/file/response/post", "post") 41 | statusCode = 200 42 | file { 43 | path = temporaryFileWithContent("postFile", "Hello, Post!") 44 | } 45 | } 46 | } 47 | 48 | put { 49 | path = "/file/response/put" 50 | response { 51 | header("/file/response/put", "put") 52 | statusCode = 200 53 | file { 54 | path = temporaryFileWithContent("putFile", "Hello, Put!") 55 | } 56 | } 57 | } 58 | 59 | patch { 60 | path = "/file/response/patch" 61 | response { 62 | header("/file/response/patch", "patch") 63 | statusCode = 200 64 | file { 65 | path = temporaryFileWithContent("patchFile", "Hello, Patch!") 66 | } 67 | } 68 | } 69 | 70 | delete { 71 | path = "/file/response/delete" 72 | response { 73 | header("/file/response/delete", "delete") 74 | statusCode = 200 75 | file { 76 | path = temporaryFileWithContent("deleteFile", "Hello, Delete!") 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | class RouteResponseFileTest : RouteResponseAbstractTest(currentServer) { 84 | 85 | @Test 86 | fun `Test if a GET route with File response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 87 | testRequest(webClient, HttpMethod.GET, "/file/response/get") 88 | .expect( 89 | statusCode(200), 90 | responseHeader("/file/response/get", "get"), 91 | bodyResponse(Buffer.buffer("Hello, Get!"), "application/text") 92 | ) 93 | .send(testContext) 94 | } 95 | 96 | @Test 97 | fun `Test if a POST route with File response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 98 | testRequest(webClient, HttpMethod.POST, "/file/response/post") 99 | .expect( 100 | statusCode(200), 101 | responseHeader("/file/response/post", "post"), 102 | bodyResponse(Buffer.buffer("Hello, Post!"), "application/text") 103 | ) 104 | .send(testContext) 105 | } 106 | 107 | @Test 108 | fun `Test if a PUT route with File response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 109 | testRequest(webClient, HttpMethod.PUT, "/file/response/put") 110 | .expect( 111 | statusCode(200), 112 | responseHeader("/file/response/put", "put"), 113 | bodyResponse(Buffer.buffer("Hello, Put!"), "application/text") 114 | ) 115 | .send(testContext) 116 | } 117 | 118 | @Test 119 | fun `Test if a PATCH route with File response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 120 | testRequest(webClient, HttpMethod.PATCH, "/file/response/patch") 121 | .expect( 122 | statusCode(200), 123 | responseHeader("/file/response/patch", "patch"), 124 | bodyResponse(Buffer.buffer("Hello, Patch!"), "application/text") 125 | ) 126 | .send(testContext) 127 | } 128 | 129 | @Test 130 | fun `Test if a DELETE route with File response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 131 | testRequest(webClient, HttpMethod.DELETE, "/file/response/delete") 132 | .expect( 133 | statusCode(200), 134 | responseHeader("/file/response/delete", "delete"), 135 | bodyResponse(Buffer.buffer("Hello, Delete!"), "application/text") 136 | ) 137 | .send(testContext) 138 | } 139 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/serverneat/responses/RouteResponsePlainTextTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.responses 2 | 3 | import io.vertx.core.buffer.Buffer.buffer 4 | import io.vertx.core.http.HttpMethod 5 | import io.vertx.ext.web.client.WebClient 6 | import io.vertx.junit5.VertxTestContext 7 | import io.vertx.junit5.web.TestRequest.* 8 | import net.andreinc.serverneat.abstraction.RouteResponseAbstractTest 9 | import net.andreinc.serverneat.server.server 10 | import org.junit.jupiter.api.* 11 | 12 | private val currentServer = server { 13 | 14 | httpOptions { 15 | host = "localhost" 16 | port = 17880 17 | } 18 | 19 | globalHeaders { 20 | header("Content-Type", "application/text") 21 | } 22 | 23 | routes { 24 | 25 | get { 26 | path = "/path/get" 27 | response { 28 | header("path/get", "get") 29 | statusCode = 200 30 | plainText { 31 | value = "Hello, Get!" 32 | } 33 | } 34 | } 35 | 36 | head { 37 | path = "/path/head" 38 | response { 39 | header("path/head", "head") 40 | statusCode = 200 41 | } 42 | } 43 | 44 | post { 45 | path = "/path/post" 46 | response { 47 | header("path/post", "post") 48 | statusCode = 200 49 | plainText { 50 | value = "Hello, Post!" 51 | } 52 | } 53 | } 54 | 55 | put { 56 | path = "/path/put" 57 | response { 58 | header("path/put", "put") 59 | statusCode = 200 60 | plainText { 61 | value = "Hello, Put!" 62 | } 63 | } 64 | } 65 | 66 | delete { 67 | path = "/path/delete" 68 | response { 69 | header("path/delete", "delete") 70 | statusCode = 200 71 | plainText { 72 | value = "Hello, Delete!" 73 | } 74 | } 75 | } 76 | 77 | connect { 78 | path = "/path/connect" 79 | response { 80 | header("path/connect", "connect") 81 | statusCode = 200 82 | plainText { 83 | value = "Hello, Connect!" 84 | } 85 | } 86 | } 87 | 88 | options { 89 | path = "/path/options" 90 | response { 91 | header("path/options", "options") 92 | statusCode = 200 93 | plainText { 94 | value = "Hello, Options!" 95 | } 96 | } 97 | } 98 | 99 | trace { 100 | path = "/path/trace" 101 | response { 102 | header("path/trace", "trace") 103 | statusCode = 200 104 | plainText { 105 | value = "Hello, Trace!" 106 | } 107 | } 108 | } 109 | 110 | patch { 111 | path = "/path/patch" 112 | response { 113 | header("path/patch", "patch") 114 | statusCode = 200 115 | plainText { 116 | value = "Hello, Patch!" 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | class RouteResponsePlainTextTest : RouteResponseAbstractTest(currentServer) { 124 | 125 | @Test 126 | fun `Test if a GET route with Plain Text response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 127 | testRequest(webClient, HttpMethod.GET, "/path/get") 128 | .expect( 129 | statusCode(200), 130 | responseHeader("path/get", "get"), 131 | bodyResponse(buffer("Hello, Get!"), "application/text") 132 | ) 133 | .send(testContext) 134 | } 135 | 136 | @Test 137 | fun `Test if a HEAD route with Plain Text response works correctly (status, header)` (webClient: WebClient, testContext: VertxTestContext) { 138 | testRequest(webClient, HttpMethod.HEAD, "/path/head") 139 | .expect( 140 | statusCode(200), 141 | responseHeader("path/head", "head") 142 | ) 143 | .send(testContext) 144 | } 145 | 146 | @Test 147 | fun `Test if a POST route with Plain Text response works correctly (status, body, header)` (webClient: WebClient, testContext: VertxTestContext) { 148 | testRequest(webClient, HttpMethod.POST, "/path/post") 149 | .expect( 150 | statusCode(200), 151 | responseHeader("path/post", "post"), 152 | bodyResponse(buffer("Hello, Post!"), "application/text") 153 | ) 154 | .send(testContext) 155 | } 156 | 157 | @Test 158 | fun `Test if a PUT route with Plain Text response works correctly (status, body, header)` (webClient: WebClient, testContext: VertxTestContext) { 159 | testRequest(webClient, HttpMethod.PUT, "/path/put") 160 | .expect( 161 | statusCode(200), 162 | responseHeader("path/put", "put"), 163 | bodyResponse(buffer("Hello, Put!"), "application/text") 164 | ) 165 | .send(testContext) 166 | } 167 | 168 | @Test 169 | fun `Test if a DELETE route with Plain Text response works correctly (status, body, header)` (webClient: WebClient, testContext: VertxTestContext) { 170 | testRequest(webClient, HttpMethod.DELETE, "/path/delete") 171 | .expect( 172 | statusCode(200), 173 | responseHeader("path/delete", "delete"), 174 | bodyResponse(buffer("Hello, Delete!"), "application/text") 175 | ) 176 | .send(testContext) 177 | } 178 | 179 | @Test 180 | fun `Test if a CONNECT route with Plain Text response works correctly (status, header)` (webClient: WebClient, testContext: VertxTestContext) { 181 | testRequest(webClient, HttpMethod.CONNECT, "/path/connect") 182 | .expect( 183 | statusCode(200), 184 | responseHeader("path/connect", "connect") 185 | ) 186 | .send(testContext) 187 | } 188 | 189 | @Test 190 | fun `Test if a OPTIONS route with Plain Text response works correctly (status, header)` (webClient: WebClient, testContext: VertxTestContext) { 191 | testRequest(webClient, HttpMethod.OPTIONS, "/path/options") 192 | .expect( 193 | statusCode(200), 194 | responseHeader("path/options", "options") 195 | ) 196 | .send(testContext) 197 | } 198 | 199 | @Test 200 | fun `Test if a TRACE route with Plain Text response works correctly (status, header)` (webClient: WebClient, testContext: VertxTestContext) { 201 | testRequest(webClient, HttpMethod.TRACE, "/path/trace") 202 | .expect( 203 | statusCode(200), 204 | responseHeader("path/trace", "trace") 205 | ) 206 | .send(testContext) 207 | } 208 | 209 | @Test 210 | fun `Test if a PATCH route with Plain Text response works correctly (status, header)` (webClient: WebClient, testContext: VertxTestContext) { 211 | testRequest(webClient, HttpMethod.PATCH, "/path/patch") 212 | .expect( 213 | statusCode(200), 214 | responseHeader("path/patch", "patch"), 215 | bodyResponse(buffer("Hello, Patch!"), "application/text") 216 | ) 217 | .send(testContext) 218 | } 219 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/serverneat/responses/RouteResponseResourceTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.responses 2 | 3 | import io.vertx.core.buffer.Buffer 4 | import io.vertx.core.http.HttpMethod 5 | import io.vertx.ext.web.client.WebClient 6 | import io.vertx.junit5.VertxTestContext 7 | import io.vertx.junit5.web.TestRequest.* 8 | import net.andreinc.serverneat.abstraction.RouteResponseAbstractTest 9 | import net.andreinc.serverneat.server.server 10 | import org.junit.jupiter.api.Test 11 | 12 | private val currentServer = server { 13 | 14 | httpOptions { 15 | host = "localhost" 16 | port = 17880 17 | } 18 | 19 | globalHeaders { 20 | header("Content-Type", "application/text") 21 | } 22 | 23 | routes { 24 | 25 | get { 26 | path = "/resource/response/get" 27 | response { 28 | header("/resource/response/get", "get") 29 | statusCode = 200 30 | resource { 31 | path = "api-data/get-response.txt" 32 | } 33 | } 34 | } 35 | 36 | post { 37 | path = "/resource/response/post" 38 | response { 39 | header("/resource/response/post", "post") 40 | statusCode = 200 41 | resource { 42 | path = "api-data/post-response.txt" 43 | } 44 | } 45 | } 46 | 47 | put { 48 | path = "/resource/response/put" 49 | response { 50 | header("/resource/response/put", "put") 51 | statusCode = 200 52 | resource { 53 | path = "api-data/put-response.txt" 54 | } 55 | } 56 | } 57 | 58 | patch { 59 | path = "/resource/response/patch" 60 | response { 61 | header("/resource/response/patch", "patch") 62 | statusCode = 200 63 | resource { 64 | path = "api-data/patch-response.txt" 65 | } 66 | } 67 | } 68 | 69 | delete { 70 | path = "/resource/response/delete" 71 | response { 72 | header("/resource/response/delete", "delete") 73 | statusCode = 200 74 | resource { 75 | path = "api-data/delete-response.txt" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | class RouteResponseResourceTest : RouteResponseAbstractTest(currentServer) { 83 | 84 | @Test 85 | fun `Test if a GET route with Resource response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 86 | testRequest(webClient, HttpMethod.GET, "/resource/response/get") 87 | .expect( 88 | statusCode(200), 89 | responseHeader("/resource/response/get", "get"), 90 | bodyResponse(Buffer.buffer("Hello, Get!"), "application/text") 91 | ) 92 | .send(testContext) 93 | } 94 | 95 | @Test 96 | fun `Test if a POST route with Resource response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 97 | testRequest(webClient, HttpMethod.POST, "/resource/response/post") 98 | .expect( 99 | statusCode(200), 100 | responseHeader("/resource/response/post", "post"), 101 | bodyResponse(Buffer.buffer("Hello, Post!"), "application/text") 102 | ) 103 | .send(testContext) 104 | } 105 | 106 | @Test 107 | fun `Test if a PUT route with Resource response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 108 | testRequest(webClient, HttpMethod.PUT, "/resource/response/put") 109 | .expect( 110 | statusCode(200), 111 | responseHeader("/resource/response/put", "put"), 112 | bodyResponse(Buffer.buffer("Hello, Put!"), "application/text") 113 | ) 114 | .send(testContext) 115 | } 116 | 117 | @Test 118 | fun `Test if a PATCH route with Resource response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 119 | testRequest(webClient, HttpMethod.PATCH, "/resource/response/patch") 120 | .expect( 121 | statusCode(200), 122 | responseHeader("/resource/response/patch", "patch"), 123 | bodyResponse(Buffer.buffer("Hello, Patch!"), "application/text") 124 | ) 125 | .send(testContext) 126 | } 127 | 128 | @Test 129 | fun `Test if a DELETE route with Resource response works correctly (status, body, header) ` (webClient: WebClient, testContext: VertxTestContext) { 130 | testRequest(webClient, HttpMethod.DELETE, "/resource/response/delete") 131 | .expect( 132 | statusCode(200), 133 | responseHeader("/resource/response/delete", "delete"), 134 | bodyResponse(Buffer.buffer("Hello, Delete!"), "application/text") 135 | ) 136 | .send(testContext) 137 | } 138 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/serverneat/responses/RouteResponseSimpleJsonTest.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.responses 2 | 3 | import io.vertx.core.buffer.Buffer 4 | import io.vertx.core.http.HttpMethod 5 | import io.vertx.ext.web.client.HttpResponse 6 | import io.vertx.ext.web.client.WebClient 7 | import io.vertx.junit5.VertxTestContext 8 | import io.vertx.junit5.web.TestRequest.* 9 | import io.vertx.kotlin.core.json.get 10 | import net.andreinc.serverneat.abstraction.RouteResponseAbstractTest 11 | import net.andreinc.serverneat.mockneat.extension.obj 12 | import net.andreinc.serverneat.server.server 13 | import org.junit.jupiter.api.AfterAll 14 | import org.junit.jupiter.api.Assertions.* 15 | import org.junit.jupiter.api.Test 16 | import java.io.File 17 | import java.util.function.Consumer 18 | 19 | private val getPersistentJsonPath = "tests/get-persistent.json" 20 | 21 | private val currentServer = server { 22 | 23 | httpOptions { 24 | host = "localhost" 25 | port = 17880 26 | } 27 | 28 | globalHeaders { 29 | header("Content-Type", "application/json") 30 | } 31 | 32 | routes { 33 | 34 | get { 35 | path = "/simple/json/get" 36 | response { 37 | header("/simple/json/get", "get") 38 | json { 39 | value = obj { 40 | "firstName" const "Mike" 41 | "lastName" const "Smith" 42 | "pc" value obj { 43 | "type" const "laptop" 44 | "operatingSystem" const "linux" 45 | } 46 | "integers" const arrayOf(1,2,3) 47 | } 48 | } 49 | } 50 | } 51 | 52 | get { 53 | path = "/simple/json/get/persistent" 54 | response { 55 | header("/simple/json/get/persistent", "get") 56 | json { 57 | persistent = true 58 | file = getPersistentJsonPath 59 | value = obj { 60 | "firstName" const "Mike" 61 | "lastName" const "Smith" 62 | "pc" value obj { 63 | "type" const "laptop" 64 | "operatingSystem" const "linux" 65 | } 66 | "integers" const arrayOf(1,2,3) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | } 74 | 75 | class RouteResponseSimpleJsonTest : RouteResponseAbstractTest(currentServer) { 76 | 77 | private fun correctJsonResponse () : Consumer> { 78 | return Consumer { response -> 79 | val json = response.bodyAsJsonObject() 80 | 81 | assertNotNull(json.getString("firstName")) 82 | assertEquals(json.getString("firstName"), "Mike") 83 | 84 | assertNotNull(json.getString("lastName")) 85 | assertEquals(json.getString("lastName"), "Smith") 86 | 87 | assertNotNull(json.getJsonArray("integers")) 88 | assertEquals(json.getJsonArray("integers").size(), 3) 89 | assertEquals(json.getJsonArray("integers")[0], 1) 90 | assertEquals(json.getJsonArray("integers")[1], 2) 91 | assertEquals(json.getJsonArray("integers")[2], 3) 92 | 93 | assertNotNull(json.getJsonObject("pc")) 94 | assertEquals(json.getJsonObject("pc").getString("type"), "laptop") 95 | assertEquals(json.getJsonObject("pc").getString("operatingSystem"), "linux") 96 | } 97 | } 98 | 99 | @Test 100 | fun `Test if a GET route with Simple JSON response works correctly (status, body, header)` (webClient: WebClient, testContext: VertxTestContext) { 101 | testRequest(webClient, HttpMethod.GET, "/simple/json/get") 102 | .expect( 103 | statusCode(200), 104 | responseHeader("/simple/json/get", "get"), 105 | responseHeader("Content-Type", "application/json"), 106 | correctJsonResponse() 107 | ) 108 | .send(testContext) 109 | } 110 | 111 | @Test 112 | fun `Test if a GET route with (Persistent) Simple JSON response works correctly (status, body, header)` (webClient: WebClient, testContext: VertxTestContext) { 113 | testRequest(webClient, HttpMethod.GET, "/simple/json/get/persistent") 114 | .expect( 115 | statusCode(200), 116 | responseHeader("/simple/json/get/persistent", "get"), 117 | responseHeader("Content-Type", "application/json"), 118 | correctJsonResponse(), 119 | Consumer { assertTrue(File("data/dynamic/$getPersistentJsonPath").exists()) } 120 | ) 121 | .send(testContext) 122 | } 123 | 124 | @AfterAll 125 | fun cleanUp() { 126 | File("data/dynamic/$getPersistentJsonPath").delete() 127 | File("data/dynamic/tests").delete() 128 | } 129 | } -------------------------------------------------------------------------------- /src/test/kotlin/net/andreinc/serverneat/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package net.andreinc.serverneat.utils 2 | 3 | fun temporaryFileWithContent(fileName: String, content: String, suffix : String = ".tmp") : String { 4 | val file = createTempFile(fileName, suffix) 5 | file.deleteOnExit() 6 | file.writeText(content) 7 | return file.absolutePath 8 | } -------------------------------------------------------------------------------- /src/test/resources/api-data/delete-response.txt: -------------------------------------------------------------------------------- 1 | Hello, Delete! -------------------------------------------------------------------------------- /src/test/resources/api-data/get-response.txt: -------------------------------------------------------------------------------- 1 | Hello, Get! -------------------------------------------------------------------------------- /src/test/resources/api-data/patch-response.txt: -------------------------------------------------------------------------------- 1 | Hello, Patch! -------------------------------------------------------------------------------- /src/test/resources/api-data/post-response.txt: -------------------------------------------------------------------------------- 1 | Hello, Post! -------------------------------------------------------------------------------- /src/test/resources/api-data/put-response.txt: -------------------------------------------------------------------------------- 1 | Hello, Put! -------------------------------------------------------------------------------- /userList.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "firstName": "Tommie", 5 | "lastName": "Pecinousky", 6 | "creditCards": [ 7 | "375647171048833", 8 | "347920529111073", 9 | "371184549985144", 10 | "349473327586755", 11 | "370307238027578", 12 | "343580278024242", 13 | "348120393529927", 14 | "342973082782199", 15 | "378901579144455", 16 | "341116851066276" 17 | ] 18 | }, 19 | { 20 | "firstName": "Corey", 21 | "lastName": "Carhart", 22 | "creditCards": [ 23 | "372784190137876", 24 | "346877443230259", 25 | "343786405499880", 26 | "379487625914900", 27 | "375713954377888", 28 | "372430589980340", 29 | "343261663796534", 30 | "347858583414803", 31 | "340446503381463", 32 | "375318958497656" 33 | ] 34 | }, 35 | { 36 | "firstName": "Mathew", 37 | "lastName": "Girvan", 38 | "creditCards": [ 39 | "348935765671514", 40 | "340821246397184", 41 | "348629855683436", 42 | "372320857952073", 43 | "347568046336952", 44 | "340298818197815", 45 | "349789580577011", 46 | "347391356314208", 47 | "342629293752484", 48 | "372291477823178" 49 | ] 50 | }, 51 | { 52 | "firstName": "Rufus", 53 | "lastName": "Jaffy", 54 | "creditCards": [ 55 | "348192244441339", 56 | "379129988190781", 57 | "344068626894382", 58 | "377642302098573", 59 | "347489529305606", 60 | "372006253939208", 61 | "376537667102575", 62 | "371146295654786", 63 | "378962258646152", 64 | "379282303907056" 65 | ] 66 | }, 67 | { 68 | "firstName": "Sherwood", 69 | "lastName": "Valiente", 70 | "creditCards": [ 71 | "370158331274009", 72 | "378678258403093", 73 | "342167589813645", 74 | "346229193992276", 75 | "341860153620773", 76 | "375966417981031", 77 | "375431475425801", 78 | "379246499996090", 79 | "349404599039991", 80 | "349219732707225" 81 | ] 82 | }, 83 | { 84 | "firstName": "Garth", 85 | "lastName": "Youngquist", 86 | "creditCards": [ 87 | "342339587487819", 88 | "379440611480428", 89 | "348811914090999", 90 | "377128378528651", 91 | "347141647682149", 92 | "344546968957495", 93 | "371258799392589", 94 | "372022613433501", 95 | "376726354971307", 96 | "371657479276295" 97 | ] 98 | }, 99 | { 100 | "firstName": "Lance", 101 | "lastName": "Patriarco", 102 | "creditCards": [ 103 | "342971704050631", 104 | "342593185728095", 105 | "342535105999314", 106 | "347358718327608", 107 | "347973677775124", 108 | "376489230308323", 109 | "348072437529480", 110 | "372215303514420", 111 | "378106193593760", 112 | "340936402468148" 113 | ] 114 | }, 115 | { 116 | "firstName": "Refugio", 117 | "lastName": "Yeilding", 118 | "creditCards": [ 119 | "377312228169151", 120 | "374928418304408", 121 | "343642716928960", 122 | "348028378498762", 123 | "370387599857060", 124 | "343374218903024", 125 | "346039969000703", 126 | "377395268764189", 127 | "377881186245880", 128 | "373025354892073" 129 | ] 130 | }, 131 | { 132 | "firstName": "Dana", 133 | "lastName": "Stachowicz", 134 | "creditCards": [ 135 | "372067501440514", 136 | "344785996823358", 137 | "348602262940418", 138 | "343075960718466", 139 | "347317495255457", 140 | "373387638075432", 141 | "345791387522301", 142 | "343224894198417", 143 | "371287165964150", 144 | "379859545299309" 145 | ] 146 | }, 147 | { 148 | "firstName": "Lesley", 149 | "lastName": "Feldstein", 150 | "creditCards": [ 151 | "340515541407058", 152 | "376966034192623", 153 | "371022643140762", 154 | "372421982647719", 155 | "371733755591861", 156 | "372545352022028", 157 | "371892169691632", 158 | "344524290390800", 159 | "370634628975839", 160 | "373335728106542" 161 | ] 162 | } 163 | ] 164 | } --------------------------------------------------------------------------------