|
1 | 1 | # JSONScript |
2 | 2 | Platform independent scripting language expressed in JSON format |
| 3 | + |
| 4 | + |
| 5 | +## Problem |
| 6 | +----- |
| 7 | + |
| 8 | +Management of remote systems is usually done via APIs. |
| 9 | + |
| 10 | +It is often required to make multiple sequential or parallel calls, sometimes with some conditions between calls, to get the required result. It can be achieved in two ways: |
| 11 | + |
| 12 | +1. Sending multiple requests to the remote system and implementing all the logic in the client system. The advantage of this approach is that the remote system remains unchanged and client can easily change the logic and flow of requests. The disadvantage is the latency - each request should travel via the network. |
| 13 | + |
| 14 | +2. Implementing additional methods/endpoints in the remote system. The advantage of this approach is that the client has to make only one request. The disadvantage is that it requires changing the remote system (= coding + testing + documenting + deploying + monitoring + supporting...). In some cases it is simply impossible. When it is possible, it inevitably leads to the frowing complexity of the remote system as more and more specialized methods/APIs are added to it. |
| 15 | + |
| 16 | +In some cases, developers implement "batch endpoints" that allow to process multiple requests in parallel in a single HTTP request. It covers some of the cases when multiple requests are sent, but only a relatively small share. |
| 17 | + |
| 18 | + |
| 19 | +## Solution |
| 20 | +----- |
| 21 | + |
| 22 | +JSONScript allows you to send a script to the remote system that will be interpreted by the remote system. It will execute all the containig instructions sequesntially, or in parallel, passing results from one instruction to another when required, and returning all results to the client. All this in a single HTTP (or any other transport) request. |
| 23 | + |
| 24 | +JSONScript allows to keep the API of remote system conscise and simple, only implementing simple basic methods. At the same time JSONScript allows the client to implement an advanced logic with conditions and looping, sequential and concurrent execution, defining and calling functions and handling exceptions. So quite advanced execution can be requested from the remote system in a single transport request. |
| 25 | + |
| 26 | +At the same time JSONScript allows keeping the remote system completely secure as only the commands registered with interpreter can be envoked from the JSONScript script. |
| 27 | + |
| 28 | + |
| 29 | +## Qualities |
| 30 | +----- |
| 31 | + |
| 32 | +- Platform and language independent |
| 33 | +- Asynchronous and concurrent |
| 34 | +- Supports all control from instructions from general purpose languages, including functions and exception handling |
| 35 | +- Does not support any data structures or calculations - they will be provided by the host platform. |
| 36 | +- Results from all previous instructions in the script can be used as parameters by the following instructions. |
| 37 | + |
| 38 | + |
| 39 | +## Language |
| 40 | +----- |
| 41 | + |
| 42 | +JSONScript uses JSON format to express the script. |
| 43 | + |
| 44 | +A valid JSONScript script can be an object or an array. |
| 45 | + |
| 46 | +JSON data is a valid JSONScript as long as none of the objects keys use $ as the first symbol. If you need to use JSON data with the key starting with "$", it can be escaped with "\": `{ "\$exec": "name" }`. |
| 47 | + |
| 48 | +If there is no execution instructions in JSON, it will return the same JSON as the result of it's execution by JSONScript interpreter. |
| 49 | + |
| 50 | + |
| 51 | +### $exec - basic execution instruction |
| 52 | +----- |
| 53 | + |
| 54 | +Syntax: |
| 55 | + |
| 56 | +``` |
| 57 | +{ "$exec": "<executor>", |
| 58 | + "$method": "<method>", |
| 59 | + "$args": <JSONScript> } |
| 60 | +``` |
| 61 | + |
| 62 | +Basic instruction in JSONScript is Object that has keys `$exec` (shortened "executor"), `$method` and `$args`. |
| 63 | + |
| 64 | +Basic instruction is also a valid JSONScript script and can be executed on its own. |
| 65 | + |
| 66 | +Instruction can be executed synchronously or asynchronously, as determined by processor. The implementation of asynchronous execution is determined by the implementation of the interpreter and the features available in the host language - it can use callbacks, promises, generators, co-routines, etc. to determine instruction completion. |
| 67 | + |
| 68 | +`$exec` is the string with the name of the instruction processor that was previously registered with JSONScript interpreter. Depending on the implementation of the interpreter, an instruction processor can be an object or function. |
| 69 | + |
| 70 | +`$method` is the string with the name of method that the instruction processor supports. Methods should NOT be previously registered with interpreter, if the method is not supported, the processor should throw an exception METHOD_NOT_IMPLEMENTED. |
| 71 | + |
| 72 | +`$args` can be a scalar, object or array with arguments. Arguments can be references to the results of previous instructions, as shown below, and any valid JSONScript. If some of the argument is a JSONScript, the result of it's execution will be passed. |
| 73 | + |
| 74 | +The result of the basic instruction execution should be any valid JSON. |
| 75 | + |
| 76 | + |
| 77 | +##### Examples |
| 78 | + |
| 79 | +Instruction to execute HTTP request on the local process (without sending request) to update some resource with id=1: |
| 80 | + |
| 81 | +```JSON |
| 82 | +{ "$exec": "router", |
| 83 | + "$method": "PUT", |
| 84 | + "$args": |
| 85 | + { "url": "/resource/1", |
| 86 | + "body": { "active": false } |
| 87 | + "headers": { "Content-Type": "application/json" } } } |
| 88 | +``` |
| 89 | + |
| 90 | + |
| 91 | +JSONScript to create a resource and a child resource using basic HTTP endpoints (can be done in a single request): |
| 92 | + |
| 93 | +```JSON |
| 94 | +{ "$exec": "router", |
| 95 | + "$method": "POST", |
| 96 | + "args": |
| 97 | + { "url": "/child_resource", |
| 98 | + "body": |
| 99 | + { "name": "My child resource", |
| 100 | + "resource_id": |
| 101 | + { "$exec": "router", |
| 102 | + "$method": "POST", |
| 103 | + "args": |
| 104 | + { "url": "/resource", |
| 105 | + "body": { "name": "My resource" } } } } } } |
| 106 | +``` |
| 107 | + |
| 108 | +In the example above the result of the script execution is used as an argument for another script. The same can be achieved more elegantly (and is recommended) using sequential execution and reference (see below). The script above will return only the id of child resource, results of scripts in `$args` are not returned. |
| 109 | + |
| 110 | + |
| 111 | +Instruction to perform database access (dbjs here is some non-existent processor for database access from JSONScript): |
| 112 | + |
| 113 | +```JSON |
| 114 | +{ "$exec": "dbjs", |
| 115 | + "$method": "select", |
| 116 | + "$args": |
| 117 | + { "table": "resources", |
| 118 | + "where": { "id": 1 } } } |
| 119 | +``` |
| 120 | + |
| 121 | +Instruction to execute bash script (e.g., to use on internal network): |
| 122 | + |
| 123 | +```JSON |
| 124 | +{ |
| 125 | + "$exec": "bash", |
| 126 | + "$method": "node", |
| 127 | + "$args": [ "--harmony", "myapp.js" ] } |
| 128 | +``` |
| 129 | + |
| 130 | + |
| 131 | +### Comments |
| 132 | +----- |
| 133 | + |
| 134 | +Syntax: |
| 135 | + |
| 136 | +``` |
| 137 | +{ "$/": "<comment>" } |
| 138 | +``` |
| 139 | + |
| 140 | +Comments can be used inside JSONScript. To any object "$/" key can be added - it will be removed before execution. To any array the object `{ "$/": "comment" }` can be added. This object will be removed before execution, and it will not affect absolute or relative references (see below). |
| 141 | + |
| 142 | + |
| 143 | +### Sequential execution |
| 144 | +----- |
| 145 | + |
| 146 | +Syntax: |
| 147 | + |
| 148 | +``` |
| 149 | +[ <JSONScript>, |
| 150 | + <JSONScript>, |
| 151 | + ... ] |
| 152 | +``` |
| 153 | + |
| 154 | +Sequential execution is expressed as the array of execution steps. Each step should be a valid JSONScript script. It can be either a basic instruction or any other valid JSONScript execution construct (including array for sequential execution). |
| 155 | + |
| 156 | +Interpreter will ensure that the next step will start only after the previous step has completed. |
| 157 | + |
| 158 | +The result of sequential execution is the array of execution results from each step. |
| 159 | + |
| 160 | +The following execution steps can use the results of the previous steps as their arguments, as shown in the examples and in [References to the previous results]. |
| 161 | + |
| 162 | + |
| 163 | +##### Examples |
| 164 | + |
| 165 | +JSONScript to create a resource and a child resource using basic HTTP endpoints (can be done in a single request): |
| 166 | + |
| 167 | +```JSON |
| 168 | +[ { "$exec": "router", |
| 169 | + "$method": "POST", |
| 170 | + "args": |
| 171 | + { "url": "/resource", |
| 172 | + "body": { "name": "My resource" } } }, |
| 173 | + { "$exec": "router", |
| 174 | + "$method": "POST", |
| 175 | + "args": |
| 176 | + { "url": "/child_resource", |
| 177 | + "body": |
| 178 | + { "name": "My child resource", |
| 179 | + "resource_id": { "$ref": -1 } } } } ] |
| 180 | +``` |
| 181 | + |
| 182 | +In this example `{ "$ref": -1 }` will be substituted with the result of the previous instruction execution. Instead of relative indeces, absolute indeces can also be used, with the first instruction in the list having the index of 0. So in this example `{ "$ref": 0 }` would mean the same. |
| 183 | + |
| 184 | +Also to refer to the result of the previous instruction { "$ref": "-" } can be used. |
| 185 | + |
| 186 | +Results of previous sub-instructions and super-instructions can also be referred, as long as they were completed before the current step. See [References to the previous results]. |
| 187 | + |
| 188 | + |
| 189 | +### Parallel execution |
| 190 | +----- |
| 191 | + |
| 192 | +Syntax: |
| 193 | + |
| 194 | +``` |
| 195 | +{ "<step_name>": <JSONScript>, |
| 196 | + "<step_name>": <JSONScript>, |
| 197 | + ..., |
| 198 | + / "$concurrency": <JSONScript> / } |
| 199 | +``` |
| 200 | + |
| 201 | +/ ... / above means an optional element. |
| 202 | + |
| 203 | + |
| 204 | +Parallel execution is expressed as the object with each key containing an execution step - see [Sequential execution] for details about execution steps. |
| 205 | + |
| 206 | +Each execution step has a name - its key in the object. This key can be a string starting from letter and containing letters, numbers and symbols "_" and "-". |
| 207 | + |
| 208 | +Interpreter will not wait for the results of any steps in the object to start executing other steps, as long as this steps do not refer to the results of other steps (see below), although a special key `$concurrency` can be used to limit the maximum number of steps executing in parallel. Interpreter should also allow setting the default maximum concurrency used if this key is not present. |
| 209 | + |
| 210 | +The implementation of parallel execution is determined by the implementation of the interpreter and the features available in the host language - it can use threads, fibers, event loops, etc. |
| 211 | + |
| 212 | +The result of parallel execution is the JSON object with the same keys containing the results of each keys. |
| 213 | + |
| 214 | +Execution steps in parallel execution can refer to the results of other steps in the same object and they can refer to the results of other instruction executed previously - see Examples and [References to the previous results]. |
| 215 | + |
| 216 | + |
| 217 | +##### Examples |
| 218 | + |
| 219 | +Request two resources in parallel: |
| 220 | + |
| 221 | +```JSON |
| 222 | +{ "1": |
| 223 | + { "$exec": "router", |
| 224 | + "$method": "GET", |
| 225 | + "args": |
| 226 | + { "url": "/resource/1" } }, |
| 227 | + "2": |
| 228 | + { "$exec": "router", |
| 229 | + "$method": "GET", |
| 230 | + "args": |
| 231 | + { "url": "/resource/2" } } } |
| 232 | +``` |
| 233 | + |
| 234 | +Request two records: |
| 235 | + |
| 236 | +```JSON |
| 237 | +{ "1": |
| 238 | + { "$exec": "dbjs", |
| 239 | + "$method": "select", |
| 240 | + "$args": |
| 241 | + { "table": "users", |
| 242 | + "where": { "id": 1 } } }, |
| 243 | + "2": |
| 244 | + { "$exec": "dbjs", |
| 245 | + "$method": "select", |
| 246 | + "$args": |
| 247 | + { "table": "documents", |
| 248 | + "where": { "id": 23 } } } |
| 249 | +``` |
| 250 | + |
| 251 | +It can be argued that the same can be achieved using SQL, but it is less safe exposing SQL that a limited commands set all of which are safe. As long as only safe commands are exposed to interpreter, the whole script is also safe. |
| 252 | + |
| 253 | + |
| 254 | +Request post with title "My post" and all comments: |
| 255 | + |
| 256 | +```JSON |
| 257 | +{ "post": |
| 258 | + { "$exec": "router", |
| 259 | + "$method": "GET", |
| 260 | + "args": |
| 261 | + { "url": "/post" } |
| 262 | + { "qs": |
| 263 | + { "title": "My post", |
| 264 | + "limit": 1 } } }, |
| 265 | + "comments": |
| 266 | + { "$exec": "router", |
| 267 | + "$method": "GET", |
| 268 | + "args": |
| 269 | + { "$/": "get all comments for post_id", |
| 270 | + "url": "/comments/:post_id", |
| 271 | + "params": { "post_id": { "$ref": "post.id" } } } } } |
| 272 | +``` |
| 273 | + |
| 274 | +Although parallel execution construct is used in the example above, it will be executed sequentially, as the result of the first step is used in the arguments of the second step. `{ "$ref": "post.id" }` returns the property `id` of the post returned by the first instruction. |
| 275 | + |
| 276 | +Interpreter should try to detect any possible circular references as early as possible before executing any steps, in which case the construct will throw an exception CIRCULAR_REFERENCE. Because "$ref" value can be another script, it may require some processing to be done before the circular reference is detected. |
| 277 | + |
| 278 | + |
| 279 | +### Control flow |
| 280 | +----- |
| 281 | + |
| 282 | +JSONScript supports control flow instructions that affect the order of excution by supporting loops, conditionals, functions, exception handling and returning results from the script. |
| 283 | + |
| 284 | +All control frow constructs are objects with some special key starting from "$". |
| 285 | + |
| 286 | + |
| 287 | +#### $end - Ending script execution |
| 288 | +----- |
| 289 | + |
| 290 | +Syntax: |
| 291 | + |
| 292 | +``` |
| 293 | +{ "$end": <JSONScript> } |
| 294 | +``` |
| 295 | +
|
| 296 | +
|
| 297 | +In some cases you may only want the result of the last step in sequential execution returned as a script result. |
| 298 | +
|
| 299 | +It can be done using `$end` instruction, that is an object with the single `$end` key. The value of the key will be the result of the script execution. As everything, the key can be any valid JSONScript - scalar, data, or the script which result will be the result of the containing script (or function) execution. |
| 300 | +
|
| 301 | +Although it is possible to always use `$end` to explicitly declare the script results, it is not idiomatic and not recommended, as it would require additional processing from the interpreter. |
| 302 | +
|
| 303 | +
|
| 304 | +Exapmle: |
| 305 | +
|
| 306 | +Add the comment to the post with the title "My post": |
| 307 | +
|
| 308 | +```JSON |
| 309 | +[ { "post": |
| 310 | + { "$exec": "router", |
| 311 | + "$method": "GET", |
| 312 | + "args": |
| 313 | + { "url": "/post" } |
| 314 | + { "qs": |
| 315 | + { "title": "My post", |
| 316 | + "limit": 1 } } }, |
| 317 | + "comment": |
| 318 | + { "$exec": "router", |
| 319 | + "$method": "POST", |
| 320 | + "args": |
| 321 | + { "url": "/comments/:post_id", |
| 322 | + "params": { "post_id": { "$ref": "post.id" } }, |
| 323 | + "body": { "text": "My comment" } } } }, |
| 324 | + { "$end": { "$ref": "-.comment" } } ] |
| 325 | +``` |
| 326 | + |
| 327 | +In the example above the result of `{ "$ref": "-.comment" }` is the result of substep "comment" in the previous step. `{ "$ref": "0.comment" }` would return the same, but the former is idiomatic and recommended as it allows adding additional steps in the beginning without changing the `$end` instruction. |
| 328 | + |
| 329 | +Without the end instruction the post would also be returned, which is a waste in case the client doesn't need it. |
| 330 | + |
| 331 | + |
| 332 | +### $each - basic iteration and mapping construct |
| 333 | + |
| 334 | +Syntax: |
| 335 | + |
| 336 | +``` |
| 337 | +{ "$each": <JSONScript>, |
| 338 | + / "$as": "<reference_name>", / |
| 339 | + "$do": <JSONScript>, |
| 340 | + / "$concurrency": <JSONScript> / } |
| 341 | +``` |
| 342 | + |
| 343 | +/ ... / above means optional elements. |
| 344 | + |
| 345 | + |
| 346 | +The `$each` construct allows to call another script passing each element of the previous data structure (or script result) as an argument. |
| 347 | + |
| 348 | +`$each` - data structure or the result of another script to iterate, can be object or array |
| 349 | + |
| 350 | +`$do` - the script that will be executed with each element of the structure in $each. |
| 351 | + |
| 352 | +`$as` - the reference name to use in arguments in the script in `$do`. If `$as` key is not specified, the element value can be referred to as { $ref: "~" }. It is recommended in simple cases. But in complex cases giving a name to the iteration reference can add clarity. |
| 353 | + |
| 354 | +`$concurrency` - prevents concurrency or specifies the maximum number of steps that can be executed in parallel. If `$concurrency` key is not specified, arrays and objects in `$each` key are iterated in parallel. `$concurrency: false` prevents concurrency completely. `$concurrency: <number>` limits the number of parallel tasks. The value for `$concurrency` can also be the result of JSONScript script. |
| 355 | + |
| 356 | +Examples: |
| 357 | + |
| 358 | + |
0 commit comments