├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config.json ├── package-lock.json ├── package.json └── src ├── core ├── builtins.js ├── fp.js ├── operators.js ├── peg │ ├── grammer.pegjs │ └── parser.js ├── policy │ └── key │ │ └── run-policy.js ├── regex │ ├── parser.js │ └── parser.spec.js ├── render.js ├── strings.js ├── strings.spec.js └── times.js ├── extension └── policy │ └── collapse_snake_case.js ├── transform.js ├── transform.spec.js └── util └── logger.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "node": "4.3", 6 | "browsers": ["last 10 versions", "ie >= 7"] 7 | }, 8 | "debug": false 9 | }] 10 | ], 11 | "plugins": ["@babel/plugin-transform-object-rest-spread", 12 | ["@babel/plugin-transform-runtime", { 13 | "regenerator": true, 14 | "corejs": 3 15 | }] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/core/peg/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // Available settings at http://eslint.org/docs/user-guide/configuring 2 | module.exports = { 3 | "parser": "@babel/eslint-parser", 4 | "env": { 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended" 11 | ], 12 | "rules": { 13 | // Rules definitions at http://eslint.org/docs/rules/ 14 | // Overrides for "all" profile 15 | "arrow-body-style": "warn", 16 | "arrow-parens": ["off", "as-needed", { "requireForBlockBody": true }], 17 | "complexity": ["warn", 5], // Valid use cases exist 18 | "consistent-return" : "off", 19 | "curly": "warn", 20 | "dot-location": ["error", "property"], 21 | "dot-notation" : "off", 22 | "func-names" : "off", 23 | "func-style" : "off", 24 | "global-require": "warn", 25 | "generator-star-spacing": ["error", "both"], 26 | "guard-for-in": "warn", 27 | "id-length" : "off", 28 | "init-declarations" : "off", 29 | "line-comment-position" : "off", 30 | "max-len" : ["warn", {"code": 160, "ignoreComments": true}], 31 | "max-lines" : "warn", 32 | "max-params" : ["warn", 5], 33 | "max-statements" : "warn", 34 | "multiline-ternary" : "off", 35 | "newline-after-var" : "off", 36 | "newline-before-return" : "off", 37 | "newline-per-chained-call" : ["error", { ignoreChainWithDepth: 3 }], 38 | "no-console": "warn", 39 | "no-else-return" : "off", 40 | "no-extra-parens": ["error", "functions"], 41 | "no-inline-comments" : "off", // Valid use cases exist, e.g. <-- this 42 | "no-magic-numbers": "warn", 43 | "no-process-env" : "off", 44 | "no-prototype-builtins" : "off", 45 | "no-return-assign": "warn", 46 | "no-shadow": "warn", 47 | "no-sync": "warn", 48 | "no-ternary" : "off", 49 | "no-trailing-spaces": ["error", { "skipBlankLines": true }], 50 | "no-undefined": "warn", // Keep? undefined triggers default function arguments, null doesn't 51 | "no-unused-vars": "warn", 52 | "no-use-before-define" : "off", 53 | "no-warning-comments": "warn", 54 | "object-curly-newline" : ["off", { "multiline": true, "minProperties": 2 }], 55 | "object-curly-spacing" : "off", 56 | "object-property-newline" : "off", 57 | "object-shorthand": "warn", 58 | "one-var" : ["error", "never"], 59 | "operator-linebreak" : ["error", "after"], 60 | "prefer-promise-reject-errors": "warn", 61 | "quote-props" : ["warn", "as-needed"], 62 | "quotes" : ["warn", "single"], 63 | "padded-blocks" : "off", 64 | "require-await": "off", // Async function is a contract for returning a promise, and a clean way to create one. 65 | "require-jsdoc" : "off", 66 | "sort-keys" : "off", // No value 67 | "space-before-function-paren" : "off", 68 | "strict" : "off", 69 | /* Quiet down rules temporarily */ 70 | "no-underscore-dangle": "warn", // Builtin streams use that 71 | "valid-jsdoc": "warn", // Not currently used in Siren 72 | "no-empty-function": "warn", // Valid use cases exist 73 | "no-invalid-this": "warn", // Under evaluation 74 | "consistent-this": "warn", 75 | "capitalized-comments": "off", 76 | "comma-dangle": "warn", // Was recently supported in arrays and object literals to minimize merge-lines 77 | "prefer-destructuring": "warn", 78 | "no-plusplus": "off", 79 | "callback-return": "warn", 80 | "spaced-comment": "warn", 81 | "lines-around-comment": "warn", 82 | "no-await-in-loop": "warn", 83 | "no-param-reassign": "warn", 84 | "no-negated-condition": "off", 85 | "no-nested-ternary": "off", 86 | "no-confusing-arrow": ["warn", {"allowParens": true}], 87 | "no-continue": "off", 88 | "no-return-await": "off", 89 | "no-extend-native": "warn", 90 | "no-constant-condition": "warn", 91 | "no-useless-escape": "warn", //complains about escape inside regex 92 | "no-implicit-coercion": "warn", 93 | "wrap-regex" : "warn" 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm install 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm install 31 | - run: npm run build 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | npm-debug.log 4 | .idea 5 | .vscode 6 | /coverage/ 7 | /src/.sandbox 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | #cache: 3 | # directories: 4 | # - node_modules 5 | # - "$HOME/.npm" 6 | notifications: 7 | email: false 8 | node_js: 9 | - '8.16' 10 | before_install: 11 | - if [[ `npm -v` != 6* ]]; then npm i -g npm@6.4.1; fi 12 | install: 13 | - npm ci 14 | # keep the npm cache around to speed up installs 15 | before_script: 16 | - npm prune 17 | script: 18 | - npm run test 19 | - npm run build 20 | 21 | after_success: 22 | - if [[ "$TRAVIS_PULL_REQUEST" == "false" ]] && [[ "$TRAVIS_BRANCH" == "master" ]]; then echo -e "Build passed, semantic-releasing to www.npmjs.com from branch [$TRAVIS_BRANCH] ..." && npm run semantic-release; fi 23 | - if [[ "$TRAVIS_BRANCH" != "master" ]]; then echo -e "Build passed, not publishing from branch [$TRAVIS_BRANCH]"; fi 24 | branches: 25 | except: 26 | - /^v\d+\.\d+\.\d+$/ 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | Issues and pull-requests are welcome. This projects uses automated release notes/changelog generation. That is why commits should follow the `conventional-changelog-format` enforced by ([Commitizen](https://github.com/commitizen/cz-cli)). 3 | For a detailed explanation of how commit messages should be formatted, the guidelines are identical to the standard used by [Angular](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md) and described in detail [here](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format) 4 | 5 | To use `commitizen` conventional-change-log format for your pull requests, commit your code by using `npm run commit` and follow the prompt. 6 | 7 | Pull requests are issued against the `develop` branch, which is set to be the default on github. 8 | 9 | ## License 10 | 11 | [MIT](LICENSE) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Shady Dawood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | master|develop|npm 2 | ---|---|--- 3 | [![Build Status](https://travis-ci.org/sdawood/json-tots.svg?branch=master)](https://travis-ci.org/sdawood/json-tots)|[![Build Status](https://travis-ci.org/sdawood/json-tots.svg?branch=develop)](https://travis-ci.org/sdawood/json-tots)|[![npm version](https://badge.fury.io/js/json-tots.svg)](https://badge.fury.io/js/json-tots) 4 | 5 | # json-tots 6 | `json-tots` offers a JSON Template Of Templates Declarative Transformation and Rendering engine. 7 | Everything is JSON, the template, the document, and extended arguments for template functions. 8 | Always think of your transformations as a pipeline of more than one template, where every output of a render is fed as a document for the next template rendering; when you do so, you should rarely walk into corner cases that you might wonder if a complicated feature should cover such an elaborate use case. 9 | 10 | json-tots supports: 11 | - JSON shape transformation 12 | - The full power of jsonpath to query document 13 | - Arbitrary nesting 14 | - Template aggregation 15 | - Advanced string interpolation 16 | - Piping rendered values through one or more functions (builtin or user-defined) 17 | - Extended arguments for user-defined functions that can be deref'd from document 18 | - for-each-sub-template iteration 19 | - for-each-sub-document iteration 20 | - zip-align sub-templates with sub-documents 21 | 22 | **Try it out `online`** [here](https://npm.runkit.com/json-tots) 23 | 24 | ### 3.0.0 (2018-11-18) 25 | #### Features 26 | - PEG.js parser instead of the handwritten regex parser: [grammer](src/core/peg/grammer.pegls) 27 | - major refactoring 28 | 29 | ### 2.3.1 (2018-11-15) 30 | #### Features 31 | - apply key policies using the new `:` operator - alpha quality 32 | - add stage applyPolicies 33 | 34 | ### 2.0.0 (2018-11-07) 35 | #### Features 36 | - #tag, # (template-string-path) #$ (leaf-path): Tag with a string-tag, current template-string (f19412d) 37 | - insertion order sensitve self back-refernece: a later tag-reference can successfully deref an (c831c32) 38 | - add stage reRenderTags 39 | #### BREAKING CHANGES 40 | - #tag, # (template-string-path) #$ (leaf-path): Refined tagging syntax for # without a string tag 41 | 42 | See [tests](src/transform.spec.js) for examples 43 | 44 | Usage: 45 | 46 | ```js 47 | const tots = require('json-tots'); 48 | 49 | const document = { 50 | a: 1, 51 | b: [10, 20, 30], 52 | c: { 53 | d: 100 54 | } 55 | }; 56 | 57 | const template = { 58 | x: '{{a}}', 59 | y: { 60 | yy: '{{..d} | add:50 | gt:128}', 61 | }, 62 | z: '{{b[1]}}', 63 | w: '{{c.d}}' 64 | }; 65 | 66 | const result = tots.transform(template)(document); // transform is a higher order function 67 | 68 | // result 69 | // { x: 1, y: { yy: true }, z: 20, w: 100 } 70 | ``` 71 | 72 | For advanced use-cases see below 73 | 74 | ## Installation 75 | 76 | ```sh 77 | npm install json-tots --save 78 | ``` 79 | 80 | ## Introduction 81 | json-tots renders your template to the same JSON shape, without mutating neither the template nor the document. 82 | Things get interesting when: 83 | - You use the string-template syntax `{{}}` in standalone or within a string literal. 84 | - Also within any array in the template, any item that is a string-template can utilize the inception operators to consume subsequent items following some interesting application of document and template items. See Inception Operators below for more information. 85 | - While all features are **Text** based by design to achieve declarative, versionable transformations; for practicality, if you inject a function (a runtime function reference) any where in your template object tree, it would be invoked and the whole `document` is passed as an argument, the returned value is rendered as-is to the result, i.e. returned result is NOT `JSON.stringify'd`. 86 | 87 | This allows for experimentation and extensibility, for example returning an object that has a new function reference in its attributes, then re-run `transform`. `Inception` ideas of partial-templates and multi-staged rendering are endless. 88 | 89 | 90 | The opening curly braces can include one or more `operator`, while the closing curly braces can include `pipes`, which is a pipeline of functions to apply to the rendered partial-result. 91 | For example: 92 | 93 | ```js 94 | const {transform} = require('./transform'); 95 | 96 | const document = {log: {user: [{firstName: 'John', lastName: 'Smith'}, {firstName: 'Sally', lastName: 'Doe'}]}}; 97 | const template = {message: 'Hello {{..user.*.firstName}}, welcome to json-tots'}; 98 | 99 | const result = transform(template)(document); 100 | console.log(result); 101 | 102 | // { message: 'Hello Sally, welcome to json-tots' } 103 | 104 | ``` 105 | 106 | Note, we used the power of jsonpath recursive query `..user` to find deeply nested user tags in the document, then we enumerate all users using `.*` and select the `firstName` for each. 107 | Although there are more than one user, following XPath convention, we have asked for value-of, which would only return one result. 108 | To retrieve all results of our jsonpath query, we can tell json-tots that we expect `one or more` values using a `operator` namely `+` 109 | 110 | ```js 111 | const {transform} = require('./transform'); 112 | 113 | const document = {log: {user: [{firstName: 'John', lastName: 'Smith'}, {firstName: 'Sally', lastName: 'Doe'}]}}; 114 | const template = {message: 'Hello {+{..user.*.firstName}}, welcome to json-tots'}; 115 | 116 | const result = transform(template)(document); 117 | console.log(result); 118 | 119 | // { message: 'Hello John,Sally, welcome to json-tots' } 120 | 121 | ``` 122 | 123 | For a refresher of what jsonpath is capable of, please check the [jsonpath](https://www.npmjs.com/package/jsonpath) npm package documentation. 124 | This particular packages is powerful since it covers all of the [proposed jsonpath syntax](http://goessner.net/articles/JsonPath/), and also it uses an optimized/cached parser to parse the path string. I've personally been using it for years and contributed a couple of features to it. 125 | 126 | Note: for readbility and practicality, the jsonpath part of the template-string is `WITHOUT` the `$.` prefix. 127 | 128 | ## Interface 129 | ```js 130 | /** 131 | * Transforms JSON document using a JSON template 132 | * @param template Template JSON 133 | * @param sources A map of alternative document-sources, including `default` source 134 | * @param tags Reference to a map that gets populated with Tags 135 | * @param functions A map of user-defined function, if name-collision occurs with builtin functions, user-defined functions take precedence 136 | * @param args A map of extended arguments to @function expressions, args keys are either functionName (if used only once), functionKey (if globally unique) or functionPath which is unique but ugliest option to write 137 | * @param config Allows to override defaultConfig 138 | * @param builtins A map of builtin functions, defaults to ./core/builtins.js functions 139 | * @returns {function(*=): *} 140 | */ 141 | const transform = (template, {meta = 0, sources = {'default': {}}, tags = {}, functions = {}, args = {}, config = defaultConfig} = {}, {builtins = bins} = {}) => document => {...} 142 | ``` 143 | 144 | ## JSONPath Syntax 145 | 146 | Here are syntax and examples adapted from [Stefan Goessner's original post](http://goessner.net/articles/JsonPath/) introducing JSONPath in 2007. 147 | 148 | JSONPath | Description 149 | -----------------|------------ 150 | `$` | The root object/element 151 | `@` | The current object/element 152 | `.` | Child member operator 153 | `..` | Recursive descendant operator; JSONPath borrows this syntax from E4X 154 | `*` | Wildcard matching all objects/elements regardless their names 155 | `[]` | Subscript operator 156 | `[,]` | Union operator for alternate names or array indices as a set 157 | `[start:end:step]` | Array slice operator borrowed from ES4 / Python 158 | `?()` | Applies a filter (script) expression via static evaluation 159 | `()` | Script expression via static evaluation 160 | 161 | And some examples: 162 | 163 | JSONPath | Description 164 | ------------------------------|------------ 165 | `$.store.book[*].author` | The authors of all books in the store 166 | `$..author` | All authors 167 | `$.store.*` | All things in store, which are some books and a red bicycle 168 | `$.store..price` | The price of everything in the store 169 | `$..book[2]` | The third book 170 | `$..book[(@.length-1)]` | The last book via script subscript 171 | `$..book[-1:]` | The last book via slice 172 | `$..book[0,1]` | The first two books via subscript union 173 | `$..book[:2]` | The first two books via subscript array slice 174 | `$..book[?(@.isbn)]` | Filter all books with isbn number 175 | `$..book[?(@.price<10)]` | Filter all books cheaper than 10 176 | `$..book[?(@.price==8.95)]` | Filter all books that cost 8.95 177 | `$..book[?(@.price<30 && @.category=="fiction")]` | Filter all fiction books cheaper than 30 178 | `$..*` | All members of JSON structure 179 | 180 | Now that we have covered the template structure (everything is JSON) and learned the power of jsonpath, let's look at the template-string operators and pipes syntax. 181 | 182 | ## Template String Syntax Reference 183 | 184 | json-tots Template String | Description 185 | ------------------------------|------------ 186 | `"Arbitrary text { [*] {} [*] } and then some more text"` | A JSON string literal that includes a place holder containing a jsonpath to be derefed from the document (or scoped-document), `operators` and `pipes` are optional. multiple operators can be separated with a |, similarly for pipes. 187 | **Operators**|**Description** 188 | **Query Operators**| Examples: `'{+{a.b.c}}', '{+10{a.b.c}}', '{-10{a.b.c}}'` 189 | `+`|Return all jsonpath query results as an Array, without `+` we get only one result. 190 | `+n`|Take exactly items, where n is a numerical value. 191 | `-n`|Skip exactly items, where n is a numerical value. 192 | **Constraint Operators**| Examples: `'{?=default{a.b.c}}'`, `'{?=default:"Not available"{a.b.c}}'`, `'{?=myOtherSource{a.b.c}}'`, `'{?=myOtherSource:"Not available"{a.b.c}}'`, `'{!{a.b.c}}'`, `'{!=myOtherSource{a.b.c}}', '{!=default{a.b.c}}'`, ... 193 | `?`| Explicitly forces `optional` value, if value is missing, key would vanish from rendered result. Default behavior if `?` is not used. 194 | `?=default`| If value is missing, look it up in default source (`sources['default']`) if provided, if missing from default source, key would vanish from rendered result 195 | `?=default:`| If value is missing, look it up in default source (`sources['default']`) if provided; if missing from default source, use the `` provided `inline` 196 | `?=`| If value is missing, look it up in alternate source (`sources[]`) if provided, if missing from default source, key would vanish from rendered result 197 | `?=:`| If value is missing, look it up in alternate source (`sources[]`) if provided; if missing from alternate source, use the `` provided `inline` 198 | `!`| Explicitly forces `required` value, if value is missing, value would be set to `null` in rendered result 199 | `!=default`| If value is missing, look it up in default source (`sources['default']`) if provided, if missing from default source, value would be set to `null` in rendered result 200 | `!=default:`| If value is missing, look it up in default source (`sources['default']`) if provided; if missing from default source, use the `` provided `inline` 201 | `!=`| If value is missing, look it up in alternate source (`sources[]`) if provided, if missing from default source, value would be set to `null` in rendered result 202 | `!=:`| If value is missing, look it up in alternate source (`sources[]`) if provided; if missing from alternate source, use the `` provided `inline` 203 | **Symbol Operator**| Examples: `'{#myTagName{a.b.c}}'` 204 | `#`| Adds the value to the `tags` mapping if provided, the current JSON node's template-string {{`path`}} is used as `key` 205 | `#$`| Adds the value to the `tags` mapping if provided, the current JSON node's `template-path` is used as `key` 206 | `#