├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cucumber.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── serializer.ts └── types.ts └── test ├── cucumberSnippet.js ├── features └── Serializer.feature └── steps ├── index.ts └── steps.ts /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .rts2_cache_cjs 3 | .rts2_cache_es 4 | .rts2_cache_umd 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | # next.js build output 66 | .next 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.7.13] - 2023-04-20 9 | 10 | ### Updated 11 | 12 | - Updated license copyright to be in line with SaaSquatch open-source policy 13 | 14 | ## [1.7.12] - 2019-11-29 15 | 16 | ### Updated 17 | 18 | - No release notes 19 | 20 | ## [1.7.11] - 2019-11-29 21 | 22 | ### Updated 23 | 24 | - No release notes 25 | 26 | ## [1.7.10] - 2019-11-27 27 | 28 | ### Updated 29 | 30 | - No release notes 31 | 32 | ## [1.7.9] - 2019-11-27 33 | 34 | ### Updated 35 | 36 | - No release notes 37 | 38 | ## [1.7.8] - 2019-11-27 39 | 40 | ### Updated 41 | 42 | - No release notes 43 | 44 | ## [1.7.7] - 2019-11-26 45 | 46 | ### Updated 47 | 48 | - No release notes 49 | 50 | ## [1.7.6] - 2019-11-26 51 | 52 | ### Updated 53 | 54 | - No release notes 55 | 56 | ## [1.7.5] - 2019-11-25 57 | 58 | ### Updated 59 | 60 | - No release notes 61 | 62 | ## [1.7.4] - 2019-11-25 63 | 64 | ### Updated 65 | 66 | - No release notes 67 | 68 | ## [1.7.2] - 2019-10-29 69 | 70 | ### Updated 71 | 72 | - No release notes 73 | 74 | ## [1.7.1] - 2019-10-24 75 | 76 | ### Updated 77 | 78 | - No release notes 79 | 80 | ## [1.7.0] - 2019-10-24 81 | 82 | ### Updated 83 | 84 | - No release notes 85 | 86 | ## [1.0.1] - 2019-10-22 87 | 88 | ### Added 89 | 90 | - Initial release 91 | 92 | [unreleased]: https://github.com/saasquatch/jsonata-ui-core/compare/jsonata-ui-core@0.4.3...HEAD 93 | [1.7.13]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/jsonata-ui-core%401.7.13 94 | [1.7.12]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.12 95 | [1.7.11]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.11 96 | [1.7.10]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.10 97 | [1.7.9]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.9 98 | [1.7.8]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.8 99 | [1.7.7]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.7 100 | [1.7.6]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.6 101 | [1.7.5]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.5 102 | [1.7.4]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.4 103 | [1.7.2]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.2 104 | [1.7.1]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.1 105 | [1.7.0]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.7.0 106 | [1.0.1]: https://github.com/saasquatch/jsonata-ui-core/releases/tag/v1.0.1 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ReferralSaaSquatch.com, Inc. 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 | # jsonata-ui-core 2 | 3 | [![npm version](https://badge.fury.io/js/jsonata-ui-core.svg)](https://badge.fury.io/js/jsonata-ui-core) 4 | 5 | Core AST and serializers for jsonata-ui 6 | 7 | ```sh 8 | npm install jsonata-ui-core 9 | ``` 10 | 11 | The core library includes a serializer to turning JSONata ASTs back into strings. 12 | 13 | ```js 14 | import {serializer} from "jsonata-ui-core"; 15 | import jsonata from "jsonata" 16 | 17 | const input = `a.b.c = "foo"`; 18 | const ast = jsonata(input).ast(); 19 | const output = serializer(ast); 20 | console.log("There and back again", input, output); 21 | ``` 22 | [![Edit jsonata serializer demo](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/jsonata-serializer-demo-q67m3?fontsize=14) 23 | 24 | ## Versions matching with JSONata 25 | 26 | This library serializes ASTs, and aims to match versions numbers with jsonata-js. This is because the ASTs returned by jsonata vary significantly by version. 27 | 28 | | [jsonata](https://www.npmjs.com/package/jsonata) | Jsonata-ui-core | 29 | |---------|-----------------| 30 | | 1.7.x | 1.7.x | 31 | | <1.6.x | Not supported | 32 | 33 | 34 | ## Not Implemented 35 | 36 | - Some operators not yet implemented 37 | - Complex `thunk` functions: `λ($f) { λ($x) { $x($x) }( λ($g) { $f( (λ($a) {$g($g)($a)}))})}(λ($f) { λ($n) { $n < 2 ? 1 : $n * $f($n - 1) } })(6)` 38 | - Regex: `$matcher := /[a-z]*an[a-z]*/i` 39 | - Partial function application `( $first5 := $substring(?, 0, 5); $first5("Hello, World") )` 40 | - Transform operator: `| Account.Order.Product | {'Price': Price * 1.2} |` 41 | - Conditional without else: `Account ? null` vs `Account ? null : true` 42 | - Merged back into main `jsonata-js` package 43 | 44 | ## Sponsors 45 | 46 | Sponsored by [SaaSquatch](http://saasquatch.com). Loyalty, point and referral programs for forward-looking companies. 47 | -------------------------------------------------------------------------------- /cucumber.js: -------------------------------------------------------------------------------- 1 | 2 | var common = [ 3 | "test/features/**/*.feature", 4 | "--require-module ts-node/register", 5 | `--format ${process.env.CI ? "progress" : "progress-bar"}`, 6 | `--format-options '{"snippetSyntax": "test/cucumberSnippet.js"}'`, 7 | `--require test/steps/**/*.ts --tags \"not @skip\"` 8 | ].join(" "); 9 | 10 | module.exports = { 11 | default: common 12 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonata-ui-core", 3 | "version": "1.7.13", 4 | "description": "Core AST and serializers for jsonata-ui", 5 | "source": "src/index.ts", 6 | "main": "dist/index.js", 7 | "unpkg": "dist/index.umd.js", 8 | "types": "dist/src/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "start": "microbundle --watch", 14 | "build": "microbundle -f cjs,umd", 15 | "test": "nyc cucumber-js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/jsonata-ui/jsonata-ui-core.git" 20 | }, 21 | "keywords": [ 22 | "jsonata" 23 | ], 24 | "author": "ReferralSaaSquatch.com, Inc.", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/jsonata-ui/jsonata-ui-core/issues" 28 | }, 29 | "homepage": "https://github.com/jsonata-ui/jsonata-ui-core#readme", 30 | "devDependencies": { 31 | "@types/chai": "^4.2.4", 32 | "@types/cucumber": "^4.0.7", 33 | "chai": "^4.2.0", 34 | "cucumber": "^6.0.2", 35 | "jsonata": "^1.7.0", 36 | "microbundle": "^0.11.0", 37 | "nyc": "^14.1.1", 38 | "ts-node": "^8.4.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import serializer, { escapeString } from "./serializer"; 2 | 3 | export { serializer, escapeString } 4 | export * from "./types" -------------------------------------------------------------------------------- /src/serializer.ts: -------------------------------------------------------------------------------- 1 | import { JsonataASTNode, ObjectUnaryNode, ArrayUnaryNode } from "./types"; 2 | 3 | // TODO: Should eventually replace this with types from the `jsonata` package once they're complete. 4 | type AST = JsonataASTNode; 5 | 6 | export function escapeString(name:string){ 7 | if ( 8 | /\s/.test(name) 9 | || ["null", "false", "true"].includes(name) 10 | || /^\d/.test(name) 11 | || !(/^[a-zA-Z()._]+$/.test(name)) 12 | ) { 13 | return "`" + name + "`"; 14 | } 15 | return name; 16 | } 17 | 18 | export default function serializer(node: AST): string { 19 | if(!node) return undefined; 20 | if (node.type === "binary") { 21 | return serializer(node.lhs) + " " + node.value + " " + serializer(node.rhs); 22 | } else if (node.type === "function") { 23 | /*{ 24 | "type": "function", 25 | "value": "(", 26 | "position": 9, 27 | "arguments": [ 28 | { 29 | "value": 10, 30 | "type": "number", 31 | "position": 11 32 | } 33 | ], 34 | "procedure": { 35 | "value": "revenue", 36 | "type": "variable", 37 | "position": 8 38 | } 39 | } 40 | */ 41 | return ( 42 | serializer(node.procedure) + 43 | "(" + 44 | (node.arguments ? node.arguments.map(serializer).join(", ") : "") + 45 | ")" 46 | ); 47 | } else if (node.type === "variable") { 48 | let stages = ""; 49 | if (node.stages) { 50 | stages = node.stages.map(serializer).join(""); 51 | } 52 | const predicate = node.predicate 53 | ? node.predicate.map(serializer).join() 54 | : ""; 55 | return "$" + node.value + predicate + stages; 56 | } else if (node.type === "wildcard") { 57 | return node.value; 58 | } else if (node.type === "descendant") { 59 | return node.value; 60 | } else if (node.type === "number") { 61 | return JSON.stringify(node.value); 62 | } else if (node.type === "string") { 63 | return JSON.stringify(node.value); 64 | } else if (node.type === "name") { 65 | let stages = ""; 66 | if (node.stages) { 67 | stages = node.stages.map(serializer).join(""); 68 | } 69 | let bind = ""; 70 | if (node.focus){ 71 | let index = node.index ? "$" + node.index : ""; 72 | let focus = node.focus ? "$" + node.focus : ""; 73 | bind = "@" + index + focus 74 | } 75 | let name = node.value; 76 | name = escapeString(name); 77 | return name + bind + stages; 78 | } else if (node.type === "filter") { 79 | return "[" + serializer(node.expr) + "]"; 80 | } else if (node.type === "bind") { 81 | return serializer(node.lhs) + " " + node.value + " " + serializer(node.rhs); 82 | } else if (node.type === "lambda") { 83 | return ( 84 | "function (" + 85 | node.arguments.map(arg => serializer(arg)).join(", ") + 86 | ") {" + 87 | serializer(node.body) + 88 | "}" 89 | ); 90 | } else if (node.type === "condition") { 91 | return (`${serializer(node.condition)} ? ${serializer(node.then)}${node.else ? ` : ${serializer(node.else)}` : ''}`); 92 | } else if (node.type === "value") { 93 | if (node.value === null) return "null"; 94 | if (node.value === false) return "false"; 95 | if (node.value === true) return "true"; 96 | throw Error("Unhandled value node " + node.value); 97 | } else if (node.type === "block") { 98 | return "(" + node.expressions.map(serializer).join("; ") + ")"; 99 | } else if (node.type === "path") { 100 | return node.steps.reduce((arr,c) => [ ...(arr.length?[...arr, c.type ==="sort" ? "^":"."]:[]), serializer(c)], []).join(""); 101 | } else if (node.type === "apply") { 102 | return serializer(node.lhs) + " " + node.value + " " + serializer(node.rhs); 103 | } else if (node.type === "sort") { 104 | return "(" + node.terms.map( t=> t.descending?">":"<" + serializer(t.expression)) + ")"; 105 | } else if (node.type === "unary") { 106 | if (node.value === "{" && node.type === "unary") { 107 | let o = node as ObjectUnaryNode; 108 | return ( 109 | node.value + 110 | "\n\t" + 111 | o.lhs 112 | .map( 113 | (set: JsonataASTNode[]) => 114 | serializer(set[0]) + ":" + serializer(set[1]) 115 | ) 116 | .join(",\n\t") + 117 | "\n}" 118 | ); 119 | } else if (node.value === "[") { 120 | let a = node as ArrayUnaryNode; 121 | return node.value + a.expressions.map(serializer).join(", ") + "]"; 122 | } else { 123 | throw Error("Unhandled unary node " + node.value); 124 | } 125 | } 126 | 127 | return "Error: Invalid node type."; 128 | } 129 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Node { 2 | position: number; 3 | value: unknown; 4 | } 5 | 6 | export interface NumberNode extends Node { 7 | type: "number"; 8 | value: number; 9 | } 10 | 11 | export interface StringNode extends Node { 12 | type: "string"; 13 | value: string; 14 | } 15 | 16 | export interface BinaryNode extends Node { 17 | type: "binary"; 18 | lhs: JsonataASTNode; 19 | rhs: JsonataASTNode; 20 | value: BinaryValue; 21 | } 22 | 23 | export interface BindNode extends Node { 24 | type: "bind"; 25 | value: ":="; 26 | lhs: VariableNode; 27 | rhs: JsonataASTNode; 28 | } 29 | 30 | type BinaryValue = 31 | | "=" 32 | | "!=" 33 | | ">" 34 | | "<" 35 | | ">=" 36 | | "<=" 37 | | "in" 38 | | "+" 39 | | "-" 40 | | "/" 41 | | "*" 42 | | "%"; 43 | 44 | export interface FunctionNode extends Node { 45 | type: "function"; 46 | value: "("; 47 | arguments: JsonataASTNode[]; 48 | procedure: VariableNode; 49 | } 50 | 51 | export interface VariableNode extends Node { 52 | type: "variable"; 53 | value: string; 54 | predicate?: JsonataASTNode[]; 55 | stages?: JsonataASTNode[]; 56 | } 57 | 58 | export interface PathNode extends Node { 59 | type: "path"; 60 | steps: JsonataASTNode[]; 61 | } 62 | 63 | export interface BlockNode extends Node { 64 | type: "block"; 65 | expressions: JsonataASTNode[]; 66 | } 67 | 68 | export interface ApplyNode extends Node { 69 | type: "apply"; 70 | value: "~>"; 71 | lhs: JsonataASTNode; 72 | rhs: JsonataASTNode; 73 | } 74 | 75 | export interface SortNode extends Node { 76 | type: "sort"; 77 | terms: [ 78 | { 79 | descending: boolean; 80 | expression: JsonataASTNode; 81 | } 82 | ]; 83 | } 84 | 85 | export type UnaryNode = ObjectUnaryNode | ArrayUnaryNode; 86 | 87 | export interface ObjectUnaryNode extends Node { 88 | type: "unary"; 89 | value: "{"; 90 | lhs: UnaryTuple[]; 91 | } 92 | 93 | export interface ArrayUnaryNode extends Node { 94 | type: "unary"; 95 | value: "["; 96 | expressions: JsonataASTNode[]; 97 | consarray: boolean; 98 | } 99 | 100 | type UnaryTuple = [JsonataASTNode, JsonataASTNode]; 101 | 102 | export interface FilterNode extends Node { 103 | type: "filter"; 104 | expr: JsonataASTNode; 105 | } 106 | 107 | export interface ValueNode extends Node { 108 | type: "value"; 109 | value: true | false | null; 110 | } 111 | 112 | export interface NameNode extends Node { 113 | type: "name"; 114 | value: string; 115 | stages?: JsonataASTNode[]; 116 | focus?: string; 117 | index?: string; 118 | tuple?: true; 119 | } 120 | 121 | export interface WildcardNode extends Node { 122 | type: "wildcard"; 123 | value: "*" | "**"; 124 | } 125 | export interface DescendantNode extends Node { 126 | type: "descendant"; 127 | value: string; 128 | } 129 | 130 | export interface ConditionNode extends Node { 131 | type: "condition"; 132 | condition: JsonataASTNode; 133 | then: JsonataASTNode; 134 | else?: JsonataASTNode; 135 | } 136 | 137 | export interface LambdaNode extends Node { 138 | type: "lambda"; 139 | arguments: JsonataASTNode[]; 140 | body: JsonataASTNode; 141 | } 142 | 143 | export type LiteralNode = NumberNode | StringNode | ValueNode; 144 | 145 | export type JsonataASTNode = 146 | | NumberNode 147 | | StringNode 148 | | ValueNode 149 | | BinaryNode 150 | | FunctionNode 151 | | VariableNode 152 | | PathNode 153 | | BlockNode 154 | | ApplyNode 155 | | UnaryNode 156 | | FilterNode 157 | | NameNode 158 | | WildcardNode 159 | | DescendantNode 160 | | ConditionNode 161 | | BindNode 162 | | LambdaNode 163 | | SortNode; 164 | 165 | export const module = true; 166 | -------------------------------------------------------------------------------- /test/cucumberSnippet.js: -------------------------------------------------------------------------------- 1 | class TypescriptSnippetSyntax { 2 | constructor() {} 3 | 4 | build({ comment, generatedExpressions, functionName, stepParameterNames }) { 5 | const functionKeyword = "async function "; 6 | const implementation = "return 'pending';"; 7 | const worldTypeDef = "this:World"; 8 | 9 | const definitionChoices = generatedExpressions.map( 10 | (generatedExpression, index) => { 11 | const prefix = index === 0 ? "" : "// "; 12 | 13 | const generatedParams = generatedExpression.parameterNames.concat( 14 | stepParameterNames 15 | ); 16 | const allParameterNames = [worldTypeDef, ...generatedParams]; 17 | return `${prefix + functionName}('${generatedExpression.source.replace( 18 | /'/g, 19 | "\\'" 20 | )}', ${functionKeyword}(${allParameterNames.join(", ")}) {\n`; 21 | } 22 | ); 23 | 24 | return ( 25 | `${definitionChoices.join("")}` + 26 | ` ${implementation}\n` + 27 | `});` 28 | ); 29 | } 30 | } 31 | 32 | module.exports = TypescriptSnippetSyntax; -------------------------------------------------------------------------------- /test/features/Serializer.feature: -------------------------------------------------------------------------------- 1 | Feature: AST Serializer 2 | 3 | Serializes JSONata expressions back into valid strings. 4 | 5 | 6 | Scenario Outline: Serialized ASTs match after two rounds 7 | Given a jsonata expression 8 | """ 9 | 10 | """ 11 | When I parse it to an AST 12 | And serialize the AST 13 | Then parse the serialized value 14 | And the ASTs should match 15 | When I serialize the new AST 16 | Then the new serialized copy should match the first serialized copy 17 | # Because comments and whitespace aren't included in the AST 18 | But the serialized version won't match the original jsonata expression 19 | 20 | Examples: 21 | | expr | 22 | | foo | 23 | | "foo" | 24 | | true | 25 | | false | 26 | | 1 | 27 | | 1.1 | 28 | | foo = 1 | 29 | | foo.bar | 30 | | foo[bar =1 ] | 31 | | $.foo | 32 | | *.foo | 33 | | **.foo | 34 | | foo ~> $max() | 35 | | foo^(bar) | 36 | | loans@$foo#$bar | 37 | | loans@$foo | 38 | | loans#$bar | 39 | | foo-bar | 40 | | foo+bar | 41 | | foo/bar | 42 | | foo*bar | 43 | | `😊` | 44 | | "foo-bar" | 45 | | "foo+bar" | 46 | | "foo/bar" | 47 | | "foo*bar" | 48 | | "foo-bar" | 49 | | "foo+bar" | 50 | | "foo/bar" | 51 | | "foo*bar" | 52 | | "\"" | 53 | | foo_bar | 54 | | `foo_bar` | 55 | | foo1bar | 56 | | foobar1 | 57 | | foobar_1 | 58 | | `1foobar` | 59 | | `foo-bar` | 60 | | `foo+bar` | 61 | | `foo/bar` | 62 | | `foo*bar` | 63 | | `foobar-1` | 64 | | foo.bar.baz | 65 | | foo.foo_bar | 66 | 67 | 68 | -------------------------------------------------------------------------------- /test/steps/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setWorldConstructor 3 | } from "cucumber"; 4 | 5 | declare interface World { 6 | state: Readonly>; 7 | 8 | /** 9 | * Like React's setState method 10 | * 11 | * @param newState 12 | */ 13 | setState(newState: Partial): World; 14 | } 15 | 16 | declare interface State { 17 | expr: string; 18 | ast: any; 19 | ast2: any; 20 | serialized: string; 21 | serialized2: string; 22 | } 23 | 24 | class _CustomWorld implements World { 25 | state: Readonly> = {}; 26 | setState(newState: Partial): World { 27 | this.state = { 28 | ...this.state, 29 | ...newState 30 | }; 31 | return this; 32 | } 33 | } 34 | 35 | setWorldConstructor(_CustomWorld); 36 | 37 | export { World, State }; 38 | -------------------------------------------------------------------------------- /test/steps/steps.ts: -------------------------------------------------------------------------------- 1 | import { Given, When, Then } from "cucumber"; 2 | import * as jsonata from "jsonata"; 3 | import { expect } from "chai"; 4 | 5 | import { World } from "."; 6 | import { serializer } from "../../src/index"; 7 | 8 | Given("a jsonata expression", async function(this: World, expr) { 9 | this.setState({ expr }); 10 | }); 11 | 12 | When("I parse it to an AST", async function(this: World) { 13 | const ast = jsonata(this.state.expr).ast(); 14 | this.setState({ ast }); 15 | }); 16 | 17 | When("serialize the AST", async function(this: World) { 18 | const serialized = serializer(this.state.ast); 19 | console.log("serialized", serialized); 20 | this.setState({ serialized }); 21 | }); 22 | 23 | Then("parse the serialized value", async function(this: World) { 24 | const ast2 = jsonata(this.state.serialized).ast(); 25 | this.setState({ ast2 }); 26 | }); 27 | 28 | function filterClone(obj: any, key: string) { 29 | const clone = JSON.parse(JSON.stringify(obj)); 30 | return filterObject(clone, key); 31 | } 32 | function filterObject(obj: any, key: string) { 33 | for (var i in obj) { 34 | if (!obj.hasOwnProperty(i)) continue; 35 | if (typeof obj[i] == "object") { 36 | filterObject(obj[i], key); 37 | } else if (i == key) { 38 | delete obj[key]; 39 | } 40 | } 41 | return obj; 42 | } 43 | 44 | Then("the ASTs should match", async function(this: World) { 45 | const one = filterClone(this.state.ast, "position"); 46 | const two = filterClone(this.state.ast, "position"); 47 | expect(one).to.be.deep.equals(two); 48 | }); 49 | 50 | When("I serialize the new AST", async function(this: World) { 51 | const serialized2 = serializer(this.state.ast2); 52 | this.setState({ serialized2 }); 53 | }); 54 | 55 | Then( 56 | "the new serialized copy should match the first serialized copy", 57 | async function(this: World) { 58 | expect(this.state.serialized).to.be.equal(this.state.serialized2); 59 | } 60 | ); 61 | 62 | Then( 63 | "the serialized version won't match the original jsonata expression", 64 | async function(this: World) { 65 | // Since this only maybe happens, nothing to implemented 66 | } 67 | ); 68 | --------------------------------------------------------------------------------