├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature-request.md ├── pull_request_template.md └── workflows │ ├── codecov.yml │ ├── commitlint.yml │ ├── housekeeping.yml │ ├── pr-description-enforcer.yml │ ├── release-please.yml │ ├── semantic-pr.yml │ ├── tests.yml │ └── verify.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .release-please-manifest.json ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── commitlint.config.js ├── index.html ├── jest.config.ts ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── readme.md ├── release-please-config.json ├── sonar-project.properties ├── src ├── constants.ts ├── engine.test.ts ├── engine.ts ├── errors │ ├── index.ts │ ├── lexer.ts │ ├── mapping.ts │ ├── parser.ts │ └── translator.ts ├── index.ts ├── lexer.ts ├── operator.test.ts ├── operators.ts ├── parser.ts ├── reverse_translator.test.ts ├── reverse_translator.ts ├── translator.ts ├── types.ts ├── utils │ ├── common.test.ts │ ├── common.ts │ ├── converter.test.ts │ ├── converter.ts │ ├── index.ts │ └── translator.ts └── vite-env.d.ts ├── stryker.conf.json ├── test ├── e2e.test.ts ├── scenario.test.ts ├── scenarios │ ├── arrays │ │ ├── data.ts │ │ └── template.jt │ ├── assignments │ │ ├── data.ts │ │ └── template.jt │ ├── bad_templates │ │ ├── bad_array_coalese_expr.jt │ │ ├── bad_async_usage.jt │ │ ├── bad_context_var.jt │ │ ├── bad_function_params.jt │ │ ├── bad_function_rest_param.jt │ │ ├── bad_number.jt │ │ ├── bad_regex.jt │ │ ├── bad_string.jt │ │ ├── data.ts │ │ ├── empty_block.jt │ │ ├── empty_object_vars_for_definition.jt │ │ ├── incomplete_statement.jt │ │ ├── invalid_new_function_call.jt │ │ ├── invalid_object_vars_for_definition.jt │ │ ├── invalid_token_after_function_def.jt │ │ ├── invalid_variable_assignment1.jt │ │ ├── invalid_variable_assignment2.jt │ │ ├── invalid_variable_assignment3.jt │ │ ├── invalid_variable_assignment4.jt │ │ ├── invalid_variable_assignment5.jt │ │ ├── invalid_variable_assignment6.jt │ │ ├── invalid_variable_assignment7.jt │ │ ├── invalid_variable_assignment8.jt │ │ ├── invalid_variable_assignment9.jt │ │ ├── invalid_variable_definition.jt │ │ ├── object_with_invalid_closing.jt │ │ ├── object_with_invalid_key.jt │ │ ├── reserved_id.jt │ │ ├── unknown_token.jt │ │ └── unsupported_assignment.jt │ ├── base │ │ └── data.ts │ ├── bindings │ │ ├── async.jt │ │ ├── data.ts │ │ ├── new_operator.jt │ │ └── template.jt │ ├── block │ │ ├── data.ts │ │ └── template.jt │ ├── comments │ │ ├── data.ts │ │ └── template.jt │ ├── comparisons │ │ ├── anyof.jt │ │ ├── contains.jt │ │ ├── data.ts │ │ ├── empty.jt │ │ ├── ends_with.jt │ │ ├── ends_with_ignore_case.jt │ │ ├── eq.jt │ │ ├── ge.jt │ │ ├── gte.jt │ │ ├── in.jt │ │ ├── le.jt │ │ ├── lte.jt │ │ ├── ne.jt │ │ ├── noneof.jt │ │ ├── not_in.jt │ │ ├── regex.jt │ │ ├── size.jt │ │ ├── starts_with.jt │ │ ├── starts_with_ignore_case.jt │ │ ├── string_contains_ignore_case.jt │ │ ├── string_eq.jt │ │ ├── string_eq_ingore_case.jt │ │ ├── string_ne.jt │ │ ├── string_ne_ingore_case.jt │ │ └── subsetof.jt │ ├── compile_time_expressions │ │ ├── data.ts │ │ ├── template.jt │ │ └── two_level_path_processing.jt │ ├── conditions │ │ ├── data.ts │ │ ├── if_block.jt │ │ ├── if_then.jt │ │ ├── objects.jt │ │ ├── template.jt │ │ └── undefined_arr_cond.jt │ ├── context_variables │ │ ├── data.ts │ │ ├── filter.jt │ │ ├── function.jt │ │ ├── selector.jt │ │ └── template.jt │ ├── filters │ │ ├── array_filters.jt │ │ ├── data.ts │ │ ├── invalid_object_index_filters.jt │ │ ├── object_filters.jt │ │ └── object_indexes.jt │ ├── functions │ │ ├── array_functions.jt │ │ ├── data.ts │ │ ├── function_calls.jt │ │ ├── js_date_function.jt │ │ ├── new_operator.jt │ │ ├── parent_scope_vars.jt │ │ ├── promise.jt │ │ └── template.jt │ ├── increment_statements │ │ ├── data.ts │ │ ├── postfix_decrement_on_literal.jt │ │ ├── postfix_decrement_on_non_id.jt │ │ ├── postfix_increment_on_literal.jt │ │ ├── postfix_increment_on_non_id.jt │ │ ├── prefix_decrement_on_literal.jt │ │ └── prefix_increment_on_literal.jt │ ├── inputs │ │ ├── data.ts │ │ └── template.jt │ ├── logics │ │ ├── data.ts │ │ └── template.jt │ ├── loops │ │ ├── break_without_condition.jt │ │ ├── break_without_loop.jt │ │ ├── complex_loop.jt │ │ ├── continue.jt │ │ ├── continue_without_condition.jt │ │ ├── continue_without_loop.jt │ │ ├── data.ts │ │ ├── empty_loop.jt │ │ ├── just_for.jt │ │ ├── no_init.jt │ │ ├── no_test.jt │ │ ├── no_update.jt │ │ ├── statement_after_break.jt │ │ ├── statement_after_continue.jt │ │ └── template.jt │ ├── mappings │ │ ├── all_features.json │ │ ├── context_vars_mapping.json │ │ ├── data.ts │ │ ├── filters.json │ │ ├── index_mappings.json │ │ ├── invalid_array_index_mappings.json │ │ ├── invalid_array_mappings.json │ │ ├── invalid_object_mappings.json │ │ ├── mappings_with_root_fields.json │ │ ├── missing_array_index_mappings.json │ │ ├── nested_mappings.json │ │ ├── non_path_output.json │ │ ├── object_mappings.json │ │ ├── or_mappings.json │ │ ├── override_nested_arrays.json │ │ ├── root_array_mappings.json │ │ ├── root_context_vars_mapping.json │ │ ├── root_index_mappings.json │ │ ├── root_mappings.json │ │ ├── root_nested_mappings.json │ │ ├── root_object_mappings.json │ │ ├── simple_array_mappings.json │ │ ├── template.jt │ │ └── transformations.json │ ├── math │ │ ├── data.ts │ │ └── template.jt │ ├── objects │ │ ├── context_props.jt │ │ ├── data.ts │ │ ├── invalid_context_prop.jt │ │ └── template.jt │ ├── paths │ │ ├── block.jt │ │ ├── data.ts │ │ ├── json_path.jt │ │ ├── options.jt │ │ ├── rich_path.jt │ │ ├── simple_path.jt │ │ └── template.jt │ ├── return │ │ ├── data.ts │ │ ├── return_no_value.jt │ │ ├── return_value.jt │ │ ├── return_without_condition.jt │ │ └── statement_after_return.jt │ ├── selectors │ │ ├── data.ts │ │ ├── template.jt │ │ └── wild_cards.jt │ ├── standard_functions │ │ ├── data.ts │ │ └── template.jt │ ├── statements │ │ ├── data.ts │ │ └── template.jt │ ├── template_strings │ │ ├── data.ts │ │ └── template.jt │ └── throw │ │ ├── data.ts │ │ ├── statement_after_throw.jt │ │ ├── template.jt │ │ └── throw_without_condition.jt ├── types.ts └── utils │ ├── index.ts │ └── scenario.ts ├── tsconfig.json └── vite.config.mts /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | *.jt 3 | coverage 4 | test 5 | commitlint.config.js 6 | jest.config.ts 7 | vite.config.*ts 8 | app.ts 9 | *.properties 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": ["airbnb-base", "airbnb-typescript/base", "plugin:sonarjs/recommended", "prettier"], 8 | "parser": "@typescript-eslint/parser", 9 | "overrides": [], 10 | "parserOptions": { 11 | "requireConfigFile": false, 12 | "ecmaVersion": 12, 13 | "sourceType": "module", 14 | "project": "./tsconfig.json" 15 | }, 16 | "rules": { 17 | "max-classes-per-file": "off", 18 | "import/prefer-default-export": "off", 19 | "@typescript-eslint/no-implied-eval": "warn", 20 | "@typescript-eslint/no-unused-vars": [ 21 | "error", 22 | { 23 | "args": "all", 24 | "argsIgnorePattern": "^_" 25 | } 26 | ], 27 | "no-plusplus": "off", 28 | "consistent-return": "off", // https://eslint.org/docs/latest/rules/consistent-return#when-not-to-use-it 29 | "class-methods-use-this": "off", 30 | "no-param-reassign": "warn", 31 | "no-restricted-syntax": "off", 32 | "no-cond-assign": "warn", 33 | "no-useless-constructor": "warn", 34 | "@typescript-eslint/no-useless-constructor": "warn" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[ISSUE]' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the issue** 10 | Enter a clear and concise description of what the bug/issue is. 11 | 12 | **To Reproduce** 13 | Mention the steps to reproduce the behavior that causes the bug/issue: 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Screenshots** 19 | If applicable, add screenshots to help explain your problem. 20 | 21 | **Required information (please complete the following information):** 22 | 23 | - Package version: [e.g., v0.1.1] 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '[ENHANCEMENT]' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What are the changes introduced in this PR? 2 | 3 | Write a brief explainer on your code changes. 4 | 5 | ## What is the related Linear task? 6 | 7 | Resolves INT-XXX 8 | 9 | ## Please explain the objectives of your changes below 10 | 11 | Put down any required details on the broader aspect of your changes. If there are any dependent changes, **mandatorily** mention them here 12 | 13 | ### Any changes to existing capabilities/behaviour, mention the reason & what are the changes ? 14 | 15 | N/A 16 | 17 | ### Any new dependencies introduced with this change? 18 | 19 | N/A 20 | 21 | ### Any new generic utility introduced or modified. Please explain the changes. 22 | 23 | N/A 24 | 25 | ### Any technical or performance related pointers to consider with the change? 26 | 27 | N/A 28 | 29 | @coderabbitai review 30 | 31 |
32 | 33 | ### Developer checklist 34 | 35 | - [ ] My code follows the style guidelines of this project 36 | 37 | - [ ] **No breaking changes are being introduced.** 38 | 39 | - [ ] All related docs linked with the PR? 40 | 41 | - [ ] All changes manually tested? 42 | 43 | - [ ] Any documentation changes needed with this change? 44 | 45 | - [ ] Is the PR limited to 10 file changes? 46 | 47 | - [ ] Is the PR limited to one linear task? 48 | 49 | - [ ] Are relevant unit and component test-cases added? 50 | 51 | ### Reviewer checklist 52 | 53 | - [ ] Is the type of change in the PR title appropriate as per the changes? 54 | 55 | - [ ] Verified that there are no credentials or confidential data exposed with the changes. 56 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/marketplace/actions/jest-coverage-report 2 | name: 'Code Coverage' 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | jobs: 8 | coverage: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: ArtiomTr/jest-coverage-report-action@v2 13 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Commitlint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | commitlint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version-file: '.nvmrc' 18 | cache: 'npm' 19 | 20 | - name: Install Dependencies 21 | run: npm ci 22 | 23 | - name: Print versions 24 | run: | 25 | git --version 26 | node --version 27 | npm --version 28 | npx commitlint --version 29 | 30 | # Run the commitlint action, considering its own dependencies and yours as well 🚀 31 | # `github.workspace` is the path to your repository. 32 | - uses: wagoid/commitlint-github-action@v6 33 | env: 34 | NODE_PATH: ${{ github.workspace }}/node_modules 35 | with: 36 | commitDepth: 1 37 | -------------------------------------------------------------------------------- /.github/workflows/housekeeping.yml: -------------------------------------------------------------------------------- 1 | name: Handle stale PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '42 1 * * *' 6 | 7 | jobs: 8 | prs: 9 | name: Clean up stale PRs 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/stale@v9 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | operations-per-run: 200 20 | stale-pr-message: 'This PR is considered to be stale. It has been open 20 days with no further activity thus it is going to be closed in 7 days. To avoid such a case please consider removing the stale label manually or add a comment to the PR.' 21 | days-before-pr-stale: 20 22 | days-before-pr-close: 7 23 | stale-pr-label: 'Stale' 24 | 25 | branches: 26 | name: Cleanup old branches 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Run delete-old-branches-action 33 | uses: beatlabs/delete-old-branches-action@v0.0.10 34 | with: 35 | repo_token: ${{ github.token }} 36 | date: '6 months ago' 37 | dry_run: false 38 | delete_tags: false 39 | extra_protected_branch_regex: ^(main|master|release.*|rudder-saas)$ 40 | exclude_open_pr_branches: true 41 | -------------------------------------------------------------------------------- /.github/workflows/pr-description-enforcer.yml: -------------------------------------------------------------------------------- 1 | name: 'Pull Request Description' 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - reopened 8 | 9 | jobs: 10 | enforce: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: rudderlabs/pr-description-enforcer@v1.1.0 15 | with: 16 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 17 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - 'main' 5 | - 'release/*' 6 | 7 | name: release-please 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | outputs: 13 | release_created: ${{ steps.release.outputs.release_created }} 14 | steps: 15 | - name: Extract Branch Name 16 | shell: bash 17 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 18 | id: extract_branch 19 | 20 | - uses: googleapis/release-please-action@v4 21 | id: release 22 | with: 23 | token: ${{ github.token }} 24 | release-type: node 25 | 26 | publish: 27 | runs-on: ubuntu-latest 28 | needs: release 29 | if: ${{ needs.release.outputs.release_created }} 30 | 31 | steps: 32 | # The logic below handles the npm publication: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | # these if statements ensure that a publication only occurs when 36 | # a new release is created: 37 | 38 | - name: Setup Node 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version-file: '.nvmrc' 42 | cache: 'npm' 43 | 44 | - name: Install Dependencies 45 | run: npm ci 46 | 47 | - name: Build Package 48 | run: npm run build 49 | 50 | - name: Configure NPM 51 | run: npm set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 52 | 53 | - name: Publish Package to NPM 54 | run: npm publish 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yml: -------------------------------------------------------------------------------- 1 | name: 'Semantic Pull Requests' 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - labeled 9 | - unlabeled 10 | - converted_to_draft 11 | - ready_for_review 12 | - synchronize 13 | 14 | jobs: 15 | main: 16 | name: Validate PR Title 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: amannn/action-semantic-pull-request@v5 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | types: | 24 | fix 25 | feat 26 | chore 27 | refactor 28 | exp 29 | docs 30 | test 31 | requireScope: false 32 | subjectPattern: ^(?![A-Z]).+$ 33 | subjectPatternError: | 34 | The subject "{subject}" found in the pull request title "{title}" 35 | didn't match the configured pattern. Please ensure that the subject 36 | doesn't start with an uppercase character. 37 | # For work-in-progress PRs you can typically use draft pull requests 38 | # from GitHub. However, private repositories on the free plan don't have 39 | # this option and therefore this action allows you to opt-in to using the 40 | # special "[WIP]" prefix to indicate this state. This will avoid the 41 | # validation of the PR title and the pull request checks remain pending. 42 | # Note that a second check will be reported if this is enabled. 43 | wip: true 44 | # When using "Squash and merge" on a PR with only one commit, GitHub 45 | # will suggest using that commit message instead of the PR title for the 46 | # merge commit, and it's easy to commit this by mistake. Enable this option 47 | # to also validate the commit message for one commit PRs. 48 | validateSingleCommit: true 49 | # Related to `validateSingleCommit` you can opt-in to validate that the PR 50 | # title matches a single commit to avoid confusion. 51 | validateSingleCommitMatchesPrTitle: true 52 | # If the PR contains one of these labels, the validation is skipped. 53 | # Multiple labels can be separated by newlines. 54 | # If you want to rerun the validation when labels change, you might want 55 | # to use the `labeled` and `unlabeled` event triggers in your workflow. 56 | ignoreLabels: | 57 | bot 58 | dependencies 59 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | run-tests: 7 | name: Run Tests 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup Node 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version-file: '.nvmrc' 17 | cache: 'npm' 18 | 19 | - name: Install Dependencies 20 | run: npm ci 21 | 22 | - name: Run Tests 23 | run: npm run test 24 | 25 | - name: Run Lint Checks 26 | run: | 27 | npm run check:lint 28 | 29 | - name: Upload coverage reports to Codecov 30 | uses: codecov/codecov-action@v5 31 | with: 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | directory: ./reports/coverage 34 | 35 | - name: Update sonar-project.properties 36 | run: | 37 | # Retrieve the version from package.json 38 | version=$(node -e "console.log(require('./package.json').version)") 39 | # Update the sonar-project.properties file with the version 40 | sed -i "s/sonar.projectVersion=.*$/sonar.projectVersion=$version/" sonar-project.properties 41 | 42 | - name: Fix filesystem paths in generated reports 43 | if: always() 44 | run: | 45 | sed -i 's+home/runner/work/rudder-json-template-engine/rudder-json-template-engine+/github/workspace+g' reports/coverage/lcov.info 46 | sed -i 's+/home/runner/work/rudder-json-template-engine/rudder-json-template-engine+/github/workspace+g' reports/eslint.json 47 | 48 | - name: SonarCloud Scan 49 | if: always() 50 | uses: SonarSource/sonarqube-scan-action@v4 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.PAT }} 53 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | formatting-lint: 8 | name: Check for formatting & lint errors 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | # Make sure the actual branch is checked out when running on pull requests 16 | ref: ${{ github.head_ref }} 17 | 18 | - name: Setup Node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version-file: .nvmrc 22 | cache: 'npm' 23 | 24 | - name: Install Dependencies 25 | run: npm ci 26 | 27 | - name: Run Lint Checks 28 | run: | 29 | npm run lint 30 | 31 | - run: git diff --exit-code 32 | 33 | - name: Error message 34 | if: ${{ failure() }} 35 | run: | 36 | echo 'Eslint check is failing Ensure you have run `npm run lint` and committed the files locally.' 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | reports 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | 133 | # build files 134 | build/ 135 | 136 | # stryker temp files 137 | .stryker-tmp 138 | 139 | Mac 140 | .DS_Store 141 | 142 | test/test_engine.ts -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm test 5 | npm run lint:fix 6 | npm run lint:check 7 | npm run lint-staged -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.18.3 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.19.5" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Test Current TS File", 9 | "program": "${relativeFile}", 10 | "request": "launch", 11 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], 12 | "skipFiles": ["/**"], 13 | "type": "node" 14 | }, 15 | { 16 | "name": "Run Current JS File", 17 | "program": "${relativeFile}", 18 | "request": "launch", 19 | "skipFiles": ["/**"], 20 | "type": "node" 21 | }, 22 | { 23 | "runtimeExecutable": "/usr/local/bin/node", 24 | "type": "node", 25 | "request": "launch", 26 | "name": "Jest Current File", 27 | "program": "${workspaceFolder}/node_modules/.bin/jest", 28 | "args": ["${fileBasenameNoExtension}", "--config", "jest.config.ts"], 29 | "console": "integratedTerminal", 30 | "internalConsoleOptions": "neverOpen", 31 | "windows": { 32 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 33 | } 34 | }, 35 | { 36 | "runtimeExecutable": "/usr/local/bin/node", 37 | "name": "Jest Scenario", 38 | "program": "${workspaceFolder}/node_modules/.bin/jest", 39 | "request": "launch", 40 | "skipFiles": ["/**"], 41 | "args": [ 42 | "scenario.test", 43 | "--config", 44 | "jest.config.ts", 45 | "--scenario=${input:scenario}", 46 | "--index=${input:index}" 47 | ], 48 | "type": "node", 49 | "console": "integratedTerminal", 50 | "internalConsoleOptions": "neverOpen", 51 | "windows": { 52 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 53 | } 54 | }, 55 | { 56 | "runtimeExecutable": "/usr/local/bin/node", 57 | "type": "node", 58 | "request": "launch", 59 | "name": "Jest Scenarios", 60 | "program": "${workspaceFolder}/node_modules/.bin/jest", 61 | "args": ["test/e2e.test.ts", "--config", "jest.config.ts", "--scenarios=${input:scenarios}"], 62 | "console": "integratedTerminal", 63 | "internalConsoleOptions": "neverOpen", 64 | "windows": { 65 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 66 | } 67 | }, 68 | { 69 | "name": "Test Engine", 70 | "program": "${workspaceFolder}/test/test_engine.ts", 71 | "request": "launch", 72 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], 73 | "skipFiles": ["/**"], 74 | "type": "node" 75 | } 76 | ], 77 | "inputs": [ 78 | { 79 | "id": "scenarios", 80 | "type": "promptString", 81 | "description": "Enter Scenarios", 82 | "default": "all" 83 | }, 84 | { 85 | "id": "scenario", 86 | "type": "promptString", 87 | "description": "Enter Scenario", 88 | "default": "assignments" 89 | }, 90 | { 91 | "id": "index", 92 | "type": "promptString", 93 | "description": "Enter test index", 94 | "default": "0" 95 | } 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.requireConfig": true, 3 | "prettier.configPath": ".prettierrc", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true 7 | }, 8 | "[javascript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "editor.formatOnSave": true 11 | }, 12 | "[jsonc]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode", 14 | "editor.formatOnSave": true 15 | }, 16 | "[yaml]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode", 18 | "editor.formatOnSave": true 19 | }, 20 | "editor.codeActionsOnSave": { 21 | "source.organizeImports": "never" 22 | }, 23 | "eslint.validate": ["javascript", "typescript"] 24 | } 25 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rudderlabs/integrations 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at **contact@rudderstack.com**. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://www.contributor-covenant.org][contributor-covenant]. 72 | 73 | For answers to common questions about this code of conduct, see the [FAQs][faqs]. 74 | 75 | 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | [contributor-covenant]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 79 | [faqs]: https://www.contributor-covenant.org/faq 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to RudderStack 2 | 3 | Thanks for taking the time and for your help in improving this project! 4 | 5 | ## Table of contents 6 | 7 | - [**RudderStack Contributor Agreement**](#rudderstack-contributor-agreement) 8 | - [**How you can contribute to RudderStack**](#how-you-can-contribute-to-rudderstack) 9 | - [**Committing**](#committing) 10 | - [**Getting help**](#getting-help) 11 | 12 | ## RudderStack Contributor Agreement 13 | 14 | To contribute to this project, we need you to sign the [**Contributor License Agreement (“CLA”)**][CLA] for the first commit you make. By agreeing to the [**CLA**][CLA], we can add you to list of approved contributors and review the changes proposed by you. 15 | 16 | ## How you can contribute to RudderStack 17 | 18 | If you come across any issues or bugs, or have any suggestions for improvement, you can navigate to the specific file in the [**repo**](https://github.com/rudderlabs/rudder-repo-template), make the change, and raise a PR. 19 | 20 | You can also contribute to any open-source RudderStack project. View our [**GitHub page**](https://github.com/rudderlabs) to see all the different projects. 21 | 22 | ## Committing 23 | 24 | We prefer squash or rebase commits so that all changes from a branch are committed to master as a single commit. All pull requests are squashed when merged, but rebasing prior to merge gives you better control over the commit message. 25 | 26 | ## Getting help 27 | 28 | For any questions, concerns, or queries, you can start by asking a question in our [**Slack**](https://rudderstack.com/join-rudderstack-slack-community/) community. 29 | 30 | ### We look forward to your feedback on improving this project! 31 | 32 | 33 | 34 | [issue]: https://github.com/rudderlabs/rudder-server/issues/new 35 | [CLA]: https://rudderlabs.wufoo.com/forms/rudderlabs-contributor-license-agreement 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 RudderStack 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 | 23 | Copyright (c) 2012 Dmitry Filatov 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining a copy 26 | of this software and associated documentation files (the "Software"), to deal 27 | in the Software without restriction, including without limitation the rights 28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | copies of the Software, and to permit persons to whom the Software is 30 | furnished to do so, subject to the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be included in 33 | all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 41 | THE SOFTWARE. 42 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rudder Json Template Engine 8 | 9 | 10 |
11 |

Rudder Json Template Engine

12 |
13 |

Template:

14 | 17 |
18 |
19 |

Data:

20 | 23 |
24 |
25 |

Output:

26 |

27 |       
28 |
29 | 30 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/62/f7brm_9n2p75sc79zx9718_w0000gp/T/jest_dy", 15 | 16 | // Automatically clear mock calls, instances, contexts and results before every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | collectCoverageFrom: ['/src/**/*.[jt]s?(x)'], 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: 'reports/coverage', 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | coveragePathIgnorePatterns: ['/node_modules/', '/build/', 'test', 'vite-env.d.ts'], 30 | 31 | // Indicates which provider should be used to instrument code for coverage 32 | coverageProvider: 'v8', 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | coverageReporters: ['json', 'text', 'lcov', 'clover'], 36 | 37 | // An object that configures minimum threshold enforcement for coverage results 38 | coverageThreshold: { 39 | // global: { 40 | // branches: 100, 41 | // functions: 100, 42 | // lines: 100, 43 | // statements: 100, 44 | // }, 45 | }, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // The default configuration for fake timers 54 | // fakeTimers: { 55 | // "enableGlobally": false 56 | // }, 57 | 58 | // Force coverage collection from ignored files using an array of glob patterns 59 | // forceCoverageMatch: [], 60 | 61 | // A path to a module which exports an async function that is triggered once before all test suites 62 | // globalSetup: undefined, 63 | 64 | // A path to a module which exports an async function that is triggered once after all test suites 65 | // globalTeardown: undefined, 66 | 67 | // A set of global variables that need to be available in all test environments 68 | // globals: {}, 69 | 70 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 71 | // maxWorkers: "50%", 72 | 73 | // An array of directory names to be searched recursively up from the requiring module's location 74 | // moduleDirectories: [ 75 | // "node_modules" 76 | // ], 77 | 78 | // An array of file extensions your modules use 79 | // moduleFileExtensions: [ 80 | // "js", 81 | // "mjs", 82 | // "cjs", 83 | // "jsx", 84 | // "ts", 85 | // "tsx", 86 | // "json", 87 | // "node" 88 | // ], 89 | 90 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 91 | // moduleNameMapper: {}, 92 | 93 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 94 | // modulePathIgnorePatterns: [], 95 | 96 | // Activates notifications for test results 97 | // notify: false, 98 | 99 | // An enum that specifies notification mode. Requires { notify: true } 100 | // notifyMode: "failure-change", 101 | 102 | // A preset that is used as a base for Jest's configuration 103 | preset: 'ts-jest', 104 | 105 | // Run tests from one or more projects 106 | // projects: undefined, 107 | 108 | // Use this configuration option to add custom reporters to Jest 109 | // reporters: undefined, 110 | 111 | // Automatically reset mock state before every test 112 | // resetMocks: false, 113 | 114 | // Reset the module registry before running each individual test 115 | // resetModules: false, 116 | 117 | // A path to a custom resolver 118 | // resolver: undefined, 119 | 120 | // Automatically restore mock state and implementation before every test 121 | // restoreMocks: false, 122 | 123 | // The root directory that Jest should scan for tests and modules within 124 | // rootDir: undefined, 125 | 126 | // A list of paths to directories that Jest should use to search for files in 127 | // roots: [ 128 | // "" 129 | // ], 130 | 131 | // Allows you to use a custom runner instead of Jest's default test runner 132 | // runner: "jest-runner", 133 | 134 | // The paths to modules that run some code to configure or set up the testing environment before each test 135 | // setupFiles: [], 136 | 137 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 138 | // setupFilesAfterEnv: [], 139 | 140 | // The number of seconds after which a test is considered as slow and reported as such in the results. 141 | // slowTestThreshold: 5, 142 | 143 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 144 | // snapshotSerializers: [], 145 | 146 | // The test environment that will be used for testing 147 | testEnvironment: 'node', 148 | 149 | // Options that will be passed to the testEnvironment 150 | // testEnvironmentOptions: {}, 151 | 152 | // Adds a location field to test results 153 | // testLocationInResults: false, 154 | 155 | // The glob patterns Jest uses to detect test files 156 | testMatch: ['/**/__tests__/**/*.[jt]s?(x)', '/**/?(*.)+(spec|test).[tj]s?(x)'], 157 | 158 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 159 | testPathIgnorePatterns: ['/node_modules/', '/build/'], 160 | 161 | // The regexp pattern or array of patterns that Jest uses to detect test files 162 | // testRegex: [], 163 | 164 | // This option allows the use of a custom results processor 165 | // testResultsProcessor: undefined, 166 | 167 | // This option allows use of a custom test runner 168 | // testRunner: "jest-circus/runner", 169 | 170 | // A map from regular expressions to paths to transformers 171 | // transform: undefined, 172 | 173 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 174 | // transformIgnorePatterns: [ 175 | // "/node_modules/", 176 | // "\\.pnp\\.[^\\/]+$" 177 | // ], 178 | 179 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 180 | // unmockedModulePathPatterns: undefined, 181 | 182 | // Indicates whether each individual test should be reported during the run 183 | // verbose: undefined, 184 | 185 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 186 | // watchPathIgnorePatterns: [], 187 | 188 | // Whether to use watchman for file crawling 189 | // watchman: true, 190 | }; 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rudderstack/json-template-engine", 3 | "version": "0.19.5", 4 | "homepage": "https://github.com/rudderlabs/rudder-json-template-engine", 5 | "description": "A library for evaluating JSON template expressions.", 6 | "main": "build/index.js", 7 | "types": "build/index.d.ts", 8 | "keywords": [ 9 | "json", 10 | "jsonpath", 11 | "rudder", 12 | "rudderstack", 13 | "cdp", 14 | "engine" 15 | ], 16 | "author": "RudderStack", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/rudderlabs/rudder-json-template-engine.git" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "devDependencies": { 26 | "@babel/eslint-parser": "^7.19.1", 27 | "@commitlint/cli": "^17.8.1", 28 | "@commitlint/config-conventional": "^17.8.1", 29 | "@types/jest": "^29.4.0", 30 | "@types/mocha": "^10.0.1", 31 | "@types/node": "^18.14.6", 32 | "@typescript-eslint/eslint-plugin": "^6.19.1", 33 | "@typescript-eslint/parser": "^6.19.1", 34 | "commander": "^10.0.0", 35 | "eslint": "^8.35.0", 36 | "eslint-config-airbnb-base": "^15.0.0", 37 | "eslint-config-airbnb-typescript": "^17.1.0", 38 | "eslint-config-prettier": "^8.7.0", 39 | "eslint-plugin-import": "^2.27.5", 40 | "eslint-plugin-promise": "^6.1.1", 41 | "eslint-plugin-sonarjs": "^0.23.0", 42 | "glob": "^10.3.10", 43 | "husky": "^8.0.3", 44 | "jest": "^29.4.3", 45 | "lint-staged": "^15.2.10", 46 | "prettier": "^2.8.4", 47 | "ts-jest": "^29.0.5", 48 | "ts-node": "^10.9.1", 49 | "typescript": "^5.0.0", 50 | "vite": "^6.2.1" 51 | }, 52 | "scripts": { 53 | "test": "jest --coverage --verbose", 54 | "build": "vite build && tsc", 55 | "dev": "vite", 56 | "clean": "rm -rf build", 57 | "build:clean": "npm run clean && npm run build", 58 | "lint:fix": "eslint . --fix", 59 | "lint:check": "eslint . || exit 1", 60 | "format": "prettier --write '**/*.ts' '**/*.js' '**/*.json'", 61 | "lint": "npm run format && npm run lint:fix", 62 | "lint-staged": "lint-staged", 63 | "prepare": "husky install", 64 | "jest:scenarios": "jest e2e.test.ts --verbose", 65 | "test:scenario": "jest test/scenario.test.ts --verbose", 66 | "test:stryker": "stryker run", 67 | "check:lint": "eslint . -f json -o reports/eslint.json || exit 0" 68 | }, 69 | "lint-staged": { 70 | "*.(ts|json)": "prettier --write" 71 | }, 72 | "files": [ 73 | "build/**/*.[jt]s", 74 | "CHANGELOG.md" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rudderlabs/rudder-json-template-engine/b8cfa75cc7a5084cff6a88213e795676eb9bb0db/public/favicon.ico -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

The Customer Data Platform for Developers

8 | 9 |

10 | 11 | Website 12 | · 13 | Documentation 14 | · 15 | Community Slack 16 | 17 |

18 | 19 | --- 20 | 21 | # JSON Template Engine 22 | 23 | ## Overview 24 | 25 | Welcome to our JSON Template Engine! This powerful tool simplifies transforming JSON data from one format to another, making managing and maintaining complex integrations easier. 26 | 27 | ### Why JSON Template Engine? 28 | 29 | As an integration platform supporting over 200 integrations, we understand the challenges of maintaining and optimizing these connections. Traditionally, we used native JavaScript code for data transformation, which required significant effort and maintenance. While JSONata offered a more efficient way to manipulate JSON data, we still encountered performance bottlenecks due to its parsing and interpretation overhead. 30 | 31 | ### Our Solution 32 | 33 | To address these challenges, we've developed our own JSON Transformation Engine. This engine generates optimized JavaScript code from transformation templates, reducing runtime overhead and significantly improving performance. 34 | 35 | ## Key Features 36 | 37 | - **Efficiency**: Our engine generates JavaScript code that minimizes parsing and interpretation overhead, ensuring faster execution. 38 | 39 | - **Extensibility**: Easily add new transformation templates to meet your specific integration needs. 40 | 41 | - **Simplicity**: Write concise transformation templates that are easy to understand and maintain. 42 | 43 | ## Implementation 44 | 45 | This library generates a javascript function code from the template and then uses the function to evaluate the JSON data. It outputs the javascript code in the following stages: 46 | 47 | 1. [Lexing](src/lexer.ts) (Tokenization) 48 | 1. [Parsing](src/parser.ts) (AST Creation) 49 | 1. [Translation](src/translator.ts) (Code generation) 50 | 51 | ```mermaid 52 | flowchart TD; 53 | A[Code] --> B[Convert code to tokens]; 54 | B --> C[Parse tokens to create Expressions]; 55 | C --> D[Combine expressions to create statements]; 56 | D --> E[Combine statements to create AST]; 57 | E --> F[Translate AST to JS code] 58 | ``` 59 | 60 | [Engine](src/engine.ts) class abstracts the above steps and provides a convenient way to use the json templates to evaluate the inputs. 61 | 62 | ## Getting started 63 | 64 | ### Use npm package 65 | 66 | `npm install @rudderstack/json-template-engine` 67 | 68 | ```ts 69 | const { JsonTemplateEngine } = require('@rudderstack/json-template-engine'); 70 | const engine = JsonTemplateEngine.create(`'Hello ' + .name`); 71 | engine.evaluate({ name: 'World' }); // => 'Hello World' 72 | ``` 73 | 74 | ### Use CDN URL directly in the browser 75 | Latest URL: https://cdn.jsdelivr.net/npm/@rudderstack/json-template-engine/build/json-template.min.js 76 | 77 | Versioned URL: https://cdn.jsdelivr.net/npm/@rudderstack/json-template-engine@0.19.5/build/json-template.min.js 78 | 79 | ```html 80 | 85 | ``` 86 | 87 | 88 | Refer this [example](/index.html) for more details. 89 | 90 | [Demo](https://rudderlabs.github.io/rudder-json-template-engine/) 91 | 92 | ### Playground 93 | Give the JSON template engine a try in our [playground](https://transformers-workflow-engine.rudderstack.com/#/json-template) without needing to install anything. 94 | 95 | ## Features 96 | 97 | The template consists of multiple statements, with the output being the result of the final statement. 98 | 99 | 100 | ### Variables 101 | 102 | ```js 103 | const a = 1; 104 | let b = a + 2; 105 | a + b; 106 | ``` 107 | 108 | Refer this [example](test/scenarios/assignments/template.jt) for more details. 109 | 110 | #### Template Strings 111 | 112 | ```js 113 | let a = `Input a=${.a}`; 114 | let b = `Input b=${.b}`; 115 | `${a}, ${b}`; 116 | ``` 117 | Refer this [example](test/scenarios/template_strings/template.jt) for more details. 118 | 119 | ### Basic Expressions 120 | 121 | #### Conditions 122 | 123 | ```js 124 | a > b ? a : c; 125 | ``` 126 | 127 | Refer this [example](test/scenarios/conditions/template.jt) for more details. 128 | 129 | #### Comparisons 130 | 131 | ```js 132 | a === b || c > d; 133 | ``` 134 | 135 | Refer this [example](test/scenarios/comparisons/template.jt) for more details. 136 | 137 | #### Math Operations 138 | 139 | ```js 140 | 10 - 2 + 2 * 10; 141 | ``` 142 | 143 | Refer this [example](test/scenarios/math/template.jt) for more details. 144 | 145 | #### Logical operations 146 | 147 | ```js 148 | false || true; 149 | ``` 150 | 151 | Refer this [example](test/scenarios/logics/template.jt) for more details. 152 | 153 | ### Input and Bindings 154 | 155 | Input refers to the JSON document we would like to process using a template. Bindings refer to additional data or functions we would provide to process the data efficiently. 156 | 157 | Example: 158 | 159 | - Template: `"Hello " + (.name ?? $.defaultName)` 160 | - Evaluation: `engine.evaluate({name: 'World'}, {defaultName: 'World'});` 161 | - `{name: 'World'}` is input. 162 | - `^.name` refers to "name" property of the input. We can also use `.name` to refer the same. `^` always refers to the root of the input and `.` refers to current context. Refer this [example](test/scenarios/selectors/context_variables.jt) for more details. 163 | - `{defaultName: 'World'}` is bindings. 164 | - `$.defaultName` refers to "defaultName" property of the bindings. Refer this [example](test/scenarios/bindings/template.jt) for more details. 165 | 166 | ### Arrays 167 | 168 | ```js 169 | let arr = [1, 2, 3, 4] 170 | let a = arr[1, 2] // [2, 3] 171 | let b = arr[0:2] // [1, 2] 172 | let c = arr[-2:] // [3, 4] 173 | ``` 174 | 175 | Refer this [example](test/scenarios/arrays/template.jt) for more details. 176 | 177 | ### Objects 178 | 179 | ```js 180 | let key = "some key" 181 | // { "a": 1, "b": 2, "c": 3, "some key": 4 } 182 | let obj = {a: 1, b: 2, c: 3, [key]: 4 } 183 | let a = obj["a"] // 1 184 | let b = obj.a // 1 185 | let c = obj{["a", "b"]} // { "a": 1, "b": 2} 186 | let d = obj{~["a", "b"]} // { "c": 3, "some key": 4} 187 | ``` 188 | 189 | Refer this [example](test/scenarios/objects/template.jt) for more details. 190 | 191 | #### Object Context Props 192 | ```js 193 | let obj = {a: 1, b: 2, c: 3 }; 194 | obj.({ 195 | @e [e.key]: e.value * e.value, // @e refers to each key, value pairs, 196 | d: 16 // we can have other props also 197 | }) // { a: 1, b: 4, c: 9, d: 16} 198 | ``` 199 | Refer this [example](test/scenarios/objects/context_props.jt) for more details. 200 | 201 | ### Functions 202 | 203 | #### Normal functions 204 | 205 | ```js 206 | let fn = function (arg1, arg2) { 207 | arg1 + arg2; 208 | }; 209 | ``` 210 | 211 | The result of the last statement of function will be returned as result of the function. We can also use rest params (`...args`). 212 | 213 | #### Lambda/Short functions 214 | 215 | ```js 216 | let fn = array.map(lambda 2 * ?0); 217 | ``` 218 | 219 | This function gets converted to: 220 | 221 | ```js 222 | let fn = array.map(function (args) { 223 | 2 * args[0]; 224 | }); 225 | ``` 226 | 227 | Lambda functions are short to express the intention and it is convenient sometimes. 228 | 229 | #### Async functions 230 | 231 | ```js 232 | let fn = async function (arg1, arg2) { 233 | const result = await doSomethingAsync(arg1, arg2); 234 | doSomethingSync(result); 235 | }; 236 | ``` 237 | 238 | **Note:** When we want to use async functions then we need to create template engine using `JsonTemplateEngine.create`. If you create a template this way then it will be created as an async function so we can `await` anywhere in the template. 239 | 240 | ```js 241 | let result = await doSomething(.a, .b) 242 | ``` 243 | 244 | Refer this [example](test/scenarios/functions/template.jt) for more details. 245 | 246 | ### Paths 247 | 248 | Paths are used to access properties in `input`, `bindings` and `variables`. 249 | 250 | #### Simple Paths 251 | 252 | Simple paths support limited path features and get translated as direct property access statements in the generate javascript code. 253 | `a.b.c` gets translated to `a?.b?.c` so they are very fast compared to [Rich paths](#rich-paths). Simple paths are ideal when we know the object structure. 254 | 255 | **Supported features:** 256 | 257 | - [Simple Selectors](#simple-selectors) 258 | - [Single Index Filters](#single-index-or-property-filters) 259 | Refer this [example](test/scenarios/paths/simple_path.jt) for more details. 260 | 261 | #### Rich Paths 262 | 263 | Rich paths gets converted complex code to support different variations in the data. 264 | 265 | If we use this rich path`~r a.b.c` then it automatically handles following variations. 266 | 267 | - `[{"a": { "b": [{"c": 2}]}}]` 268 | - `{"a": { "b": [{"c": 2}]}}` 269 | - `{"a": [{ "b": [{"c": 2}]}]}` 270 | Refer this [example](test/scenarios/paths/rich_path.jt) for more details. 271 | 272 | #### Json Paths 273 | We support some features of [JSON Path](https://goessner.net/articles/JsonPath/index.html#) syntax using path option (`~j`). 274 | Note: This is an experimental feature and may not support all the features of JSON Paths. 275 | 276 | Refer this [example](test/scenarios/paths/json_path.jt) for more details. 277 | 278 | #### Simple selectors 279 | 280 | ```js 281 | let x = a.b.c; 282 | let y = a."some key".c 283 | ``` 284 | 285 | Refer this [example](test/scenarios/selectors/template.jt) for more details. 286 | 287 | #### Wildcard selectors 288 | 289 | ```js 290 | a.*.c // selects c from any direct property of a 291 | ``` 292 | 293 | Refer this [example](test/scenarios/selectors/wild_cards.jt) for more details. 294 | 295 | #### Descendent selectors 296 | 297 | ```js 298 | // selects c from any child property of a 299 | // a.b.c, a.b1.b2.c or a.b1.b2.b3.c 300 | let x = a..c; 301 | let y = a.."some key"; 302 | ``` 303 | 304 | Refer this [example](test/scenarios/selectors/template.jt) for more details. 305 | 306 | #### Single Index or Property Filters 307 | 308 | ```js 309 | let x = a[0].c; 310 | let y = a[-1].c; // selects last element from array 311 | let z = a['some key'].c; 312 | ``` 313 | 314 | Refer this [example](test/scenarios/filters/array_filters.jt) for more details. 315 | 316 | #### Multi Indexes or Properties Filters 317 | 318 | ```js 319 | let x = a[(0, 2, 5)].c; 320 | let y = a[('some key1', 'some key2')].c; 321 | ``` 322 | 323 | Refer this [example](test/scenarios/filters/array_filters.jt) for more details. 324 | 325 | #### Range filters 326 | 327 | ```js 328 | let x = a[2:5].c; 329 | let y = a[:-2].c; 330 | let z = a[2:].c; 331 | ``` 332 | 333 | #### Object Property Filters 334 | 335 | ```js 336 | let x = obj{["a", "b"]}; // selects a and b 337 | let y = obj{~["a", "b"]}; // selects all properties except a and b 338 | ``` 339 | 340 | Refer this [example](test/scenarios/filters/object_indexes.jt) for more details. 341 | 342 | #### Conditional or Object Filters 343 | 344 | ```js 345 | let x = obj{.a > 1}; 346 | ``` 347 | 348 | Refer this [example](test/scenarios/filters/object_filters.jt) for more details. 349 | 350 | #### Block expressions 351 | 352 | ```js 353 | let x = obj.({ 354 | a: .a + 1, 355 | b: .b + 2 356 | }); 357 | let x = obj.([.a+1, .b+2]); 358 | ``` 359 | 360 | Refer this [example](test/scenarios/paths/block.jt) for more details. 361 | 362 | #### Context Variables 363 | 364 | ```js 365 | .orders@order#idx.products.({ 366 | name: .name, 367 | price: .price, 368 | orderNum: idx, 369 | orderId: order.id 370 | }) 371 | ``` 372 | 373 | Use context variables: `@order` and `#idx`, we can combine properties of orders and products together. Refer this [example](test/scenarios/context_variables/template.jt) for more details. 374 | 375 | #### Path Options 376 | 377 | We can mention defaultPathType while creating engine instance. 378 | 379 | ```js 380 | // For using simple path as default path type 381 | // a.b.c will be treated as simple path 382 | JsonTemplateEngine.create(`a.b.c`, { defaultPathType: PathType.SIMPLE }); 383 | // For using rich path as default path type 384 | // a.b.c will be treated as rich path 385 | JsonTemplateEngine.create(`a.b.c`, { defaultPathType: PathType.RICH }); 386 | ``` 387 | 388 | We can override the default path option using tags. 389 | 390 | ```js 391 | // Use ~s to treat a.b.c as simple path 392 | ~s a.b.c 393 | // Use ~r to treat a.b.c as rich path 394 | ~r a.b.c 395 | // Use ~j for using json paths 396 | ~j items[?(@.a>1)] 397 | ``` 398 | 399 | **Note:** Rich paths are slower compare to the simple paths. 400 | Refer this [example](test/scenarios/paths/options.jt) for more details. 401 | 402 | ### Compile time expressions 403 | 404 | Compile time expressions are evaluated during compilation phase using compileTimeBindings option. 405 | 406 | ```js 407 | // {{$.a.b.c}} gets translated to 1 and 408 | // final translated code will be "let a = 1;" 409 | JsonTemplateEngine.create(`let a = {{$.a.b.c}};`, { 410 | compileTimeBindings: { 411 | a: { 412 | b: { 413 | c: 1, 414 | }, 415 | }, 416 | }, 417 | }); 418 | ``` 419 | 420 | We can use compile time expressions to generate a template and then recompile it as expression. Refer these examples [simple compilation](test/scenarios/compile_time_expressions/template.jt) and [complex compilation](test/scenarios/compile_time_expressions/two_level_path_processing.jt) for more details. 421 | 422 | ### Mappings 423 | If you are familiar with [JSON Paths](https://goessner.net/articles/JsonPath/index.html#), you can easily begin working with JSON templates by leveraging your existing knowledge through the mappings feature. 424 | 425 | **Example:** 426 | * Let's say we want to transform the following data. 427 | * Input: 428 | ```json 429 | { 430 | "a": { 431 | "foo": 1, 432 | "bar": 2 433 | }, 434 | "b": [ 435 | { 436 | "firstName": "foo", 437 | "lastName": "bar" 438 | }, 439 | { 440 | "firstName": "fizz", 441 | "lastName": "buzz" 442 | } 443 | ] 444 | } 445 | ``` 446 | * Output: 447 | ```json 448 | { 449 | "foo": 1, 450 | "bar": 2, 451 | "items":[ 452 | { 453 | "name": "foo bar" 454 | }, 455 | { 456 | "name": "fizz buzz" 457 | } 458 | ] 459 | } 460 | ``` 461 | * Mappings: 462 | ```json 463 | [ 464 | { 465 | "description": "Copies properties of a to root level in the output", 466 | "input": "$.a", 467 | "output": "$" 468 | }, 469 | { 470 | "description": "Combines first and last name in the output", 471 | "input": "$.b[*].(@.firstName + ' ' + @.lastName)", 472 | "output": "$.items[*].name" 473 | } 474 | ] 475 | ``` 476 | * Try this example in our [playground](https://transformers-workflow-engine.rudderstack.com/#/mappings?gist=e25a6ac769ee5719e928720f5c439169). 477 | 478 | For more examples, refer [Mappings](test/scenarios/mappings/) 479 | 480 | ### Comments 481 | 482 | Supports both c style single line (`//`) and block comments (`/* .. */`). 483 | Refer this [example](test/scenarios/comments/template.jt) for more details. 484 | 485 | For more examples, refer [Scenarios](test/scenarios) 486 | 487 | ## Testing 488 | 489 | `npm test` 490 | 491 | ## Contribute 492 | 493 | We would love to see you contribute to RudderStack. Get more information on how to contribute [**here**](CONTRIBUTING.md). 494 | 495 | ## License 496 | 497 | The RudderStack `rudder-json-template-engine` is released under the [**MIT License**](https://opensource.org/licenses/MIT). 498 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "release-type": "node", 4 | "include-component-in-tag": false, 5 | "packages": { 6 | ".": { 7 | "extra-files": ["readme.md"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.log.level=INFO 2 | sonar.verbose=false 3 | sonar.qualitygate.wait=false 4 | 5 | # Project details 6 | sonar.projectKey=rudderlabs_rudder-json-template-engine 7 | sonar.organization=rudderlabs 8 | sonar.projectName=rudder-json-template-engine 9 | sonar.projectVersion=0.8.2 10 | 11 | # Meta-data for the project 12 | sonar.links.scm=https://github.com/rudderlabs/rudder-json-template-engine 13 | sonar.links.issue=https://github.com/rudderlabs/rudder-json-template-engine/issues 14 | 15 | # Path to reports 16 | sonar.javascript.lcov.reportPaths=reports/coverage/lcov.info 17 | sonar.eslint.reportPaths=reports/eslint.json 18 | 19 | # Path to sources 20 | sonar.sources=src 21 | sonar.inclusions=**/*.ts 22 | sonar.exclusions=**/*.json,**/*.html,**/*.png,**/*.jpg,**/*.gif,**/*.svg,**/*.yml 23 | # Path to tests 24 | sonar.tests=test 25 | sonar.test.inclusions=**/*.test.ts 26 | sonar.test.exclusions=**/*.json,**/*.html,**/*.png,**/*.jpg,**/*.gif,**/*.svg 27 | sonar.coverage.exclusions=test/**/*,**/*.json,**/*.html,**/*.png,**/*.jpg,**/*.gif,**/*.svg 28 | 29 | # Source encoding 30 | sonar.sourceEncoding=UTF-8 31 | 32 | # Exclusions for copy-paste detection 33 | sonar.cpd.exclusions=test/**/* -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxType } from './types'; 2 | 3 | export const VARS_PREFIX = '___'; 4 | export const DATA_PARAM_KEY = '___d'; 5 | export const BINDINGS_PARAM_KEY = '___b'; 6 | export const BINDINGS_CONTEXT_KEY = '___b.context.'; 7 | export const RESULT_KEY = '___r'; 8 | export const FUNCTION_RESULT_KEY = '___f'; 9 | export const INDENTATION_SPACES = 4; 10 | export const EMPTY_EXPR = { type: SyntaxType.EMPTY }; 11 | -------------------------------------------------------------------------------- /src/engine.test.ts: -------------------------------------------------------------------------------- 1 | import { JsonTemplateEngine } from './engine'; 2 | import { PathType } from './types'; 3 | 4 | describe('engine', () => { 5 | describe('isValidJSONPath', () => { 6 | it('should return true for valid JSON root path', () => { 7 | expect(JsonTemplateEngine.isValidJSONPath('$.user.name')).toBeTruthy(); 8 | }); 9 | 10 | it('should return true for valid JSON relative path', () => { 11 | expect(JsonTemplateEngine.isValidJSONPath('.user.name')).toBeTruthy(); 12 | 13 | expect(JsonTemplateEngine.isValidJSONPath('@.user.name')).toBeTruthy(); 14 | }); 15 | 16 | it('should return false for invalid JSON path', () => { 17 | expect(JsonTemplateEngine.isValidJSONPath('userId')).toBeFalsy(); 18 | }); 19 | 20 | it('should return false for invalid template', () => { 21 | expect(JsonTemplateEngine.isValidJSONPath('a=')).toBeFalsy(); 22 | }); 23 | 24 | it('should return false for empty path', () => { 25 | expect(JsonTemplateEngine.isValidJSONPath('')).toBeFalsy(); 26 | }); 27 | }); 28 | describe('validateMappings', () => { 29 | it('should validate mappings', () => { 30 | expect(() => 31 | JsonTemplateEngine.validateMappings([ 32 | { 33 | input: '$.userId', 34 | output: '$.user.id', 35 | }, 36 | { 37 | input: '$.discount', 38 | output: '$.events[0].items[*].discount', 39 | }, 40 | ]), 41 | ).not.toThrow(); 42 | }); 43 | 44 | it('should throw error for mappings which are not compatible with each other', () => { 45 | expect(() => 46 | JsonTemplateEngine.validateMappings([ 47 | { 48 | input: '$.a[0]', 49 | output: '$.b[0].name', 50 | }, 51 | { 52 | input: '$.discount', 53 | output: '$.b[0].name[*].discount', 54 | }, 55 | ]), 56 | ).toThrowError('Invalid mapping'); 57 | }); 58 | 59 | it('should throw error for mappings with invalid json paths', () => { 60 | expect(() => 61 | JsonTemplateEngine.validateMappings([ 62 | { 63 | input: 'events[0]', 64 | output: 'events[0].name', 65 | }, 66 | ]), 67 | ).toThrowError('Invalid mapping'); 68 | }); 69 | }); 70 | describe('isValidMapping', () => { 71 | it('should convert to JSON paths when options are used', () => { 72 | expect( 73 | JsonTemplateEngine.isValidMapping( 74 | { 75 | input: '$.a[0]', 76 | output: 'b[0].name', 77 | }, 78 | { defaultPathType: PathType.JSON }, 79 | ), 80 | ).toBeTruthy(); 81 | }); 82 | it('should throw error when output is not valid json path without engine options', () => { 83 | expect( 84 | JsonTemplateEngine.isValidMapping({ 85 | input: '$.events[0]', 86 | output: 'events[0].name', 87 | }), 88 | ).toBeFalsy(); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/engine.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-cycle */ 2 | import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY, EMPTY_EXPR } from './constants'; 3 | import { JsonTemplateMappingError } from './errors/mapping'; 4 | import { JsonTemplateLexer } from './lexer'; 5 | import { JsonTemplateParser } from './parser'; 6 | import { JsonTemplateReverseTranslator } from './reverse_translator'; 7 | import { JsonTemplateTranslator } from './translator'; 8 | import { 9 | EngineOptions, 10 | Expression, 11 | FlatMappingAST, 12 | FlatMappingPaths, 13 | PathType, 14 | SyntaxType, 15 | TemplateInput, 16 | } from './types'; 17 | import { CreateAsyncFunction, convertToObjectMapping, isExpression } from './utils'; 18 | 19 | export class JsonTemplateEngine { 20 | private readonly fn: Function; 21 | 22 | private constructor(fn: Function) { 23 | this.fn = fn; 24 | } 25 | 26 | private static compileAsSync(template: TemplateInput, options?: EngineOptions): Function { 27 | // eslint-disable-next-line @typescript-eslint/no-implied-eval 28 | return Function( 29 | DATA_PARAM_KEY, 30 | BINDINGS_PARAM_KEY, 31 | JsonTemplateEngine.translate(template, options), 32 | ); 33 | } 34 | 35 | private static compileAsAsync(templateOrExpr: TemplateInput, options?: EngineOptions): Function { 36 | return CreateAsyncFunction( 37 | DATA_PARAM_KEY, 38 | BINDINGS_PARAM_KEY, 39 | JsonTemplateEngine.translate(templateOrExpr, options), 40 | ); 41 | } 42 | 43 | private static translateExpression(expr: Expression): string { 44 | const translator = new JsonTemplateTranslator(expr); 45 | return translator.translate(); 46 | } 47 | 48 | static isValidJSONPath(path: string = ''): boolean { 49 | try { 50 | const expression = JsonTemplateEngine.parse(path, { defaultPathType: PathType.JSON }); 51 | const statement = expression.statements?.[0]; 52 | return ( 53 | statement && 54 | statement.type === SyntaxType.PATH && 55 | (!statement.root || statement.root === DATA_PARAM_KEY) 56 | ); 57 | } catch (e) { 58 | return false; 59 | } 60 | } 61 | 62 | private static toJsonPath(input: string): string { 63 | if (input.startsWith('$')) { 64 | return input; 65 | } 66 | 67 | // Check if it's wrapped in quotes 68 | // or is a simple identifier 69 | // or contains dots 70 | if ( 71 | /^'.*'$/.test(input) || 72 | /^".*"$/.test(input) || 73 | /^\w+$/.test(input) || 74 | input.includes('.') 75 | ) { 76 | return `$.${input}`; 77 | } 78 | 79 | // If input contains special characters 80 | return `$.'${input}'`; 81 | } 82 | 83 | private static convertToJSONPath(path?: string, options?: EngineOptions): string | undefined { 84 | if (!path) { 85 | return path; 86 | } 87 | if (options?.defaultPathType === PathType.JSON) { 88 | return JsonTemplateEngine.toJsonPath(path); 89 | } 90 | return path; 91 | } 92 | 93 | private static prepareMapping(mapping: FlatMappingPaths, options?: EngineOptions) { 94 | return { 95 | ...mapping, 96 | input: mapping.input ?? mapping.from, 97 | output: JsonTemplateEngine.convertToJSONPath(mapping.output ?? mapping.to, options), 98 | }; 99 | } 100 | 101 | private static prepareMappings( 102 | mappings: FlatMappingPaths[], 103 | options?: EngineOptions, 104 | ): FlatMappingPaths[] { 105 | return mappings 106 | .map((mapping) => JsonTemplateEngine.prepareMapping(mapping, options)) 107 | .filter((mapping) => mapping.input && mapping.output); 108 | } 109 | 110 | static validateMappings(mappings: FlatMappingPaths[], options?: EngineOptions) { 111 | JsonTemplateEngine.prepareMappings(mappings, options).forEach((mapping) => { 112 | if (!JsonTemplateEngine.isValidJSONPath(mapping.output)) { 113 | throw new JsonTemplateMappingError( 114 | 'Invalid mapping: invalid JSON path', 115 | mapping.input as string, 116 | mapping.output as string, 117 | ); 118 | } 119 | }); 120 | JsonTemplateEngine.parseMappingPaths(mappings, options); 121 | } 122 | 123 | static isValidMapping(mapping: FlatMappingPaths, options?: EngineOptions) { 124 | try { 125 | JsonTemplateEngine.validateMappings([mapping], options); 126 | return true; 127 | } catch (e) { 128 | return false; 129 | } 130 | } 131 | 132 | private static createFlatMappingsAST( 133 | mappings: FlatMappingPaths[], 134 | options?: EngineOptions, 135 | ): FlatMappingAST[] { 136 | const newOptions = { ...options, mappings: true }; 137 | return JsonTemplateEngine.prepareMappings(mappings, options) 138 | .filter((mapping) => mapping.input && mapping.output) 139 | .map((mapping) => ({ 140 | ...mapping, 141 | inputExpr: JsonTemplateEngine.parse(mapping.input, newOptions).statements[0], 142 | outputExpr: JsonTemplateEngine.parse(mapping.output, newOptions).statements[0], 143 | })); 144 | } 145 | 146 | static parseMappingPaths(mappings: FlatMappingPaths[], options?: EngineOptions): Expression { 147 | return convertToObjectMapping(JsonTemplateEngine.createFlatMappingsAST(mappings, options)); 148 | } 149 | 150 | static create(templateOrExpr: TemplateInput, options?: EngineOptions): JsonTemplateEngine { 151 | return new JsonTemplateEngine(JsonTemplateEngine.compileAsAsync(templateOrExpr, options)); 152 | } 153 | 154 | static createAsSync(template: TemplateInput, options?: EngineOptions): JsonTemplateEngine { 155 | return new JsonTemplateEngine(JsonTemplateEngine.compileAsSync(template, options)); 156 | } 157 | 158 | static parse(template: TemplateInput, options?: EngineOptions): Expression { 159 | if (!template) { 160 | return EMPTY_EXPR; 161 | } 162 | if (isExpression(template)) { 163 | return template as Expression; 164 | } 165 | if (typeof template === 'string') { 166 | const lexer = new JsonTemplateLexer(template); 167 | const parser = new JsonTemplateParser(lexer, options); 168 | return parser.parse(); 169 | } 170 | return JsonTemplateEngine.parseMappingPaths(template as FlatMappingPaths[], options); 171 | } 172 | 173 | static translate(template: TemplateInput, options?: EngineOptions): string { 174 | return JsonTemplateEngine.translateExpression(JsonTemplateEngine.parse(template, options)); 175 | } 176 | 177 | static reverseTranslate(expr: Expression | FlatMappingPaths[], options?: EngineOptions): string { 178 | const translator = new JsonTemplateReverseTranslator(options); 179 | let newExpr = expr; 180 | if (Array.isArray(expr)) { 181 | newExpr = JsonTemplateEngine.parseMappingPaths(expr, options); 182 | } 183 | return translator.translate(newExpr as Expression); 184 | } 185 | 186 | static convertMappingsToTemplate(mappings: FlatMappingPaths[], options?: EngineOptions): string { 187 | return JsonTemplateEngine.reverseTranslate( 188 | JsonTemplateEngine.parse(mappings, options), 189 | options, 190 | ); 191 | } 192 | 193 | static evaluateAsSync( 194 | template: TemplateInput, 195 | options: EngineOptions = {}, 196 | data: unknown = {}, 197 | bindings: Record = {}, 198 | ): unknown { 199 | return JsonTemplateEngine.createAsSync(template, options).evaluate(data, bindings); 200 | } 201 | 202 | static evaluate( 203 | template: TemplateInput, 204 | options: EngineOptions = {}, 205 | data: unknown = {}, 206 | bindings: Record = {}, 207 | ): unknown { 208 | return JsonTemplateEngine.create(template, options).evaluate(data, bindings); 209 | } 210 | 211 | evaluate(data: unknown = {}, bindings: Record = {}): unknown { 212 | return this.fn(data, bindings); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lexer'; 2 | export * from './parser'; 3 | export * from './translator'; 4 | -------------------------------------------------------------------------------- /src/errors/lexer.ts: -------------------------------------------------------------------------------- 1 | export class JsonTemplateLexerError extends Error {} 2 | -------------------------------------------------------------------------------- /src/errors/mapping.ts: -------------------------------------------------------------------------------- 1 | export class JsonTemplateMappingError extends Error { 2 | inputMapping: string; 3 | 4 | outputMapping: string; 5 | 6 | constructor(message: string, inputMapping: string, outputMapping: string) { 7 | super(`${message}. Input: ${inputMapping}, Output: ${outputMapping}`); 8 | this.inputMapping = inputMapping; 9 | this.outputMapping = outputMapping; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/errors/parser.ts: -------------------------------------------------------------------------------- 1 | export class JsonTemplateParserError extends Error {} 2 | -------------------------------------------------------------------------------- /src/errors/translator.ts: -------------------------------------------------------------------------------- 1 | export class JsonTemplateTranslatorError extends Error {} 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './engine'; 3 | export * from './errors'; 4 | export * from './lexer'; 5 | export * from './operators'; 6 | export * from './parser'; 7 | export * from './translator'; 8 | export * from './types'; 9 | export * from './utils'; 10 | -------------------------------------------------------------------------------- /src/operator.test.ts: -------------------------------------------------------------------------------- 1 | import { isStandardFunction, standardFunctions } from './operators'; 2 | 3 | describe('Operators tests', () => { 4 | describe('isStandardFunction', () => { 5 | it('should return true for standard functions', () => { 6 | expect(Object.keys(standardFunctions).every(isStandardFunction)).toBeTruthy(); 7 | }); 8 | it('should return false for non standard function', () => { 9 | const nonStandardFunctions = [ 10 | 'toString', 11 | 'valueOf', 12 | 'toLocaleString', 13 | 'hasOwnProperty', 14 | 'isPrototypeOf', 15 | 'propertyIsEnumerable', 16 | 'constructor', 17 | ]; 18 | expect(Object.keys(nonStandardFunctions).every(isStandardFunction)).toBeFalsy(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/operators.ts: -------------------------------------------------------------------------------- 1 | import { VARS_PREFIX } from './constants'; 2 | 3 | function startsWithStrict(val1, val2): string { 4 | return `(typeof ${val1} === 'string' && ${val1}.startsWith(${val2}))`; 5 | } 6 | 7 | function startsWith(val1, val2): string { 8 | const code: string[] = []; 9 | code.push(`(typeof ${val1} === 'string' && `); 10 | code.push(`typeof ${val2} === 'string' && `); 11 | code.push(`${val1}.toLowerCase().startsWith(${val2}.toLowerCase()))`); 12 | return code.join(''); 13 | } 14 | 15 | function endsWithStrict(val1, val2): string { 16 | return `(typeof ${val1} === 'string' && ${val1}.endsWith(${val2}))`; 17 | } 18 | 19 | function endsWith(val1, val2): string { 20 | const code: string[] = []; 21 | code.push(`(typeof ${val1} === 'string' && `); 22 | code.push(`typeof ${val2} === 'string' && `); 23 | code.push(`${val1}.toLowerCase().endsWith(${val2}.toLowerCase()))`); 24 | return code.join(''); 25 | } 26 | 27 | function containsStrict(val1, val2): string { 28 | return `((typeof ${val1} === 'string' || Array.isArray(${val1})) && ${val1}.includes(${val2}))`; 29 | } 30 | 31 | function contains(val1, val2): string { 32 | const code: string[] = []; 33 | code.push(`(typeof ${val1} === 'string' && typeof ${val2} === 'string') ?`); 34 | code.push(`(${val1}.toLowerCase().includes(${val2}.toLowerCase()))`); 35 | code.push(':'); 36 | code.push(`(Array.isArray(${val1}) && (${val1}.includes(${val2})`); 37 | code.push(`|| (typeof ${val2} === 'string' && ${val1}.includes(${val2}.toLowerCase()))))`); 38 | return code.join(''); 39 | } 40 | 41 | export const binaryOperators = { 42 | '===': (val1, val2): string => `${val1}===${val2}`, 43 | 44 | '==': (val1, val2): string => { 45 | const code: string[] = []; 46 | code.push(`((typeof ${val1} == 'string' && `); 47 | code.push(`typeof ${val2} == 'string' && `); 48 | code.push(`${val1}.toLowerCase() == ${val2}.toLowerCase()) || `); 49 | code.push(`${val1} == ${val2})`); 50 | return code.join(''); 51 | }, 52 | 53 | '>=': (val1, val2): string => `${val1}>=${val2}`, 54 | 55 | '>': (val1, val2): string => `${val1}>${val2}`, 56 | 57 | '<=': (val1, val2): string => `${val1}<=${val2}`, 58 | 59 | '<': (val1, val2): string => `${val1}<${val2}`, 60 | 61 | '!==': (val1, val2): string => `${val1}!==${val2}`, 62 | 63 | '!=': (val1, val2): string => { 64 | const code: string[] = []; 65 | code.push(`(typeof ${val1} == 'string' && typeof ${val2} == 'string') ?`); 66 | code.push(`(${val1}.toLowerCase() != ${val2}.toLowerCase())`); 67 | code.push(':'); 68 | code.push(`(${val1} != ${val2})`); 69 | return code.join(''); 70 | }, 71 | 72 | '^==': startsWithStrict, 73 | 74 | '==^': (val1, val2): string => startsWithStrict(val2, val1), 75 | 76 | '^=': startsWith, 77 | 78 | '=^': (val1, val2): string => startsWith(val2, val1), 79 | 80 | '$==': endsWithStrict, 81 | 82 | '==$': (val1, val2): string => endsWithStrict(val2, val1), 83 | 84 | '$=': endsWith, 85 | 86 | '=$': (val1, val2): string => endsWith(val2, val1), 87 | 88 | '=~': (val1, val2): string => 89 | `(${val2} instanceof RegExp) ? (${val2}.test(${val1})) : (${val1}==${val2})`, 90 | 91 | contains, 92 | 93 | '==*': (val1, val2): string => containsStrict(val1, val2), 94 | 95 | '=*': (val1, val2): string => contains(val1, val2), 96 | 97 | size: (val1, val2): string => `${val1}.length === ${val2}`, 98 | 99 | empty: (val1, val2): string => `(${val1}.length === 0) === ${val2}`, 100 | 101 | subsetof: (val1, val2): string => `${val1}.every((el) => {return ${val2}.includes(el);})`, 102 | 103 | anyof: (val1, val2): string => `${val1}.some((el) => {return ${val2}.includes(el);})`, 104 | 105 | '+': (val1, val2): string => `${val1}+${val2}`, 106 | 107 | '-': (val1, val2): string => `${val1}-${val2}`, 108 | 109 | '*': (val1, val2): string => `${val1}*${val2}`, 110 | 111 | '/': (val1, val2): string => `${val1}/${val2}`, 112 | 113 | '%': (val1, val2): string => `${val1}%${val2}`, 114 | 115 | '>>': (val1, val2): string => `${val1}>>${val2}`, 116 | 117 | '<<': (val1, val2): string => `${val1}<<${val2}`, 118 | 119 | '**': (val1, val2): string => `${val1}**${val2}`, 120 | }; 121 | 122 | function getSumFn(prefix: string = ''): string { 123 | return `function ${prefix}sum(arr) { 124 | if(!Array.isArray(arr)) { 125 | throw new Error('Expected an array'); 126 | } 127 | return arr.reduce((a, b) => a + b, 0); 128 | }`; 129 | } 130 | 131 | function getAvgFn(prefix: string = ''): string { 132 | return `function ${prefix}avg(arr) { 133 | if(!Array.isArray(arr)) { 134 | throw new Error('Expected an array'); 135 | } 136 | ${getSumFn()} 137 | return sum(arr) / arr.length; 138 | }`; 139 | } 140 | 141 | export const standardFunctions = { 142 | sum: getSumFn(VARS_PREFIX), 143 | max: `function ${VARS_PREFIX}max(arr) { 144 | if(!Array.isArray(arr)) { 145 | throw new Error('Expected an array'); 146 | } 147 | return Math.max(...arr); 148 | }`, 149 | min: `function ${VARS_PREFIX}min(arr) { 150 | if(!Array.isArray(arr)) { 151 | throw new Error('Expected an array'); 152 | } 153 | return Math.min(...arr); 154 | }`, 155 | avg: getAvgFn(VARS_PREFIX), 156 | length: `function ${VARS_PREFIX}length(arr) { 157 | if(!Array.isArray(arr) && typeof arr !== 'string') { 158 | throw new Error('Expected an array or string'); 159 | } 160 | return arr.length; 161 | }`, 162 | stddev: `function ${VARS_PREFIX}stddev(arr) { 163 | if(!Array.isArray(arr)) { 164 | throw new Error('Expected an array'); 165 | } 166 | ${getAvgFn()} 167 | const mu = avg(arr); 168 | const diffSq = arr.map((el) => (el - mu) ** 2); 169 | return Math.sqrt(avg(diffSq)); 170 | }`, 171 | first: `function ${VARS_PREFIX}first(arr) { 172 | if(!Array.isArray(arr)) { 173 | throw new Error('Expected an array'); 174 | } 175 | return arr[0]; 176 | }`, 177 | last: `function ${VARS_PREFIX}last(arr) { 178 | if(!Array.isArray(arr)) { 179 | throw new Error('Expected an array'); 180 | } 181 | return arr[arr.length - 1]; 182 | }`, 183 | index: `function ${VARS_PREFIX}index(arr, i) { 184 | if(!Array.isArray(arr)) { 185 | throw new Error('Expected an array'); 186 | } 187 | if (i < 0) { 188 | return arr[arr.length + i]; 189 | } 190 | return arr[i]; 191 | }`, 192 | keys: `function ${VARS_PREFIX}keys(obj) { return Object.keys(obj); }`, 193 | }; 194 | 195 | export function isStandardFunction(name: string): boolean { 196 | return Object.prototype.hasOwnProperty.call(standardFunctions, name); 197 | } 198 | -------------------------------------------------------------------------------- /src/reverse_translator.test.ts: -------------------------------------------------------------------------------- 1 | import { JsonTemplateEngine } from './engine'; 2 | import { PathType } from './types'; 3 | 4 | describe('reverse_translator', () => { 5 | it('should reverse translate with indentation', () => { 6 | const template = JsonTemplateEngine.reverseTranslate( 7 | JsonTemplateEngine.parse(`{a: {b: {c: 1}}}`), 8 | ); 9 | expect(template).toEqual( 10 | '{\n "a": {\n "b": {\n "c": 1\n }\n }\n}', 11 | ); 12 | }); 13 | 14 | it('should reverse translate json mappings', () => { 15 | const template = JsonTemplateEngine.reverseTranslate( 16 | [ 17 | { 18 | input: '$.userId', 19 | output: '$.user.id', 20 | }, 21 | { 22 | input: '$.discount', 23 | output: '$.events[0].items[*].discount', 24 | }, 25 | { 26 | input: '$.products[?(@.category)].id', 27 | output: '$.events[0].items[*].product_id', 28 | }, 29 | { 30 | input: '$.events[0]', 31 | output: '$.events[0].name', 32 | }, 33 | { 34 | input: '$.products[?(@.category)].variations[*].size', 35 | output: '$.events[0].items[*].options[*].s', 36 | }, 37 | { 38 | input: '$.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100))', 39 | output: '$.events[0].items[*].value', 40 | }, 41 | { 42 | input: '$.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100)).sum()', 43 | output: '$.events[0].revenue', 44 | }, 45 | ], 46 | { defaultPathType: PathType.JSON }, 47 | ); 48 | expect(template).toEqual( 49 | '{\n "user": {\n "id": $.userId\n },\n "events": [{\n "items": $.products[?(@.category)].({\n "discount": $.discount,\n "product_id": @.id,\n "options": @.variations[*].({\n "s": @.size\n })[],\n "value": @.price * @.quantity * (1 - $.discount / 100)\n })[],\n "name": $.events[0],\n "revenue": $.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100)).sum()\n }]\n}', 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export enum Keyword { 2 | FUNCTION = 'function', 3 | NEW = 'new', 4 | TYPEOF = 'typeof', 5 | LET = 'let', 6 | CONST = 'const', 7 | LAMBDA = 'lambda', 8 | AWAIT = 'await', 9 | ASYNC = 'async', 10 | IN = 'in', 11 | NOT_IN = 'nin', 12 | NOT = 'not', 13 | CONTAINS = 'contains', 14 | SUBSETOF = 'subsetof', 15 | ANYOF = 'anyof', 16 | NONEOF = 'noneof', 17 | EMPTY = 'empty', 18 | SIZE = 'size', 19 | RETURN = 'return', 20 | THROW = 'throw', 21 | CONTINUE = 'continue', 22 | BREAK = 'break', 23 | FOR = 'for', 24 | } 25 | 26 | export enum TokenType { 27 | UNKNOWN = 'unknown', 28 | ID = 'id', 29 | INT = 'int', 30 | FLOAT = 'float', 31 | TEMPLATE = 'template', 32 | STR = 'str', 33 | BOOL = 'bool', 34 | NULL = 'null', 35 | UNDEFINED = 'undefined', 36 | LAMBDA_ARG = 'lambda_arg', 37 | PUNCT = 'punct', 38 | THROW = 'throw', 39 | KEYWORD = 'keyword', 40 | EOT = 'eot', 41 | REGEXP = 'regexp', 42 | } 43 | 44 | // In the order of precedence 45 | export enum OperatorType { 46 | BASE = 'base', 47 | CONDITIONAL = 'conditional', 48 | ASSIGNMENT = 'assignment', 49 | COALESCING = 'coalescing', 50 | OR = 'or', 51 | AND = 'and', 52 | EQUALITY = 'equality', 53 | RELATIONAL = 'relational', 54 | SHIFT = 'shift', 55 | ADDITION = 'addition', 56 | MULTIPLICATION = 'multiplication', 57 | POWER = 'power', 58 | UNARY = 'unary', 59 | PREFIX_INCREMENT = 'prefix_increment', 60 | POSTFIX_INCREMENT = 'postfix_increment', 61 | } 62 | 63 | export enum SyntaxType { 64 | EMPTY = 'empty', 65 | PATH = 'path', 66 | PATH_OPTIONS = 'path_options', 67 | SELECTOR = 'selector', 68 | LAMBDA_ARG = 'lambda_arg', 69 | INCREMENT = 'increment', 70 | LITERAL = 'literal', 71 | LOGICAL_COALESCE_EXPR = 'logical_coalesce_expr', 72 | LOGICAL_OR_EXPR = 'logical_or_expr', 73 | LOGICAL_AND_EXPR = 'logical_and_expr', 74 | COMPARISON_EXPR = 'comparison_expr', 75 | IN_EXPR = 'in_expr', 76 | MATH_EXPR = 'math_expr', 77 | UNARY_EXPR = 'unary_expr', 78 | SPREAD_EXPR = 'spread_expr', 79 | CONDITIONAL_EXPR = 'conditional_expr', 80 | ARRAY_INDEX_FILTER_EXPR = 'array_index_filter_expr', 81 | ALL_FILTER_EXPR = 'all_filter_expr', 82 | OBJECT_INDEX_FILTER_EXPR = 'object_index_filter_expr', 83 | RANGE_FILTER_EXPR = 'range_filter_expr', 84 | OBJECT_FILTER_EXPR = 'object_filter_expr', 85 | ARRAY_FILTER_EXPR = 'array_filter_expr', 86 | DEFINITION_EXPR = 'definition_expr', 87 | ASSIGNMENT_EXPR = 'assignment_expr', 88 | OBJECT_PROP_EXPR = 'object_prop_expr', 89 | OBJECT_EXPR = 'object_expr', 90 | ARRAY_EXPR = 'array_expr', 91 | BLOCK_EXPR = 'block_expr', 92 | FUNCTION_EXPR = 'function_expr', 93 | FUNCTION_CALL_EXPR = 'function_call_expr', 94 | RETURN_EXPR = 'return_expr', 95 | THROW_EXPR = 'throw_expr', 96 | STATEMENTS_EXPR = 'statements_expr', 97 | LOOP_CONTROL_EXPR = 'loop_control_expr', 98 | LOOP_EXPR = 'loop_expr', 99 | TEMPLATE_EXPR = 'TEMPLATE_EXPR', 100 | } 101 | 102 | export enum PathType { 103 | SIMPLE = 'simple', 104 | RICH = 'rich', 105 | JSON = 'json', 106 | UNKNOWN = 'unknown', 107 | } 108 | 109 | export interface EngineOptions { 110 | compileTimeBindings?: Record; 111 | defaultPathType?: PathType; 112 | mappings?: boolean; 113 | } 114 | 115 | export type Token = { 116 | type: TokenType; 117 | value: any; 118 | range: [number, number]; 119 | }; 120 | 121 | export interface PathOptions { 122 | item?: string; 123 | index?: string; 124 | toArray?: boolean; 125 | } 126 | 127 | export interface Expression { 128 | type: SyntaxType; 129 | options?: PathOptions; 130 | [key: string]: any; 131 | } 132 | 133 | export interface PathOptionsExpression extends Expression { 134 | options: PathOptions; 135 | } 136 | 137 | export interface LambdaArgExpression extends Expression { 138 | index: number; 139 | } 140 | 141 | export interface FunctionExpression extends Expression { 142 | params?: string[]; 143 | body: StatementsExpression; 144 | block?: boolean; 145 | async?: boolean; 146 | lambda?: boolean; 147 | } 148 | 149 | export interface BlockExpression extends Expression { 150 | statements: Expression[]; 151 | } 152 | 153 | export interface ObjectPropExpression extends Expression { 154 | key?: Expression | string; 155 | value: Expression; 156 | contextVar?: string; 157 | } 158 | 159 | export interface ObjectExpression extends Expression { 160 | props: ObjectPropExpression[]; 161 | } 162 | 163 | export interface ArrayExpression extends Expression { 164 | elements: Expression[]; 165 | } 166 | 167 | export interface StatementsExpression extends Expression { 168 | statements: Expression[]; 169 | } 170 | 171 | export interface UnaryExpression extends Expression { 172 | arg: Expression; 173 | op: string; 174 | } 175 | 176 | export interface BinaryExpression extends Expression { 177 | args: [Expression, Expression]; 178 | op: string; 179 | } 180 | 181 | export interface TemplateExpression extends Expression { 182 | parts: Expression[]; 183 | } 184 | 185 | export interface AssignmentExpression extends Expression { 186 | path: PathExpression; 187 | value: Expression; 188 | op: string; 189 | } 190 | 191 | export interface DefinitionExpression extends Expression { 192 | vars: string[]; 193 | fromObject?: boolean; 194 | value: Expression; 195 | definition: string; 196 | } 197 | 198 | export interface RangeFilterExpression extends Expression { 199 | fromIdx?: Expression; 200 | toIdx?: Expression; 201 | } 202 | 203 | export interface IndexFilterExpression extends Expression { 204 | indexes: ArrayExpression; 205 | exclude?: boolean; 206 | } 207 | 208 | export interface AllFilterExpression extends Expression {} 209 | 210 | export interface ObjectFilterExpression extends Expression { 211 | filter: Expression; 212 | } 213 | 214 | export interface ArrayFilterExpression extends Expression { 215 | filter: RangeFilterExpression | IndexFilterExpression; 216 | } 217 | 218 | export type Literal = string | number | boolean | null | undefined; 219 | export interface LiteralExpression extends Expression { 220 | value: Literal; 221 | tokenType: TokenType; 222 | } 223 | 224 | export interface PathExpression extends Expression { 225 | parts: Expression[]; 226 | root?: Expression | string; 227 | returnAsArray?: boolean; 228 | pathType: PathType; 229 | inferredPathType: PathType; 230 | } 231 | 232 | export interface IncrementExpression extends Expression { 233 | id: string; 234 | op: string; 235 | postfix?: boolean; 236 | } 237 | 238 | export interface SelectorExpression extends Expression { 239 | selector: string; 240 | prop?: Omit; 241 | } 242 | export interface SpreadExpression extends Expression { 243 | value: Expression; 244 | } 245 | 246 | export interface FunctionCallExpression extends Expression { 247 | args: Expression[]; 248 | object?: Expression; 249 | id?: string; 250 | parent?: string; 251 | } 252 | 253 | export interface ConditionalExpression extends Expression { 254 | if: Expression; 255 | then: Expression; 256 | else?: Expression; 257 | } 258 | 259 | export type BlockExpressionOptions = { 260 | blockEnd?: string; 261 | parentType?: SyntaxType; 262 | }; 263 | 264 | export interface ReturnExpression extends Expression { 265 | value?: Expression; 266 | } 267 | 268 | export interface LoopControlExpression extends Expression { 269 | control: string; 270 | } 271 | export interface LoopExpression extends Expression { 272 | init?: Expression; 273 | test?: Expression; 274 | update?: Expression; 275 | body: StatementsExpression; 276 | } 277 | 278 | export interface ThrowExpression extends Expression { 279 | value: Expression; 280 | } 281 | 282 | export type FlatMappingPaths = { 283 | description?: string; 284 | from?: string; 285 | to?: string; 286 | input?: string; 287 | output?: string; 288 | [key: string]: any; 289 | }; 290 | 291 | export type FlatMappingAST = FlatMappingPaths & { 292 | inputExpr: Expression; 293 | outputExpr: PathExpression; 294 | }; 295 | 296 | export type TemplateInput = string | Expression | FlatMappingPaths[] | undefined; 297 | -------------------------------------------------------------------------------- /src/utils/common.test.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_EXPR } from '../constants'; 2 | import { SyntaxType } from '../types'; 3 | import * as CommonUtils from './common'; 4 | 5 | describe('Common Utils tests', () => { 6 | describe('toArray', () => { 7 | it('should return array for non array', () => { 8 | expect(CommonUtils.toArray(1)).toEqual([1]); 9 | }); 10 | it('should return array for array', () => { 11 | expect(CommonUtils.toArray([1])).toEqual([1]); 12 | }); 13 | it('should return array for undefined', () => { 14 | expect(CommonUtils.toArray(undefined)).toBeUndefined(); 15 | }); 16 | }); 17 | describe('getLastElement', () => { 18 | it('should return last element of non empty array', () => { 19 | expect(CommonUtils.getLastElement([1, 2])).toEqual(2); 20 | }); 21 | it('should return undefined for empty array', () => { 22 | expect(CommonUtils.getLastElement([])).toBeUndefined(); 23 | }); 24 | }); 25 | describe('convertToStatementsExpr', () => { 26 | it('should return statement expression for no expressions', () => { 27 | expect(CommonUtils.convertToStatementsExpr()).toEqual({ 28 | type: SyntaxType.STATEMENTS_EXPR, 29 | statements: [], 30 | }); 31 | }); 32 | it('should return statement expression for single expression', () => { 33 | expect(CommonUtils.convertToStatementsExpr(EMPTY_EXPR)).toEqual({ 34 | type: SyntaxType.STATEMENTS_EXPR, 35 | statements: [EMPTY_EXPR], 36 | }); 37 | }); 38 | it('should return statement expression for multiple expression', () => { 39 | expect(CommonUtils.convertToStatementsExpr(EMPTY_EXPR, EMPTY_EXPR)).toEqual({ 40 | type: SyntaxType.STATEMENTS_EXPR, 41 | statements: [EMPTY_EXPR, EMPTY_EXPR], 42 | }); 43 | }); 44 | }); 45 | 46 | describe('escapeStr', () => { 47 | it('should return emtpy string for non string input', () => { 48 | expect(CommonUtils.escapeStr(undefined)).toEqual(''); 49 | }); 50 | it('should return escaped string for simple string input', () => { 51 | expect(CommonUtils.escapeStr('aabc')).toEqual(`"aabc"`); 52 | }); 53 | 54 | it('should return escaped string for string with escape characters', () => { 55 | expect(CommonUtils.escapeStr(`a\nb'"c`)).toEqual(`"a\nb'\\"c"`); 56 | }); 57 | }); 58 | describe('CreateAsyncFunction', () => { 59 | it('should return async function from code without args', async () => { 60 | expect(await CommonUtils.CreateAsyncFunction('return 1')()).toEqual(1); 61 | }); 62 | it('should return async function from code with args', async () => { 63 | expect(await CommonUtils.CreateAsyncFunction('input', 'return input')(1)).toEqual(1); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Expression, 3 | type StatementsExpression, 4 | SyntaxType, 5 | BlockExpression, 6 | FlatMappingPaths, 7 | } from '../types'; 8 | 9 | export function toArray(val: T | T[] | undefined): T[] | undefined { 10 | if (val === undefined || val === null) { 11 | return undefined; 12 | } 13 | return Array.isArray(val) ? val : [val]; 14 | } 15 | 16 | export function getLastElement(arr: T[]): T | undefined { 17 | if (!arr.length) { 18 | return undefined; 19 | } 20 | return arr[arr.length - 1]; 21 | } 22 | 23 | export function createBlockExpression(expr: Expression): BlockExpression { 24 | return { 25 | type: SyntaxType.BLOCK_EXPR, 26 | statements: [expr], 27 | }; 28 | } 29 | 30 | export function convertToStatementsExpr(...expressions: Expression[]): StatementsExpression { 31 | return { 32 | type: SyntaxType.STATEMENTS_EXPR, 33 | statements: expressions, 34 | }; 35 | } 36 | 37 | export function CreateAsyncFunction(...args) { 38 | // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names 39 | return async function () {}.constructor(...args); 40 | } 41 | 42 | export function isExpression(val: string | Expression | FlatMappingPaths[]): boolean { 43 | return ( 44 | typeof val === 'object' && !Array.isArray(val) && Object.values(SyntaxType).includes(val.type) 45 | ); 46 | } 47 | 48 | export function escapeStr(s?: string): string { 49 | if (typeof s !== 'string') { 50 | return ''; 51 | } 52 | return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/converter.test.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxType } from '../types'; 2 | import { convertToObjectMapping } from './converter'; 3 | 4 | describe('Converter:', () => { 5 | describe('convertToObjectMapping', () => { 6 | it('should validate mappings', () => { 7 | expect(() => 8 | convertToObjectMapping([ 9 | { inputExpr: { type: SyntaxType.EMPTY }, outputExpr: { type: SyntaxType.EMPTY } }, 10 | ] as any), 11 | ).toThrowError(/Invalid mapping/); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './converter'; 3 | -------------------------------------------------------------------------------- /src/utils/translator.ts: -------------------------------------------------------------------------------- 1 | import { TokenType, Literal } from '../types'; 2 | import { escapeStr } from './common'; 3 | 4 | export function translateLiteral(type: TokenType, val: Literal): string { 5 | if (type === TokenType.STR) { 6 | return escapeStr(String(val)); 7 | } 8 | 9 | return String(val); 10 | } 11 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /stryker.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", 3 | "_comment": "This config was generated using 'stryker init'. Please take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information.", 4 | "packageManager": "npm", 5 | "reporters": ["html", "clear-text", "progress"], 6 | "testRunner": "command", 7 | "testRunner_comment": "Take a look at (missing 'homepage' URL in package.json) for information about the command plugin.", 8 | "coverageAnalysis": "all", 9 | "buildCommand": "npm run build", 10 | "mutate": [ 11 | "{src,lib}/**/!(*.+(s|S)pec|*.+(t|T)est).+(cjs|mjs|js|ts|jsx|tsx|html|vue)", 12 | "!{src,lib}/**/__tests__/**/*.+(cjs|mjs|js|ts|jsx|tsx|html|vue)" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob'; 2 | import path, { join } from 'path'; 3 | import { Command } from 'commander'; 4 | import * as ScenarioUtils from './utils'; 5 | import { Scenario } from './types'; 6 | 7 | const rootDirName = 'scenarios'; 8 | const command = new Command(); 9 | command.allowUnknownOption().option('--scenarios ', 'Enter Scenario Names', 'all').parse(); 10 | 11 | const opts = command.opts(); 12 | let scenarios = opts.scenarios.split(/[, ]/); 13 | 14 | if (scenarios[0] === 'all') { 15 | scenarios = glob.sync(join(__dirname, rootDirName, '**/data.ts')); 16 | } 17 | 18 | describe('Scenarios tests', () => { 19 | scenarios.forEach((scenarioFileName) => { 20 | const scenarioDir = path.dirname(scenarioFileName); 21 | const scenarioName = path.basename(scenarioDir); 22 | describe(`${scenarioName}`, () => { 23 | const scenarios = ScenarioUtils.extractScenarios(scenarioDir); 24 | scenarios.forEach((scenario, index) => { 25 | it(`Scenario ${index}: ${Scenario.getTemplatePath(scenario)}`, async () => { 26 | try { 27 | const result = await ScenarioUtils.evaluateScenario(scenarioDir, scenario); 28 | expect(result).toEqual(scenario.output); 29 | } catch (error: any) { 30 | expect(error.message).toContain(scenario.error); 31 | } 32 | }); 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/scenario.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { Command } from 'commander'; 3 | import { Scenario } from './types'; 4 | import * as ScenarioUtils from './utils'; 5 | 6 | // Run: npm run test:scenario -- --scenario=arrays --index=1 7 | const command = new Command(); 8 | command 9 | .allowUnknownOption() 10 | .option('-s, --scenario ', 'Enter Scenario Name') 11 | .option('-i, --index ', 'Enter Test case index') 12 | .parse(); 13 | 14 | const opts = command.opts(); 15 | const scenarioName = opts.scenario || 'arrays'; 16 | const index = +(opts.index || 0); 17 | 18 | describe(`${scenarioName}:`, () => { 19 | const scenarioDir = join(__dirname, 'scenarios', scenarioName); 20 | const scenarios = ScenarioUtils.extractScenarios(scenarioDir); 21 | const scenario: Scenario = scenarios[index] || scenarios[0]; 22 | const templatePath = Scenario.getTemplatePath(scenario); 23 | it(`Scenario ${index}: ${templatePath}`, async () => { 24 | let result; 25 | try { 26 | console.log(`Executing scenario: ${scenarioName}, test: ${index}, template: ${templatePath}`); 27 | result = await ScenarioUtils.evaluateScenario(scenarioDir, scenario); 28 | expect(result).toEqual(scenario.output); 29 | } catch (error: any) { 30 | console.error(error); 31 | console.log('Actual result', JSON.stringify(result, null, 2)); 32 | console.log('Expected result', JSON.stringify(scenario.output, null, 2)); 33 | expect(error.message).toContain(scenario.error); 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/scenarios/arrays/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | output: [1, 2, 3, ['string1', 20.02], ['string2', 'string3', 'aa"a', true, false], 2], 6 | }, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/scenarios/arrays/template.jt: -------------------------------------------------------------------------------- 1 | let a = [ 2 | "string1", `string2`, 'string3', "aa\"a", true, false, 3 | undefined, null, 20.02, .22, [1., 2, 3], {"b": [1, 2]} 4 | ]; 5 | [...~r a[-2], a[0,8], a[1:6],~r a[-1].b[1]] 6 | 7 | -------------------------------------------------------------------------------- /test/scenarios/assignments/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | bindings: { 6 | context: {}, 7 | }, 8 | output: { 9 | a: { 10 | b: [11, 13], 11 | 'c key': 4, 12 | }, 13 | }, 14 | }, 15 | ]; 16 | -------------------------------------------------------------------------------- /test/scenarios/assignments/template.jt: -------------------------------------------------------------------------------- 1 | let a = 3; 2 | a*=3; 3 | --a; 4 | ++a; 5 | let b = -a + 30; 6 | b-=1 7 | b/=2 8 | b+=1; 9 | b--; 10 | b++; 11 | let cKey = "c key"; 12 | let c = { a: { b: [a, b], [cKey]: 2 } } 13 | let {d, e, f} = {d: 2, e: 2, f: 1} 14 | // updating binding value 15 | $.context.f = 1 16 | c.a.b[0] = c.a.b[0] + d 17 | c.a.b[1] = c.a.b[1] + e 18 | c.a."c key" = c.a."c key" + f 19 | c.a[cKey] = c.a[cKey] + ~s $.context.f 20 | c 21 | -------------------------------------------------------------------------------- /test/scenarios/bad_templates/bad_array_coalese_expr.jt: -------------------------------------------------------------------------------- 1 | ??? [] -------------------------------------------------------------------------------- /test/scenarios/bad_templates/bad_async_usage.jt: -------------------------------------------------------------------------------- 1 | async abc -------------------------------------------------------------------------------- /test/scenarios/bad_templates/bad_context_var.jt: -------------------------------------------------------------------------------- 1 | .a@1#2 -------------------------------------------------------------------------------- /test/scenarios/bad_templates/bad_function_params.jt: -------------------------------------------------------------------------------- 1 | function(1, 2, 3){} -------------------------------------------------------------------------------- /test/scenarios/bad_templates/bad_function_rest_param.jt: -------------------------------------------------------------------------------- 1 | function(a, ...b, c){} -------------------------------------------------------------------------------- /test/scenarios/bad_templates/bad_number.jt: -------------------------------------------------------------------------------- 1 | 2.2.3 -------------------------------------------------------------------------------- /test/scenarios/bad_templates/bad_regex.jt: -------------------------------------------------------------------------------- 1 | /?/ -------------------------------------------------------------------------------- /test/scenarios/bad_templates/bad_string.jt: -------------------------------------------------------------------------------- 1 | "aaaa -------------------------------------------------------------------------------- /test/scenarios/bad_templates/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'bad_array_coalese_expr.jt', 6 | error: 'expected at least 1 expression', 7 | }, 8 | { 9 | templatePath: 'bad_async_usage.jt', 10 | error: 'Unexpected token', 11 | }, 12 | { 13 | templatePath: 'bad_context_var.jt', 14 | error: 'Unexpected token', 15 | }, 16 | { 17 | templatePath: 'bad_function_params.jt', 18 | error: 'Unexpected token', 19 | }, 20 | { 21 | templatePath: 'bad_function_rest_param.jt', 22 | error: 'Unexpected token', 23 | }, 24 | { 25 | templatePath: 'bad_number.jt', 26 | error: 'Unexpected token', 27 | }, 28 | { 29 | templatePath: 'bad_regex.jt', 30 | error: 'invalid regular expression', 31 | }, 32 | { 33 | templatePath: 'bad_string.jt', 34 | error: 'Unexpected end of template', 35 | }, 36 | { 37 | templatePath: 'empty_block.jt', 38 | error: 'empty block is not allowed', 39 | }, 40 | { 41 | templatePath: 'empty_object_vars_for_definition.jt', 42 | error: 'Empty object vars', 43 | }, 44 | { 45 | templatePath: 'incomplete_statement.jt', 46 | error: 'Unexpected end of template', 47 | }, 48 | { 49 | templatePath: 'invalid_new_function_call.jt', 50 | error: 'Unexpected token', 51 | }, 52 | { 53 | templatePath: 'invalid_object_vars_for_definition.jt', 54 | error: 'Invalid object vars', 55 | }, 56 | { 57 | templatePath: 'invalid_variable_assignment1.jt', 58 | error: 'Invalid assignment path', 59 | }, 60 | { 61 | templatePath: 'invalid_variable_assignment2.jt', 62 | error: 'Invalid assignment path', 63 | }, 64 | { 65 | templatePath: 'invalid_variable_assignment3.jt', 66 | error: 'Invalid assignment path', 67 | }, 68 | { 69 | templatePath: 'invalid_variable_assignment4.jt', 70 | error: 'Invalid assignment path', 71 | }, 72 | { 73 | templatePath: 'invalid_variable_assignment5.jt', 74 | error: 'Invalid assignment path', 75 | }, 76 | { 77 | templatePath: 'invalid_variable_assignment6.jt', 78 | error: 'Invalid assignment path', 79 | }, 80 | { 81 | templatePath: 'invalid_variable_assignment7.jt', 82 | error: 'Invalid assignment path', 83 | }, 84 | { 85 | templatePath: 'invalid_variable_assignment8.jt', 86 | error: 'Invalid assignment path', 87 | }, 88 | { 89 | templatePath: 'invalid_variable_assignment9.jt', 90 | error: 'Invalid assignment path', 91 | }, 92 | { 93 | templatePath: 'invalid_variable_definition.jt', 94 | error: 'Invalid normal vars', 95 | }, 96 | { 97 | templatePath: 'invalid_token_after_function_def.jt', 98 | error: 'Unexpected token', 99 | }, 100 | { 101 | templatePath: 'object_with_invalid_closing.jt', 102 | error: 'Unexpected token', 103 | }, 104 | { 105 | templatePath: 'object_with_invalid_key.jt', 106 | error: 'Unexpected token', 107 | }, 108 | { 109 | templatePath: 'reserved_id.jt', 110 | error: 'Reserved ID pattern', 111 | }, 112 | { 113 | templatePath: 'unknown_token.jt', 114 | error: 'Unknown token', 115 | }, 116 | { 117 | templatePath: 'unsupported_assignment.jt', 118 | error: 'Unexpected token', 119 | }, 120 | ]; 121 | -------------------------------------------------------------------------------- /test/scenarios/bad_templates/empty_block.jt: -------------------------------------------------------------------------------- 1 | () -------------------------------------------------------------------------------- /test/scenarios/bad_templates/empty_object_vars_for_definition.jt: -------------------------------------------------------------------------------- 1 | let {} = {a: 1} -------------------------------------------------------------------------------- /test/scenarios/bad_templates/incomplete_statement.jt: -------------------------------------------------------------------------------- 1 | 2 ** -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_new_function_call.jt: -------------------------------------------------------------------------------- 1 | new .a() -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_object_vars_for_definition.jt: -------------------------------------------------------------------------------- 1 | let {"a"} = {a: 1} -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_token_after_function_def.jt: -------------------------------------------------------------------------------- 1 | function(){}[] -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment1.jt: -------------------------------------------------------------------------------- 1 | let a = [{a: 1, b: 2}]; 2 | a{.a===1}.b = 3; -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment2.jt: -------------------------------------------------------------------------------- 1 | let a = [{a: 1, b: 2}]; 2 | a[1, 2].b = 3; -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment3.jt: -------------------------------------------------------------------------------- 1 | let a = [{a: 1, b: 2}]; 2 | a..b = 3; -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment4.jt: -------------------------------------------------------------------------------- 1 | let a = [{a: [1,2,3,4], b: 2}]; 2 | a[1:3].b = 3; -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment5.jt: -------------------------------------------------------------------------------- 1 | [].length = 1 -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment6.jt: -------------------------------------------------------------------------------- 1 | .length = 1 -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment7.jt: -------------------------------------------------------------------------------- 1 | .a.().b = 1 -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment8.jt: -------------------------------------------------------------------------------- 1 | ^.a.b = 1 -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_assignment9.jt: -------------------------------------------------------------------------------- 1 | $.a.b = 1 -------------------------------------------------------------------------------- /test/scenarios/bad_templates/invalid_variable_definition.jt: -------------------------------------------------------------------------------- 1 | let 1 = 2; -------------------------------------------------------------------------------- /test/scenarios/bad_templates/object_with_invalid_closing.jt: -------------------------------------------------------------------------------- 1 | { "aa": 1] -------------------------------------------------------------------------------- /test/scenarios/bad_templates/object_with_invalid_key.jt: -------------------------------------------------------------------------------- 1 | {/1/: 2} -------------------------------------------------------------------------------- /test/scenarios/bad_templates/reserved_id.jt: -------------------------------------------------------------------------------- 1 | let ___a = 1; -------------------------------------------------------------------------------- /test/scenarios/bad_templates/unknown_token.jt: -------------------------------------------------------------------------------- 1 | \ -------------------------------------------------------------------------------- /test/scenarios/bad_templates/unsupported_assignment.jt: -------------------------------------------------------------------------------- 1 | let a = 1 2 | var b = 1 3 | -------------------------------------------------------------------------------- /test/scenarios/base/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | template: '', 6 | output: undefined, 7 | }, 8 | ]; 9 | -------------------------------------------------------------------------------- /test/scenarios/bindings/async.jt: -------------------------------------------------------------------------------- 1 | const data = await Promise.all(.map(async lambda await $.square(?0))) 2 | Promise.all(data.map(async function(a){ 3 | await $.sqrt(a) 4 | })) 5 | -------------------------------------------------------------------------------- /test/scenarios/bindings/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | class CustomError extends Error {} 4 | 5 | export const data: Scenario[] = [ 6 | { 7 | templatePath: 'async.jt', 8 | bindings: { 9 | square: (a) => new Promise((resolve) => setTimeout(() => resolve(a * a), 5)), 10 | sqrt: (a) => new Promise((resolve) => setTimeout(() => resolve(Math.sqrt(a)), 5)), 11 | }, 12 | input: [1, 2, 3], 13 | output: [1, 2, 3], 14 | }, 15 | { 16 | templatePath: 'new_operator.jt', 17 | bindings: { CustomError }, 18 | output: new CustomError('some error'), 19 | }, 20 | { 21 | bindings: { 22 | a: 10, 23 | b: 2, 24 | c: (a, b) => a * b, 25 | }, 26 | output: 20, 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /test/scenarios/bindings/new_operator.jt: -------------------------------------------------------------------------------- 1 | new $.CustomError("some error") -------------------------------------------------------------------------------- /test/scenarios/bindings/template.jt: -------------------------------------------------------------------------------- 1 | $.c($.a, $.b); 2 | -------------------------------------------------------------------------------- /test/scenarios/block/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | output: 15, 6 | }, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/scenarios/block/template.jt: -------------------------------------------------------------------------------- 1 | let c = ( 2 | let a = 1 3 | let b = 2 4 | a + b 5 | ) 6 | /* 7 | redefining a and b is possible because 8 | we declared them in block previously 9 | */ 10 | let a = 2 11 | let b = 3 12 | (a + b) * c -------------------------------------------------------------------------------- /test/scenarios/comments/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | output: ['////', '/*** /// */'], 6 | }, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/scenarios/comments/template.jt: -------------------------------------------------------------------------------- 1 | // line comment 2 | /** 3 | //////////////// 4 | * block comment 5 | */ 6 | ["////", "/*** /// */"] -------------------------------------------------------------------------------- /test/scenarios/comparisons/anyof.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: [1, 2] anyof [2, 3], 3 | false: [1, 2] anyof [3, 4] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/contains.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: ["aBc" ==* "aB", "abc" contains "c", ["a", "b", "c"] contains "c"], 3 | false: ["aBc" ==* "ab", "abc" contains "d", ["a", "b", "c"] contains "d"] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'anyof.jt', 6 | output: { 7 | true: true, 8 | false: false, 9 | }, 10 | }, 11 | { 12 | templatePath: 'contains.jt', 13 | output: { 14 | true: [true, true, true], 15 | false: [false, false, false], 16 | }, 17 | }, 18 | { 19 | templatePath: 'empty.jt', 20 | output: { 21 | true: [true, true], 22 | false: [false, false], 23 | }, 24 | }, 25 | { 26 | templatePath: 'string_contains_ignore_case.jt', 27 | output: { 28 | true: true, 29 | false: false, 30 | }, 31 | }, 32 | { 33 | templatePath: 'ends_with.jt', 34 | output: { 35 | true: [true, true], 36 | false: [false, false], 37 | }, 38 | }, 39 | { 40 | templatePath: 'ends_with_ignore_case.jt', 41 | output: { 42 | true: [true, true], 43 | false: [false, false], 44 | }, 45 | }, 46 | { 47 | templatePath: 'eq.jt', 48 | output: { 49 | true: true, 50 | false: false, 51 | }, 52 | }, 53 | { 54 | templatePath: 'ge.jt', 55 | output: { 56 | true: true, 57 | false: false, 58 | }, 59 | }, 60 | { 61 | templatePath: 'gte.jt', 62 | output: { 63 | true: true, 64 | false: false, 65 | }, 66 | }, 67 | { 68 | templatePath: 'in.jt', 69 | output: { 70 | true: [true, true], 71 | false: [false, false], 72 | }, 73 | }, 74 | { 75 | templatePath: 'le.jt', 76 | output: { 77 | true: true, 78 | false: false, 79 | }, 80 | }, 81 | { 82 | templatePath: 'lte.jt', 83 | output: { 84 | true: true, 85 | false: false, 86 | }, 87 | }, 88 | { 89 | templatePath: 'ne.jt', 90 | output: { 91 | true: true, 92 | false: false, 93 | }, 94 | }, 95 | { 96 | templatePath: 'noneof.jt', 97 | output: { 98 | true: true, 99 | false: false, 100 | }, 101 | }, 102 | { 103 | templatePath: 'not_in.jt', 104 | output: { 105 | true: [true, true], 106 | false: [false, false], 107 | }, 108 | }, 109 | { 110 | templatePath: 'regex.jt', 111 | output: { 112 | true: [true, true], 113 | false: [false, false], 114 | }, 115 | }, 116 | { 117 | templatePath: 'size.jt', 118 | output: { 119 | true: [true, true], 120 | false: [false, false], 121 | }, 122 | }, 123 | { 124 | templatePath: 'starts_with.jt', 125 | output: { 126 | true: [true, true], 127 | false: [false, false], 128 | }, 129 | }, 130 | { 131 | templatePath: 'starts_with_ignore_case.jt', 132 | output: { 133 | true: [true, true], 134 | false: [false, false], 135 | }, 136 | }, 137 | { 138 | templatePath: 'string_eq.jt', 139 | output: { 140 | true: true, 141 | false: false, 142 | }, 143 | }, 144 | { 145 | templatePath: 'string_ne.jt', 146 | output: { 147 | true: true, 148 | false: false, 149 | }, 150 | }, 151 | { 152 | templatePath: 'string_eq_ingore_case.jt', 153 | output: { 154 | true: true, 155 | false: false, 156 | }, 157 | }, 158 | { 159 | templatePath: 'string_ne.jt', 160 | output: { 161 | true: true, 162 | false: false, 163 | }, 164 | }, 165 | { 166 | templatePath: 'string_ne_ingore_case.jt', 167 | output: { 168 | true: true, 169 | false: false, 170 | }, 171 | }, 172 | { 173 | templatePath: 'subsetof.jt', 174 | output: { 175 | true: [true, true], 176 | false: [false, false], 177 | }, 178 | }, 179 | ]; 180 | -------------------------------------------------------------------------------- /test/scenarios/comparisons/empty.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: ["" empty true, [] empty true], 3 | false: ["a" empty true, ["a"] empty true] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/ends_with.jt: -------------------------------------------------------------------------------- 1 | { 2 | true:["EndsWith" $== "With", "With" ==$ "EndsWith"], 3 | false: ["EndsWith" $== "NotWith", "NotWith" ==$ "EndsWith"] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/ends_with_ignore_case.jt: -------------------------------------------------------------------------------- 1 | { 2 | true:["EndsWith" $= "with", "with" =$ "EndsWith"], 3 | false: ["EndsWith" $= "NotWith", "NotWith" =$ "EndsWith"] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/eq.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: 10 == 10, 3 | false: 10 == 2 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/ge.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: 10 > 2, 3 | false: 2 > 10 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/gte.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: 10 >= 10, 3 | false: 2 >= 10 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/in.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: ["a" in ["a", "b"], "a" in {"a": 1, "b": 2}], 3 | false: ["c" in ["a", "b"], "c" in {"a": 1, "b": 2}] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/le.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: 2 < 10, 3 | false: 10 < 2 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/lte.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: 10 <= 10, 3 | false: 10 <= 2 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/ne.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: 10 != 2, 3 | false: 10 != 10 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/noneof.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: [1, 2] noneof [3, 4], 3 | false: [1, 2] noneof [2, 3], 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/not_in.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: ["c" nin ["a", "b"], "c" nin {"a": 1, "b": 2}], 3 | false: ["a" nin ["a", "b"], "a" nin {"a": 1, "b": 2}] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/regex.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: ['abc' =~ /a.*c/, 'aBC' =~ /a.*c/i], 3 | false: ['abC' =~ /a.*c/, 'aBd' =~ /a.*c/i] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/size.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: [["a", "b"] size 2, "ab" size 2], 3 | false: [[] size 1, "" size 1] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/starts_with.jt: -------------------------------------------------------------------------------- 1 | { 2 | true:["StartsWith" ^== "Starts", "Starts" ==^ "StartsWith"], 3 | false: ["StartsWith" ^= "NotStarts", "NotStarts" =^ "StartsWith"] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/starts_with_ignore_case.jt: -------------------------------------------------------------------------------- 1 | { 2 | true:["StartsWith" ^= "starts", "starts" =^ "StartsWith"], 3 | false: ["StartsWith" ^= "NotStarts", "NotStarts" =^ "StartsWith"] 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/string_contains_ignore_case.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: "aBc" =* "aB", 3 | false: "ac" =* "aB" 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/string_eq.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: "aBc" === "aBc", 3 | false: "abc" === "aBc" 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/string_eq_ingore_case.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: "abc" == "aBc", 3 | false: "adc" == "aBc" 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/string_ne.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: "abc" !== "aBc", 3 | false: "aBc" !== "aBc" 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/string_ne_ingore_case.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: "adc" != "aBc", 3 | false: "abc" != "aBc" 4 | } -------------------------------------------------------------------------------- /test/scenarios/comparisons/subsetof.jt: -------------------------------------------------------------------------------- 1 | { 2 | true: [[1, 2] subsetof [1, 2, 3], [] subsetof [1]], 3 | false: [[1, 2] subsetof [1], [1] subsetof []], 4 | } -------------------------------------------------------------------------------- /test/scenarios/compile_time_expressions/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | options: { compileTimeBindings: { a: 1, b: 'string', c: { c: 1.02 }, d: [null, true, false] } }, 6 | output: [1, 'string', { c: 1.02 }, [null, true, false]], 7 | }, 8 | { 9 | templatePath: 'two_level_path_processing.jt', 10 | options: { 11 | compileTimeBindings: { 12 | paths: ['a.non_existing', 'a.b.non_existing', 'a.c'], 13 | }, 14 | }, 15 | input: { 16 | a: { 17 | c: 9, 18 | }, 19 | }, 20 | output: 9, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /test/scenarios/compile_time_expressions/template.jt: -------------------------------------------------------------------------------- 1 | // this will be resolved at compile time 2 | {{ $.([.a, .b, .c, .d]) }}; 3 | -------------------------------------------------------------------------------- /test/scenarios/compile_time_expressions/two_level_path_processing.jt: -------------------------------------------------------------------------------- 1 | // Here we are generating a coalescing expression and recompiling it 2 | // We are generating a string inside and that needs to be recompiled so 3 | // we need to use {{ {{ }} }} 4 | {{ {{ $.paths.map(lambda '.' + ?0).join(' ?? ') }} }} -------------------------------------------------------------------------------- /test/scenarios/conditions/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'if_block.jt', 6 | input: { 7 | a: -5, 8 | }, 9 | output: 'a <= 1', 10 | }, 11 | { 12 | templatePath: 'if_block.jt', 13 | input: { 14 | a: 1, 15 | }, 16 | output: 'a <= 1', 17 | }, 18 | { 19 | templatePath: 'if_block.jt', 20 | input: { 21 | a: 2, 22 | }, 23 | output: 'a > 1', 24 | }, 25 | { 26 | templatePath: 'if_block.jt', 27 | input: { 28 | a: 3, 29 | }, 30 | output: 'a > 2', 31 | }, 32 | { 33 | templatePath: 'if_block.jt', 34 | input: { 35 | a: 10, 36 | }, 37 | output: 'a > 3', 38 | }, 39 | { 40 | templatePath: 'if_then.jt', 41 | input: { 42 | a: -5, 43 | }, 44 | output: 0, 45 | }, 46 | { 47 | templatePath: 'if_then.jt', 48 | input: { 49 | a: 5, 50 | }, 51 | output: 5, 52 | }, 53 | { 54 | templatePath: 'objects.jt', 55 | input: { 56 | a: 5, 57 | }, 58 | output: { message: 'a > 1' }, 59 | }, 60 | { 61 | templatePath: 'objects.jt', 62 | input: { 63 | a: 0, 64 | }, 65 | output: { message: 'a <= 1' }, 66 | }, 67 | { 68 | input: { 69 | a: 5, 70 | b: 10, 71 | c: 15, 72 | }, 73 | output: 15, 74 | }, 75 | { 76 | input: { 77 | a: 15, 78 | b: 5, 79 | c: 10, 80 | }, 81 | output: 15, 82 | }, 83 | { 84 | input: { 85 | a: 10, 86 | b: 15, 87 | c: 5, 88 | }, 89 | output: 15, 90 | }, 91 | { 92 | templatePath: 'undefined_arr_cond.jt', 93 | input: { 94 | products: [{ a: 1 }, { a: 2 }], 95 | }, 96 | output: 'no', 97 | }, 98 | { 99 | templatePath: 'undefined_arr_cond.jt', 100 | input: { 101 | products: [{ objectID: 1 }, { objectID: 2 }], 102 | }, 103 | output: 'yes', 104 | }, 105 | { 106 | templatePath: 'undefined_arr_cond.jt', 107 | input: { 108 | otherProperty: [{ objectID: 1 }, { objectID: 2 }], 109 | }, 110 | output: 'no', 111 | }, 112 | ]; 113 | -------------------------------------------------------------------------------- /test/scenarios/conditions/if_block.jt: -------------------------------------------------------------------------------- 1 | (.a > 1) ? { 2 | (.a > 2) ? { 3 | (.a > 3) ? { 4 | return "a > 3"; 5 | } 6 | return "a > 2"; 7 | } 8 | return "a > 1"; 9 | } 10 | "a <= 1" -------------------------------------------------------------------------------- /test/scenarios/conditions/if_then.jt: -------------------------------------------------------------------------------- 1 | let a = .a 2 | a < 0 ? a = 0 3 | a 4 | -------------------------------------------------------------------------------- /test/scenarios/conditions/objects.jt: -------------------------------------------------------------------------------- 1 | .a > 1 ? { message: "a > 1" } : { message: "a <= 1" } -------------------------------------------------------------------------------- /test/scenarios/conditions/template.jt: -------------------------------------------------------------------------------- 1 | let a = .a 2 | let b = .b 3 | let c = .c; 4 | a > b ? a > c ? a : c : b > c ? b : c -------------------------------------------------------------------------------- /test/scenarios/conditions/undefined_arr_cond.jt: -------------------------------------------------------------------------------- 1 | let a = ~r .products.objectID; 2 | Array.isArray(a) ? "yes":"no"; -------------------------------------------------------------------------------- /test/scenarios/context_variables/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | template: '.a.b@b[]', 6 | description: 'context variable in last part', 7 | input: { 8 | a: { 9 | b: [ 10 | { 11 | c: 1, 12 | }, 13 | { 14 | c: 2, 15 | }, 16 | ], 17 | }, 18 | }, 19 | output: [{ c: 1 }, { c: 2 }], 20 | }, 21 | { 22 | templatePath: 'filter.jt', 23 | input: [[{ a: 1 }], [{ a: 2 }, { a: 3 }], [{ a: 4 }, { a: 5 }, { a: 6 }]], 24 | output: [ 25 | { 26 | a: [2, 3], 27 | idx: 0, 28 | }, 29 | { 30 | a: [4, 5, 6], 31 | idx: 1, 32 | }, 33 | ], 34 | }, 35 | { 36 | templatePath: 'function.jt', 37 | input: { 38 | a: [ 39 | { 40 | b: [1, 2], 41 | }, 42 | { 43 | b: [1, 2], 44 | }, 45 | ], 46 | }, 47 | output: [1, 2, 2, 3], 48 | }, 49 | { 50 | templatePath: 'selector.jt', 51 | input: { 52 | a: 10, 53 | b: [ 54 | { 55 | c: [ 56 | { 57 | id: 1, 58 | }, 59 | { 60 | id: 2, 61 | }, 62 | ], 63 | id: 1, 64 | }, 65 | { 66 | c: [ 67 | { 68 | id: 3, 69 | }, 70 | { 71 | id: 4, 72 | }, 73 | ], 74 | id: 2, 75 | }, 76 | ], 77 | }, 78 | output: [ 79 | { 80 | cid: 1, 81 | cidx: 0, 82 | bid: 1, 83 | bidx: 0, 84 | a: 10, 85 | }, 86 | { 87 | cid: 2, 88 | cidx: 1, 89 | bid: 1, 90 | bidx: 0, 91 | a: 10, 92 | }, 93 | { 94 | cid: 3, 95 | cidx: 0, 96 | bid: 2, 97 | bidx: 1, 98 | a: 10, 99 | }, 100 | { 101 | cid: 4, 102 | cidx: 1, 103 | bid: 2, 104 | bidx: 1, 105 | a: 10, 106 | }, 107 | ], 108 | }, 109 | { 110 | input: { 111 | orders: [ 112 | { 113 | id: 1, 114 | products: [ 115 | { 116 | name: 'A', 117 | price: 10, 118 | }, 119 | { 120 | name: 'B', 121 | price: 5, 122 | }, 123 | ], 124 | }, 125 | { 126 | id: 2, 127 | products: [ 128 | { 129 | name: 'A', 130 | price: 10, 131 | }, 132 | { 133 | name: 'C', 134 | price: 15, 135 | }, 136 | ], 137 | }, 138 | ], 139 | }, 140 | output: [ 141 | { 142 | name: 'A', 143 | price: 10, 144 | orderNum: 0, 145 | orderId: 1, 146 | }, 147 | { 148 | name: 'B', 149 | price: 5, 150 | orderNum: 0, 151 | orderId: 1, 152 | }, 153 | { 154 | name: 'A', 155 | price: 10, 156 | orderNum: 1, 157 | orderId: 2, 158 | }, 159 | { 160 | name: 'C', 161 | price: 15, 162 | orderNum: 1, 163 | orderId: 2, 164 | }, 165 | ], 166 | }, 167 | ]; 168 | -------------------------------------------------------------------------------- /test/scenarios/context_variables/filter.jt: -------------------------------------------------------------------------------- 1 | /* 2 | .() breaks the path, so path before will evaluated first 3 | and then on the result rest of the path will be executed 4 | */ 5 | .{.[].length > 1}.().@item#idx.({ 6 | a: ~r item.a, 7 | idx: idx 8 | }) 9 | 10 | -------------------------------------------------------------------------------- /test/scenarios/context_variables/function.jt: -------------------------------------------------------------------------------- 1 | let add = function(a, b) { a + b } 2 | .a#i.b.(add(i, .)) -------------------------------------------------------------------------------- /test/scenarios/context_variables/selector.jt: -------------------------------------------------------------------------------- 1 | ..b@b#bi.c#ci.({ 2 | cid: .id, 3 | cidx: ci, 4 | bid: b.id, 5 | bidx: bi, 6 | a: ^.a 7 | }) -------------------------------------------------------------------------------- /test/scenarios/context_variables/template.jt: -------------------------------------------------------------------------------- 1 | .orders@order#idx.products.({ 2 | name: .name, 3 | price: .price, 4 | orderNum: idx, 5 | orderId: order.id 6 | }) -------------------------------------------------------------------------------- /test/scenarios/filters/array_filters.jt: -------------------------------------------------------------------------------- 1 | let a = [1, 2, 3, 4, 5, {"a": 1, "b": 2, "c": 3}]; 2 | [ 3 | a[2:], a[:3], a[3:5], a[...[1, 3]].[0, 1], 4 | ~r a[-2], a[1], a[:-2], a[-2:], a[-1]["a", "b"] 5 | ] -------------------------------------------------------------------------------- /test/scenarios/filters/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'array_filters.jt', 6 | output: [ 7 | [ 8 | 3, 9 | 4, 10 | 5, 11 | { 12 | a: 1, 13 | b: 2, 14 | c: 3, 15 | }, 16 | ], 17 | [1, 2, 3], 18 | [4, 5], 19 | [2, 4], 20 | 5, 21 | 2, 22 | [1, 2, 3, 4], 23 | [ 24 | 5, 25 | { 26 | a: 1, 27 | b: 2, 28 | c: 3, 29 | }, 30 | ], 31 | [1, 2], 32 | ], 33 | }, 34 | { 35 | templatePath: 'invalid_object_index_filters.jt', 36 | error: 'Unexpected token', 37 | }, 38 | { 39 | templatePath: 'object_filters.jt', 40 | output: { 41 | a: [3, 4], 42 | b: 2, 43 | }, 44 | }, 45 | { 46 | templatePath: 'object_indexes.jt', 47 | output: [ 48 | { a: 1, b: 2 }, 49 | { c: 3, d: 4 }, 50 | { a: 1, b: 2 }, 51 | ], 52 | }, 53 | ]; 54 | -------------------------------------------------------------------------------- /test/scenarios/filters/invalid_object_index_filters.jt: -------------------------------------------------------------------------------- 1 | obj{~a} -------------------------------------------------------------------------------- /test/scenarios/filters/object_filters.jt: -------------------------------------------------------------------------------- 1 | [ 2 | {a: [1, 2], b: "1"}, {a: [3, 4], b: "22"}, 3 | {a: [5, 3], b: 3}, {a:[3], b: 4}, {b: 5}, 4 | {a: [3, 4], b: 2} 5 | ] 6 | {.a[].length > 1} 7 | {3 in .a}.{.a[].includes(4)} 8 | {!(typeof .b === "string")} 9 | 10 | -------------------------------------------------------------------------------- /test/scenarios/filters/object_indexes.jt: -------------------------------------------------------------------------------- 1 | let obj = { 2 | a: 1, 3 | b: 2, 4 | c: 3, 5 | d: 4 6 | }; 7 | [obj{["a", "b"]}, obj{!["a", "b"]}, obj{~["c", "d"]}] -------------------------------------------------------------------------------- /test/scenarios/functions/array_functions.jt: -------------------------------------------------------------------------------- 1 | { 2 | map: .map(lambda ?0 * 2), 3 | filter: .filter(lambda ?0 % 2 == 0) 4 | } -------------------------------------------------------------------------------- /test/scenarios/functions/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'array_functions.jt', 6 | input: [1, 2, 3, 4], 7 | output: { 8 | map: [2, 4, 6, 8], 9 | filter: [2, 4], 10 | }, 11 | }, 12 | { 13 | templatePath: 'function_calls.jt', 14 | output: ['abc', null, undefined], 15 | }, 16 | { 17 | templatePath: 'js_date_function.jt', 18 | output: ['2022', 8, 19], 19 | }, 20 | { 21 | templatePath: 'new_operator.jt', 22 | output: [ 23 | { 24 | name: 'foo', 25 | grade: 1, 26 | }, 27 | { 28 | name: 'bar', 29 | grade: 2, 30 | }, 31 | ], 32 | }, 33 | { 34 | templatePath: 'parent_scope_vars.jt', 35 | output: 90, 36 | }, 37 | { 38 | templatePath: 'promise.jt', 39 | input: [1, 2], 40 | output: [1, 2], 41 | }, 42 | { 43 | templatePath: 'promise.jt', 44 | input: { a: 1 }, 45 | output: { a: 1 }, 46 | }, 47 | { 48 | output: 80, 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /test/scenarios/functions/function_calls.jt: -------------------------------------------------------------------------------- 1 | let a = {a: "Abc", b: null, c: undefined}; 2 | a.([.a.toLowerCase(), .b.toLowerCase(), .c.toLowerCase()]) -------------------------------------------------------------------------------- /test/scenarios/functions/js_date_function.jt: -------------------------------------------------------------------------------- 1 | const date = new Date('2022-08-19'); 2 | [date.getFullYear().toString(), date.getMonth() + 1, date.getDate()]; 3 | -------------------------------------------------------------------------------- /test/scenarios/functions/new_operator.jt: -------------------------------------------------------------------------------- 1 | const Person = { 2 | Student: function(name, grade) { 3 | this.name = name; 4 | this.grade = grade; 5 | } 6 | } 7 | 8 | const foo = new Person.Student('foo', 1); 9 | const bar = new Person.Student('bar', 2); 10 | [ 11 | {name: foo.name, grade: foo.grade}, 12 | {...bar} 13 | ] -------------------------------------------------------------------------------- /test/scenarios/functions/parent_scope_vars.jt: -------------------------------------------------------------------------------- 1 | let a = 1; let b = 2 2 | let fn = function(e){ 3 | (a+b)*e 4 | } 5 | fn(10) + fn(20) -------------------------------------------------------------------------------- /test/scenarios/functions/promise.jt: -------------------------------------------------------------------------------- 1 | await new Promise(function(resolve) { 2 | setTimeout(lambda resolve(^), 100) 3 | }); -------------------------------------------------------------------------------- /test/scenarios/functions/template.jt: -------------------------------------------------------------------------------- 1 | let normalFn = function(a) { 2 | a + 10 3 | }; 4 | let lambdaFn = lambda ?0 + 10; 5 | let spreadFn = function(...a) { 6 | a.reduce(lambda ?0 + ?1, 0) 7 | }; 8 | let fnArr = {spread: spreadFn, other: [normalFn, lambdaFn]}; 9 | fnArr.spread(fnArr.other[0](10), fnArr.other[1](20), function(){30}()) -------------------------------------------------------------------------------- /test/scenarios/increment_statements/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'postfix_decrement_on_literal.jt', 6 | error: 'Invalid postfix increment expression', 7 | }, 8 | { 9 | templatePath: 'postfix_decrement_on_non_id.jt', 10 | error: 'Invalid postfix increment expression', 11 | }, 12 | { 13 | templatePath: 'postfix_increment_on_literal.jt', 14 | error: 'Invalid postfix increment expression', 15 | }, 16 | { 17 | templatePath: 'postfix_increment_on_non_id.jt', 18 | error: 'Invalid postfix increment expression', 19 | }, 20 | { 21 | templatePath: 'prefix_decrement_on_literal.jt', 22 | error: 'Invalid prefix increment expression', 23 | }, 24 | { 25 | templatePath: 'prefix_increment_on_literal.jt', 26 | error: 'Invalid prefix increment expression', 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /test/scenarios/increment_statements/postfix_decrement_on_literal.jt: -------------------------------------------------------------------------------- 1 | 1-- -------------------------------------------------------------------------------- /test/scenarios/increment_statements/postfix_decrement_on_non_id.jt: -------------------------------------------------------------------------------- 1 | .a-- -------------------------------------------------------------------------------- /test/scenarios/increment_statements/postfix_increment_on_literal.jt: -------------------------------------------------------------------------------- 1 | 1++ -------------------------------------------------------------------------------- /test/scenarios/increment_statements/postfix_increment_on_non_id.jt: -------------------------------------------------------------------------------- 1 | .a++ -------------------------------------------------------------------------------- /test/scenarios/increment_statements/prefix_decrement_on_literal.jt: -------------------------------------------------------------------------------- 1 | --1 -------------------------------------------------------------------------------- /test/scenarios/increment_statements/prefix_increment_on_literal.jt: -------------------------------------------------------------------------------- 1 | ++1 -------------------------------------------------------------------------------- /test/scenarios/inputs/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | input: { 6 | a: 10, 7 | b: 2, 8 | c: { 9 | d: 30, 10 | }, 11 | }, 12 | output: 50, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /test/scenarios/inputs/template.jt: -------------------------------------------------------------------------------- 1 | .c.(.d + ^.a * ^.b); 2 | -------------------------------------------------------------------------------- /test/scenarios/logics/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | output: [3, 0, 2, 3, 0, 3, true, true], 6 | }, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/scenarios/logics/template.jt: -------------------------------------------------------------------------------- 1 | [ 2 | 2 && 3, 3 | 0 && 3, 4 | 2 || 3, 5 | 0 || 3, 6 | 0 ?? 3, 7 | ??? [null, undefined, 3], 8 | !false, !!true, 9 | ]; 10 | -------------------------------------------------------------------------------- /test/scenarios/loops/break_without_condition.jt: -------------------------------------------------------------------------------- 1 | for { 2 | break; 3 | } 4 | -------------------------------------------------------------------------------- /test/scenarios/loops/break_without_loop.jt: -------------------------------------------------------------------------------- 1 | 2 | (.num < 0) ? break; -------------------------------------------------------------------------------- /test/scenarios/loops/complex_loop.jt: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | for (let i = 0; i < 5; i++) { 3 | for (let j=0; j < 5; j++) { 4 | i < j ? { 5 | j > 4 ? continue; 6 | count++; 7 | } 8 | } 9 | } 10 | count; -------------------------------------------------------------------------------- /test/scenarios/loops/continue.jt: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | for (let i=0;i <= ^.num;i++) { 3 | console.log(i); 4 | i % 2 === 0 ? continue; 5 | count+=i; 6 | } 7 | count -------------------------------------------------------------------------------- /test/scenarios/loops/continue_without_condition.jt: -------------------------------------------------------------------------------- 1 | for { 2 | continue; 3 | } -------------------------------------------------------------------------------- /test/scenarios/loops/continue_without_loop.jt: -------------------------------------------------------------------------------- 1 | 2 | (.num < 0) ? continue; -------------------------------------------------------------------------------- /test/scenarios/loops/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'break_without_condition.jt', 6 | error: 7 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', 8 | }, 9 | { 10 | templatePath: 'break_without_loop.jt', 11 | error: 'encounted loop control outside loop', 12 | }, 13 | { 14 | templatePath: 'complex_loop.jt', 15 | output: 10, 16 | }, 17 | { 18 | templatePath: 'continue_without_condition.jt', 19 | error: 20 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', 21 | }, 22 | { 23 | templatePath: 'continue_without_loop.jt', 24 | error: 'encounted loop control outside loop', 25 | }, 26 | { 27 | templatePath: 'continue.jt', 28 | input: { 29 | num: 10, 30 | }, 31 | output: 25, 32 | }, 33 | { 34 | templatePath: 'empty_loop.jt', 35 | error: 'Empty statements are not allowed in loop and condtional expressions', 36 | }, 37 | { 38 | templatePath: 'just_for.jt', 39 | input: { 40 | num: 10, 41 | }, 42 | output: 55, 43 | }, 44 | { 45 | input: { 46 | num: 10, 47 | }, 48 | output: 55, 49 | templatePath: 'no_init.jt', 50 | }, 51 | { 52 | input: { 53 | num: 10, 54 | }, 55 | output: 55, 56 | templatePath: 'no_test.jt', 57 | }, 58 | { 59 | input: { 60 | num: 10, 61 | }, 62 | output: 55, 63 | templatePath: 'no_update.jt', 64 | }, 65 | { 66 | templatePath: 'statement_after_break.jt', 67 | error: 68 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', 69 | }, 70 | { 71 | templatePath: 'statement_after_continue.jt', 72 | error: 73 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', 74 | }, 75 | { 76 | input: { 77 | num: 10, 78 | }, 79 | output: 55, 80 | }, 81 | ]; 82 | -------------------------------------------------------------------------------- /test/scenarios/loops/empty_loop.jt: -------------------------------------------------------------------------------- 1 | for {} -------------------------------------------------------------------------------- /test/scenarios/loops/just_for.jt: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | let i = 0; 3 | for { 4 | i > ^.num ? break; 5 | count+=i; 6 | i++; 7 | } 8 | count -------------------------------------------------------------------------------- /test/scenarios/loops/no_init.jt: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | let i = 0; 3 | for (;i <= ^.num;i++) { 4 | count+=i; 5 | } 6 | count -------------------------------------------------------------------------------- /test/scenarios/loops/no_test.jt: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | for (let i=0;;i++) { 3 | i > ^.num ? break; 4 | count+=i; 5 | } 6 | count -------------------------------------------------------------------------------- /test/scenarios/loops/no_update.jt: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | for (let i=0;i <= ^.num;) { 3 | count+=i; 4 | i++; 5 | } 6 | count -------------------------------------------------------------------------------- /test/scenarios/loops/statement_after_break.jt: -------------------------------------------------------------------------------- 1 | for { 2 | .num > 1 ? { 3 | break; 4 | let count = 0; 5 | } 6 | } -------------------------------------------------------------------------------- /test/scenarios/loops/statement_after_continue.jt: -------------------------------------------------------------------------------- 1 | for { 2 | .num > 1 ? { 3 | continue; 4 | let count = 0; 5 | } 6 | } -------------------------------------------------------------------------------- /test/scenarios/loops/template.jt: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | for (let i=0;i <= ^.num;i++) { 3 | count+=i; 4 | } 5 | count -------------------------------------------------------------------------------- /test/scenarios/mappings/all_features.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.userId", 4 | "output": "$.user.id" 5 | }, 6 | { 7 | "input": "$.discount", 8 | "output": "$.events[0].items[*].discount" 9 | }, 10 | { 11 | "input": "$.products[?(@.category)].id", 12 | "output": "$.events[0].items[*].product_id" 13 | }, 14 | { 15 | "input": "$.coupon", 16 | "output": "$.events[0].items[*].coupon_code" 17 | }, 18 | { 19 | "input": "$.events[0]", 20 | "output": "$.events[0].name" 21 | }, 22 | { 23 | "input": "$.products[*].name", 24 | "output": "$.events[0].items[*].product_name" 25 | }, 26 | { 27 | "from": "$.products[*].category", 28 | "to": "$.events[0].items[*].product_category" 29 | }, 30 | { 31 | "input": "$.products[*].variations[*].size", 32 | "output": "$.events[0].items[*].options[*].s" 33 | }, 34 | { 35 | "input": "$.products[*].(@.price * @.quantity * (1 - $.discount / 100))", 36 | "output": "$.events[0].items[*].value" 37 | }, 38 | { 39 | "input": "$.products[?(@.category)].(@.price * @.quantity * (1 - $.discount / 100)).sum()", 40 | "output": "$.events[0].revenue" 41 | }, 42 | { 43 | "input": "$.products[*].variations[*].length", 44 | "output": "$.events[0].items[*].options[*].l" 45 | }, 46 | { 47 | "input": "$.products[*].variations[*].width", 48 | "output": "$.events[0].items[*].options[*].w" 49 | }, 50 | { 51 | "input": "$.products[*].variations[*].color", 52 | "output": "$.events[0].items[*].options[*].c" 53 | }, 54 | { 55 | "input": "$.products[*].variations[*].height", 56 | "output": "$.events[0].items[*].options[*].h" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /test/scenarios/mappings/context_vars_mapping.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from": "$.a[*].#index", 4 | "to": "$.b[*].#index" 5 | }, 6 | { 7 | "from": "$.a[*].#index.foo", 8 | "to": "$.b[*].bar" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/scenarios/mappings/data.ts: -------------------------------------------------------------------------------- 1 | import type { Scenario } from '../../types'; 2 | 3 | const input = { 4 | userId: 'u1', 5 | discount: 10, 6 | coupon: 'DISCOUNT', 7 | events: ['purchase', 'custom'], 8 | details: { 9 | name: 'Purchase', 10 | timestamp: 1630000000, 11 | }, 12 | context: { 13 | traits: { 14 | email: 'dummy@example.com', 15 | first_name: 'John', 16 | last_name: 'Doe', 17 | phone: '1234567890', 18 | }, 19 | }, 20 | products: [ 21 | { 22 | id: 1, 23 | name: 'p1', 24 | category: 'baby', 25 | price: 3, 26 | quantity: 2, 27 | variations: [ 28 | { 29 | color: 'blue', 30 | size: 1, 31 | }, 32 | { 33 | size: 2, 34 | }, 35 | ], 36 | }, 37 | { 38 | id: 2, 39 | name: 'p2', 40 | price: 5, 41 | quantity: 3, 42 | variations: [ 43 | { 44 | length: 1, 45 | }, 46 | { 47 | color: 'red', 48 | length: 2, 49 | }, 50 | ], 51 | }, 52 | { 53 | id: 3, 54 | name: 'p3', 55 | category: 'home', 56 | price: 10, 57 | quantity: 1, 58 | variations: [ 59 | { 60 | width: 1, 61 | height: 2, 62 | length: 3, 63 | }, 64 | ], 65 | }, 66 | ], 67 | }; 68 | export const data: Scenario[] = [ 69 | { 70 | mappingsPath: 'all_features.json', 71 | input, 72 | output: { 73 | events: [ 74 | { 75 | items: [ 76 | { 77 | discount: 10, 78 | product_id: 1, 79 | coupon_code: 'DISCOUNT', 80 | product_name: 'p1', 81 | product_category: 'baby', 82 | options: [ 83 | { 84 | s: 1, 85 | c: 'blue', 86 | }, 87 | { 88 | s: 2, 89 | }, 90 | ], 91 | value: 5.4, 92 | }, 93 | { 94 | discount: 10, 95 | product_id: 3, 96 | coupon_code: 'DISCOUNT', 97 | product_name: 'p3', 98 | product_category: 'home', 99 | options: [ 100 | { 101 | l: 3, 102 | w: 1, 103 | h: 2, 104 | }, 105 | ], 106 | value: 9, 107 | }, 108 | ], 109 | name: 'purchase', 110 | revenue: 14.4, 111 | }, 112 | ], 113 | user: { 114 | id: 'u1', 115 | }, 116 | }, 117 | }, 118 | { 119 | mappingsPath: 'context_vars_mapping.json', 120 | input: { 121 | a: [ 122 | { 123 | foo: 1, 124 | }, 125 | { 126 | foo: 2, 127 | }, 128 | ], 129 | }, 130 | output: { 131 | b: [ 132 | { bar: 1, index: 0 }, 133 | { bar: 2, index: 1 }, 134 | ], 135 | }, 136 | }, 137 | { 138 | mappings: [ 139 | { 140 | from: '$.a[*]', 141 | to: '$.b[*].#index', 142 | }, 143 | ], 144 | error: 'Invalid mapping', 145 | }, 146 | { 147 | mappings: [ 148 | { 149 | from: '1', 150 | to: '$.b[*].#index', 151 | }, 152 | ], 153 | error: 'Invalid mapping', 154 | }, 155 | { 156 | mappingsPath: 'filters.json', 157 | 158 | input, 159 | output: { 160 | items: [ 161 | { 162 | product_id: 1, 163 | product_name: 'p1', 164 | product_category: 'baby', 165 | }, 166 | { 167 | product_id: 3, 168 | product_name: 'p3', 169 | product_category: 'home', 170 | }, 171 | ], 172 | }, 173 | }, 174 | { 175 | mappingsPath: 'index_mappings.json', 176 | input, 177 | output: { 178 | events: [ 179 | { 180 | name: 'purchase', 181 | type: 'identify', 182 | }, 183 | { 184 | name: 'custom', 185 | type: 'track', 186 | }, 187 | ], 188 | }, 189 | }, 190 | { 191 | mappingsPath: 'invalid_array_index_mappings.json', 192 | error: 'Invalid mapping', 193 | }, 194 | { 195 | description: 'Index mappings in last part', 196 | mappings: [ 197 | { 198 | from: '$.a[0]', 199 | to: '$.b[0]', 200 | }, 201 | { 202 | from: '$.a[1]', 203 | to: '$.b[1]', 204 | }, 205 | ], 206 | input: { a: [1, 2, 3] }, 207 | output: { b: [1, 2] }, 208 | }, 209 | { 210 | mappingsPath: 'invalid_array_mappings.json', 211 | error: 'Invalid mapping', 212 | }, 213 | { 214 | mappingsPath: 'invalid_object_mappings.json', 215 | error: 'Invalid mapping', 216 | }, 217 | { 218 | mappingsPath: 'mappings_with_root_fields.json', 219 | input, 220 | output: { 221 | items: [ 222 | { 223 | product_id: 1, 224 | product_name: 'p1', 225 | product_category: 'baby', 226 | discount: 10, 227 | coupon_code: 'DISCOUNT', 228 | }, 229 | { 230 | product_id: 2, 231 | product_name: 'p2', 232 | discount: 10, 233 | coupon_code: 'DISCOUNT', 234 | }, 235 | { 236 | product_id: 3, 237 | product_name: 'p3', 238 | product_category: 'home', 239 | discount: 10, 240 | coupon_code: 'DISCOUNT', 241 | }, 242 | ], 243 | }, 244 | }, 245 | { 246 | mappingsPath: 'missing_array_index_mappings.json', 247 | error: 'Invalid mapping', 248 | }, 249 | { 250 | mappingsPath: 'nested_mappings.json', 251 | input, 252 | output: { 253 | items: [ 254 | { 255 | product_id: 1, 256 | product_name: 'p1', 257 | product_category: 'baby', 258 | options: [ 259 | { 260 | s: 1, 261 | c: 'blue', 262 | }, 263 | { 264 | s: 2, 265 | }, 266 | ], 267 | }, 268 | { 269 | product_id: 2, 270 | product_name: 'p2', 271 | options: [ 272 | { 273 | l: 1, 274 | }, 275 | { 276 | l: 2, 277 | c: 'red', 278 | }, 279 | ], 280 | }, 281 | { 282 | product_id: 3, 283 | product_name: 'p3', 284 | product_category: 'home', 285 | options: [ 286 | { 287 | l: 3, 288 | w: 1, 289 | h: 2, 290 | }, 291 | ], 292 | }, 293 | ], 294 | }, 295 | }, 296 | { 297 | mappingsPath: 'non_path_output.json', 298 | output: { 299 | 'Content-Type': 'application/json', 300 | 'a.b.c': 3, 301 | bar: 1, 302 | c: { 'Content-Type': 'text/plain' }, 303 | 'x-bar': 2, 304 | }, 305 | }, 306 | { 307 | mappingsPath: 'object_mappings.json', 308 | input: { 309 | user_id: 1, 310 | traits1: { 311 | name: 'John Doe', 312 | age: 30, 313 | }, 314 | traits2: [ 315 | { 316 | name: { 317 | value: 'John Doe', 318 | }, 319 | }, 320 | { 321 | age: { 322 | value: 30, 323 | }, 324 | }, 325 | ], 326 | traits3: { 327 | display_name: 'Rudderstack Inc.', 328 | category: 'Analytics', 329 | custom_properties: { 330 | bar: 1, 331 | }, 332 | }, 333 | }, 334 | output: { 335 | user_id: { 336 | value: 1, 337 | }, 338 | traits1: { 339 | value: { 340 | name: 'John Doe', 341 | age: 30, 342 | }, 343 | }, 344 | traits2: { 345 | value: [ 346 | { 347 | name: { 348 | value: 'John Doe', 349 | }, 350 | }, 351 | { 352 | age: { 353 | value: 30, 354 | }, 355 | }, 356 | ], 357 | }, 358 | traits3: { 359 | value: { 360 | display_name: 'Rudderstack Inc.', 361 | category: 'Analytics', 362 | custom_properties: { 363 | bar: 1, 364 | }, 365 | }, 366 | }, 367 | properties1: { 368 | name: { 369 | value: 'John Doe', 370 | }, 371 | age: { 372 | value: 30, 373 | }, 374 | }, 375 | properties2: [ 376 | { 377 | name: 'John Doe', 378 | }, 379 | { 380 | age: 30, 381 | }, 382 | ], 383 | properties3: { 384 | display_name: 'Rudderstack Inc.', 385 | category: 'Analytics', 386 | custom_properties: { 387 | bar: 1, 388 | }, 389 | name: 'Rudderstack Inc.', 390 | custom: { 391 | bar: 1, 392 | foo: 1, 393 | }, 394 | }, 395 | }, 396 | }, 397 | { 398 | mappingsPath: 'or_mappings.json', 399 | input: { 400 | context: { 401 | properties: { 402 | name: 'John', 403 | age: 30, 404 | }, 405 | }, 406 | }, 407 | output: { 408 | user: { 409 | name: 'John', 410 | age: 30, 411 | }, 412 | }, 413 | }, 414 | { 415 | mappingsPath: 'override_nested_arrays.json', 416 | input: { 417 | properties: { 418 | type: 'bar', 419 | products: [ 420 | { 421 | name: 'a', 422 | }, 423 | { 424 | name: 'b', 425 | }, 426 | ], 427 | }, 428 | }, 429 | output: { 430 | properties: { 431 | products: [{ category: 'bar' }, { category: 'bar' }], 432 | type: 'bar', 433 | }, 434 | }, 435 | }, 436 | { 437 | mappingsPath: 'or_mappings.json', 438 | input: { 439 | properties: { 440 | name: 'John Doe', 441 | age: 30, 442 | }, 443 | context: { 444 | properties: { 445 | name: 'John', 446 | age: 30, 447 | }, 448 | }, 449 | }, 450 | output: { 451 | user: { 452 | name: 'John Doe', 453 | age: 30, 454 | }, 455 | }, 456 | }, 457 | { 458 | mappingsPath: 'or_mappings.json', 459 | input: { 460 | properties: { 461 | name: 'John Doe', 462 | age: 30, 463 | }, 464 | }, 465 | output: { 466 | user: { 467 | name: 'John Doe', 468 | age: 30, 469 | }, 470 | }, 471 | }, 472 | { 473 | mappingsPath: 'root_array_mappings.json', 474 | input: [ 475 | { 476 | user_id: 1, 477 | user_name: 'John Doe', 478 | }, 479 | { 480 | user_id: 2, 481 | user_name: 'Jane Doe', 482 | }, 483 | ], 484 | output: [ 485 | { 486 | user: { 487 | id: 1, 488 | name: 'John Doe', 489 | }, 490 | }, 491 | { 492 | user: { 493 | id: 2, 494 | name: 'Jane Doe', 495 | }, 496 | }, 497 | ], 498 | }, 499 | { 500 | mappingsPath: 'root_context_vars_mapping.json', 501 | input: [ 502 | { 503 | foo: 1, 504 | }, 505 | { 506 | foo: 2, 507 | }, 508 | ], 509 | output: [ 510 | { 511 | bar: 1, 512 | index: 0, 513 | }, 514 | { 515 | bar: 2, 516 | index: 1, 517 | }, 518 | ], 519 | }, 520 | { 521 | mappingsPath: 'root_index_mappings.json', 522 | input: { 523 | id: 1, 524 | name: 'John Doe', 525 | }, 526 | output: [ 527 | { 528 | user_id: 1, 529 | user_name: 'John Doe', 530 | }, 531 | ], 532 | }, 533 | { 534 | mappingsPath: 'root_mappings.json', 535 | input, 536 | output: { 537 | traits: { 538 | email: 'dummy@example.com', 539 | first_name: 'John', 540 | last_name: 'Doe', 541 | phone: '1234567890', 542 | }, 543 | event_names: ['purchase', 'custom'], 544 | name: 'Purchase', 545 | timestamp: 1630000000, 546 | }, 547 | }, 548 | { 549 | mappingsPath: 'root_nested_mappings.json', 550 | input: [ 551 | { 552 | user_id: 1, 553 | user_name: 'John Doe', 554 | }, 555 | { 556 | user_id: 2, 557 | user_name: 'Jane Doe', 558 | }, 559 | ], 560 | output: [ 561 | { 562 | user_id: { 563 | value: 1, 564 | }, 565 | user_name: { 566 | value: 'John Doe', 567 | }, 568 | }, 569 | { 570 | user_id: { 571 | value: 2, 572 | }, 573 | user_name: { 574 | value: 'Jane Doe', 575 | }, 576 | }, 577 | ], 578 | }, 579 | { 580 | mappingsPath: 'root_object_mappings.json', 581 | input: { 582 | user_id: 1, 583 | user_name: 'John Doe', 584 | }, 585 | output: { 586 | user_id: { 587 | value: 1, 588 | }, 589 | user_name: { 590 | value: 'John Doe', 591 | }, 592 | }, 593 | }, 594 | { 595 | description: 'array mappings in last part', 596 | mappings: [ 597 | { 598 | from: '$.a[*]', 599 | to: '$.b[*]', 600 | }, 601 | ], 602 | input: { a: [1, 2, 3] }, 603 | output: { b: [1, 2, 3] }, 604 | }, 605 | { 606 | description: 'array mappings to scalar value', 607 | mappings: [ 608 | { 609 | from: '1', 610 | to: '$.a[*].b', 611 | }, 612 | ], 613 | output: { a: [{ b: 1 }] }, 614 | }, 615 | { 616 | description: 'array mappings to scalar value', 617 | mappings: [ 618 | { 619 | from: '1', 620 | to: '$.a[*]', 621 | }, 622 | ], 623 | output: { a: [1] }, 624 | }, 625 | { 626 | mappingsPath: 'simple_array_mappings.json', 627 | input: { 628 | user_id: 1, 629 | user_name: 'John Doe', 630 | }, 631 | output: { 632 | users: [ 633 | { 634 | id: 1, 635 | name: 'John Doe', 636 | }, 637 | ], 638 | }, 639 | }, 640 | { 641 | input: { 642 | a: [ 643 | { 644 | a: 1, 645 | }, 646 | { 647 | a: 2, 648 | }, 649 | ], 650 | }, 651 | output: 3, 652 | }, 653 | { 654 | template: '~m[1, 2]', 655 | error: 'Invalid mapping', 656 | }, 657 | { 658 | template: '~m[{}]', 659 | error: 'Invalid mapping', 660 | }, 661 | { 662 | template: '~m[{input: 1, output: 2}]', 663 | error: 'Invalid mapping', 664 | }, 665 | { 666 | mappingsPath: 'transformations.json', 667 | input, 668 | output: { 669 | items: [ 670 | { 671 | product_id: 1, 672 | product_name: 'p1', 673 | product_category: 'baby', 674 | value: 5.4, 675 | }, 676 | { 677 | product_id: 2, 678 | product_name: 'p2', 679 | value: 13.5, 680 | }, 681 | { 682 | product_id: 3, 683 | product_name: 'p3', 684 | product_category: 'home', 685 | value: 9, 686 | }, 687 | ], 688 | revenue: 27.9, 689 | }, 690 | }, 691 | ]; 692 | -------------------------------------------------------------------------------- /test/scenarios/mappings/filters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.products[?(@.category)].id", 4 | "output": "$.items[*].product_id" 5 | }, 6 | { 7 | "input": "$.products[*].name", 8 | "output": "$.items[*].product_name" 9 | }, 10 | { 11 | "input": "$.products[*].category", 12 | "output": "$.items[*].product_category" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/scenarios/mappings/index_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.events[0]", 4 | "output": "$.events[0].name" 5 | }, 6 | { 7 | "input": "'identify'", 8 | "output": "$.events[0].type" 9 | }, 10 | { 11 | "input": "$.events[1]", 12 | "output": "$.events[1].name" 13 | }, 14 | { 15 | "input": "'track'", 16 | "output": "$.events[1].type" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /test/scenarios/mappings/invalid_array_index_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from": "$.products[*].a", 4 | "to": "$.items[*].a" 5 | }, 6 | { 7 | "from": "$.products[*].b", 8 | "to": "$.items[0].b" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/scenarios/mappings/invalid_array_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.events[0]", 4 | "output": "$.events[0].name" 5 | }, 6 | { 7 | "input": "$.discount", 8 | "output": "$.events[0].name[*].discount" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/scenarios/mappings/invalid_object_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.traits1", 4 | "output": "$.properties1.*.value" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /test/scenarios/mappings/mappings_with_root_fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.discount", 4 | "output": "$.items[*].discount" 5 | }, 6 | { 7 | "input": "$.products[*].id", 8 | "output": "$.items[*].product_id" 9 | }, 10 | { 11 | "input": "$.products[*].name", 12 | "output": "$.items[*].product_name" 13 | }, 14 | { 15 | "input": "$.coupon", 16 | "output": "$.items[*].coupon_code" 17 | }, 18 | { 19 | "input": "$.products[*].category", 20 | "output": "$.items[*].product_category" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /test/scenarios/mappings/missing_array_index_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.events[1]", 4 | "output": "$.events[1].name" 5 | }, 6 | { 7 | "input": "'track'", 8 | "output": "$.events[1].type" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/scenarios/mappings/nested_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.products[*].id", 4 | "output": "$.items[*].product_id" 5 | }, 6 | { 7 | "input": "$.products[*].name", 8 | "output": "$.items[*].product_name" 9 | }, 10 | { 11 | "input": "$.products[*].category", 12 | "output": "$.items[*].product_category" 13 | }, 14 | { 15 | "input": "$.products[*].variations[*].size", 16 | "output": "$.items[*].options[*].s" 17 | }, 18 | { 19 | "input": "$.products[*].variations[*].length", 20 | "output": "$.items[*].options[*].l" 21 | }, 22 | { 23 | "input": "$.products[*].variations[*].width", 24 | "output": "$.items[*].options[*].w" 25 | }, 26 | { 27 | "input": "$.products[*].variations[*].color", 28 | "output": "$.items[*].options[*].c" 29 | }, 30 | { 31 | "input": "$.products[*].variations[*].height", 32 | "output": "$.items[*].options[*].h" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /test/scenarios/mappings/non_path_output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "'application/json'", 4 | "output": "Content-Type" 5 | }, 6 | { 7 | "input": "1", 8 | "output": "bar" 9 | }, 10 | { 11 | "input": "2", 12 | "output": "x-bar" 13 | }, 14 | { 15 | "input": "2", 16 | "description": "This mapping will be ignored because the output is not defined" 17 | }, 18 | { 19 | "input": "3", 20 | "output": "'a.b.c'" 21 | }, 22 | { 23 | "input": "'text/plain'", 24 | "output": "c.'Content-Type'" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /test/scenarios/mappings/object_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.*", 4 | "output": "$.*.value" 5 | }, 6 | { 7 | "input": "$.traits1.*", 8 | "output": "$.properties1.*.value" 9 | }, 10 | { 11 | "input": "$.traits2[*].*.value", 12 | "output": "$.properties2[*].*" 13 | }, 14 | { 15 | "input": "$.traits3", 16 | "output": "$.properties3" 17 | }, 18 | { 19 | "input": "$.traits3.display_name", 20 | "output": "$.properties3.name" 21 | }, 22 | { 23 | "input": "$.traits3.custom_properties", 24 | "output": "$.properties3['custom']" 25 | }, 26 | { 27 | "input": "$.traits3.custom_properties.bar", 28 | "output": "$.properties3.custom.foo" 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /test/scenarios/mappings/or_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.properties.name", 4 | "output": "$.user.name" 5 | }, 6 | { 7 | "input": "$.properties.age", 8 | "output": "$.user.age" 9 | }, 10 | { 11 | "input": "$.context.properties.name", 12 | "output": "$.user.name" 13 | }, 14 | { 15 | "input": "$.context.properties.age", 16 | "output": "$.user.age" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /test/scenarios/mappings/override_nested_arrays.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from": "$.properties", 4 | "to": "$.properties" 5 | }, 6 | { 7 | "to": "$.properties.products[0].category", 8 | "from": "$.properties.type" 9 | }, 10 | { 11 | "to": "$.properties.products[1].category", 12 | "from": "$.properties.type" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/scenarios/mappings/root_array_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$[*].user_id", 4 | "output": "$[*].user.id" 5 | }, 6 | { 7 | "input": "$[*].user_name", 8 | "output": "$[*].user.name" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/scenarios/mappings/root_context_vars_mapping.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from": "$[*].#index", 4 | "to": "$[*].#index" 5 | }, 6 | { 7 | "from": "$[*].foo", 8 | "to": "$[*].bar" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/scenarios/mappings/root_index_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.id", 4 | "output": "$[0].user_id" 5 | }, 6 | { 7 | "input": "$.name", 8 | "output": "$[0].user_name" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/scenarios/mappings/root_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.context", 4 | "output": "$" 5 | }, 6 | { 7 | "input": "$.events", 8 | "output": "$.event_names" 9 | }, 10 | { 11 | "input": "$.details", 12 | "output": "$" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/scenarios/mappings/root_nested_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$[*].*", 4 | "output": "$[*].*.value" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /test/scenarios/mappings/root_object_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.*", 4 | "output": "$.*.value" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /test/scenarios/mappings/simple_array_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.user_id", 4 | "output": "$.users[*].id" 5 | }, 6 | { 7 | "input": "$.user_name", 8 | "output": "$.users[*].name" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/scenarios/mappings/template.jt: -------------------------------------------------------------------------------- 1 | const temp = ~m[ 2 | { 3 | input: '^.a[*].a', 4 | output: '^.b[*].b', 5 | } 6 | ]; 7 | 8 | ~r temp.b.b.sum(); 9 | -------------------------------------------------------------------------------- /test/scenarios/mappings/transformations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "input": "$.products[*].id", 4 | "output": "$.items[*].product_id" 5 | }, 6 | { 7 | "input": "$.products[*].name", 8 | "output": "$.items[*].product_name" 9 | }, 10 | { 11 | "input": "$.products[*].category", 12 | "output": "$.items[*].product_category" 13 | }, 14 | { 15 | "input": "$.products[*].(@.price * @.quantity * (1 - $.discount / 100))", 16 | "output": "$.items[*].value" 17 | }, 18 | { 19 | "input": "$.products[*].(@.price * @.quantity * (1 - $.discount / 100)).sum()", 20 | "output": "$.revenue" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /test/scenarios/math/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | output: [12, 8, 20, 5, 100, 0, 40, 2], 6 | }, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/scenarios/math/template.jt: -------------------------------------------------------------------------------- 1 | [10 + 2, 10 - 2, 10 * 2, 10 / 2, 10 ** 2, 10 % 2, 10 << 2, 10 >> 2]; 2 | -------------------------------------------------------------------------------- /test/scenarios/objects/context_props.jt: -------------------------------------------------------------------------------- 1 | { 2 | user: { 3 | props: .traits.({ 4 | @e [e.key]: { 5 | value: e.value 6 | }, 7 | someKey: { 8 | value: 'someValue' 9 | } 10 | }), 11 | events: .events.({ 12 | @e [e.value]: e.key, 13 | someEventValue: 'someEventName' 14 | }) 15 | } 16 | } -------------------------------------------------------------------------------- /test/scenarios/objects/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'context_props.jt', 6 | input: { 7 | traits: { 8 | name: 'John Doe', 9 | age: 30, 10 | }, 11 | events: { 12 | foo: 'bar', 13 | something: 'something else', 14 | }, 15 | }, 16 | output: { 17 | user: { 18 | props: { 19 | name: { 20 | value: 'John Doe', 21 | }, 22 | age: { 23 | value: 30, 24 | }, 25 | someKey: { 26 | value: 'someValue', 27 | }, 28 | }, 29 | events: { 30 | bar: 'foo', 31 | 'something else': 'something', 32 | someEventValue: 'someEventName', 33 | }, 34 | }, 35 | }, 36 | }, 37 | { 38 | templatePath: 'invalid_context_prop.jt', 39 | error: 'Context prop should be used with a key expression', 40 | }, 41 | { 42 | output: { 43 | a: 1, 44 | b: 2, 45 | d: 3, 46 | }, 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /test/scenarios/objects/invalid_context_prop.jt: -------------------------------------------------------------------------------- 1 | { 2 | user: { 3 | props: .traits.({ 4 | @e key: { 5 | value: @bar 6 | } 7 | }) 8 | } 9 | } -------------------------------------------------------------------------------- /test/scenarios/objects/template.jt: -------------------------------------------------------------------------------- 1 | let c = "c key"; 2 | let d = 3; 3 | let b = 2; 4 | let a = { 5 | "a b": 1, 6 | b, 7 | // [c] coverts to "c key" 8 | [c]: { 9 | // this coverts to d: 3 10 | d 11 | }, 12 | }; 13 | a.({ 14 | a: ."a b", 15 | b: .b, 16 | ...(.'c key') 17 | }) -------------------------------------------------------------------------------- /test/scenarios/paths/block.jt: -------------------------------------------------------------------------------- 1 | [ 2 | .({ 3 | a: @.a + 1, 4 | b: @.b + 2 5 | }), 6 | .([.a + 1, .b + 2]) 7 | ] -------------------------------------------------------------------------------- /test/scenarios/paths/data.ts: -------------------------------------------------------------------------------- 1 | import { PathType } from '../../../src'; 2 | import { Scenario } from '../../types'; 3 | 4 | export const data: Scenario[] = [ 5 | { 6 | templatePath: 'block.jt', 7 | input: { 8 | a: 1, 9 | b: 1, 10 | }, 11 | output: [{ a: 2, b: 3 }, [2, 3]], 12 | }, 13 | { 14 | templatePath: 'options.jt', 15 | options: { 16 | defaultPathType: PathType.RICH, 17 | }, 18 | input: { 19 | a: { 20 | b: [{ c: [{ d: 1 }, { d: 2 }] }], 21 | }, 22 | }, 23 | output: [ 24 | [ 25 | { 26 | d: 1, 27 | }, 28 | { 29 | d: 2, 30 | }, 31 | ], 32 | [ 33 | { 34 | d: 1, 35 | }, 36 | { 37 | d: 2, 38 | }, 39 | ], 40 | undefined, 41 | { 42 | d: 2, 43 | }, 44 | ], 45 | }, 46 | { 47 | templatePath: 'options.jt', 48 | options: { 49 | defaultPathType: PathType.SIMPLE, 50 | }, 51 | input: { 52 | a: { 53 | b: [{ c: [{ d: 1 }, { d: 2 }] }], 54 | }, 55 | }, 56 | output: [ 57 | undefined, 58 | [ 59 | { 60 | d: 1, 61 | }, 62 | { 63 | d: 2, 64 | }, 65 | ], 66 | undefined, 67 | { 68 | d: 2, 69 | }, 70 | ], 71 | }, 72 | { 73 | templatePath: 'json_path.jt', 74 | input: { 75 | foo: 'bar', 76 | size: 1, 77 | items: [ 78 | { 79 | a: 1, 80 | b: 1, 81 | }, 82 | { 83 | a: 2, 84 | b: 2, 85 | }, 86 | { 87 | a: 3, 88 | b: 3, 89 | }, 90 | ], 91 | }, 92 | output: [ 93 | 'bar', 94 | [ 95 | { 96 | a: 1, 97 | b: 1, 98 | }, 99 | { 100 | a: 2, 101 | b: 2, 102 | }, 103 | { 104 | a: 3, 105 | b: 3, 106 | }, 107 | ], 108 | [ 109 | { 110 | a: 2, 111 | b: 2, 112 | }, 113 | { 114 | a: 3, 115 | b: 3, 116 | }, 117 | ], 118 | [2, 4, 6], 119 | ], 120 | }, 121 | { 122 | templatePath: 'options.jt', 123 | options: { 124 | defaultPathType: PathType.SIMPLE, 125 | }, 126 | input: { 127 | a: { 128 | b: { 129 | c: [{ d: 1 }, { d: 2 }], 130 | }, 131 | }, 132 | }, 133 | output: [ 134 | [ 135 | { 136 | d: 1, 137 | }, 138 | { 139 | d: 2, 140 | }, 141 | ], 142 | [ 143 | { 144 | d: 1, 145 | }, 146 | { 147 | d: 2, 148 | }, 149 | ], 150 | [ 151 | { 152 | d: 1, 153 | }, 154 | { 155 | d: 2, 156 | }, 157 | ], 158 | { 159 | d: 2, 160 | }, 161 | ], 162 | }, 163 | { 164 | templatePath: 'rich_path.jt', 165 | input: [ 166 | { 167 | a: { e: 1 }, 168 | b: [{ e: 2 }, { e: [3, 2] }], 169 | c: { c: { e: 4 } }, 170 | d: [ 171 | [1, 2], 172 | [3, 4], 173 | ], 174 | }, 175 | ], 176 | output: [1, [4], 2], 177 | }, 178 | { 179 | templatePath: 'simple_path.jt', 180 | input: [ 181 | { 182 | a: { e: 1 }, 183 | b: [{ e: 2 }, { e: [3, 2] }], 184 | c: { c: { e: 4 } }, 185 | d: [ 186 | [1, 2], 187 | [3, 4], 188 | ], 189 | }, 190 | ], 191 | output: [1, 1, [4], 2, 1], 192 | }, 193 | { 194 | input: [ 195 | { 196 | a: { e: 1 }, 197 | b: [{ e: 2 }, { e: [3, 2] }], 198 | c: { c: { e: 4 } }, 199 | d: [ 200 | [1, 2], 201 | [3, 4], 202 | ], 203 | }, 204 | ], 205 | output: [ 206 | [3], 207 | 'aa', 208 | { 209 | e: 1, 210 | }, 211 | 4, 212 | 3, 213 | 4, 214 | 1, 215 | ], 216 | options: { 217 | defaultPathType: PathType.RICH, 218 | }, 219 | }, 220 | ]; 221 | -------------------------------------------------------------------------------- /test/scenarios/paths/json_path.jt: -------------------------------------------------------------------------------- 1 | [ 2 | ~j $.foo, 3 | ~j $.items[*], 4 | ~j $.items[?(@.a>$.size)], 5 | ~j $.items.(@.a + @.b) 6 | ] -------------------------------------------------------------------------------- /test/scenarios/paths/options.jt: -------------------------------------------------------------------------------- 1 | [ 2 | // Based path options it will be treat as either simple or rich path 3 | .a.b.c, 4 | // this is always treated as rich path 5 | ~r .a.b.c, 6 | // this is always treated as simple path 7 | ~s .a.b.c, 8 | // this is always treated as rich path as it uses rich features like 9 | .a.b.c{.d > 1} 10 | ] -------------------------------------------------------------------------------- /test/scenarios/paths/rich_path.jt: -------------------------------------------------------------------------------- 1 | [ 2 | ~r .0.a."e", 3 | ~r .0.d.1.1[], 4 | ~r .[0].b.1.e.1 5 | ] -------------------------------------------------------------------------------- /test/scenarios/paths/simple_path.jt: -------------------------------------------------------------------------------- 1 | [ 2 | ~s ^.0.a."e", 3 | ~s ^.0.a["e"], 4 | ~s .0.d.1.1[], 5 | .[0].b.()~s .1.e.1, 6 | {a: {b : 1}}.a.b 7 | ] -------------------------------------------------------------------------------- /test/scenarios/paths/template.jt: -------------------------------------------------------------------------------- 1 | [ 2 | 3[], 3 | "aa"[0][][0][][0], 4 | ^.e ?? ^.a, 5 | ^.c.e ?? .c.c.e, 6 | .b.1.e.0 ?? .b.0.e, 7 | .d.1.1, 8 | ^[].length 9 | ] -------------------------------------------------------------------------------- /test/scenarios/return/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'return_without_condition.jt', 6 | error: 7 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', 8 | }, 9 | { 10 | templatePath: 'statement_after_return.jt', 11 | error: 12 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', 13 | }, 14 | { 15 | templatePath: 'return_no_value.jt', 16 | input: 2, 17 | }, 18 | { 19 | templatePath: 'return_value.jt', 20 | input: 3, 21 | output: 1, 22 | }, 23 | { 24 | templatePath: 'return_value.jt', 25 | input: 2, 26 | output: 1, 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /test/scenarios/return/return_no_value.jt: -------------------------------------------------------------------------------- 1 | (. % 2 === 0) ? { 2 | return; 3 | } 4 | (. - 1)/2; -------------------------------------------------------------------------------- /test/scenarios/return/return_value.jt: -------------------------------------------------------------------------------- 1 | (. % 2 === 0) ? { 2 | return ./2; 3 | } 4 | (. - 1)/2; -------------------------------------------------------------------------------- /test/scenarios/return/return_without_condition.jt: -------------------------------------------------------------------------------- 1 | return; -------------------------------------------------------------------------------- /test/scenarios/return/statement_after_return.jt: -------------------------------------------------------------------------------- 1 | .num > 1 ? { 2 | return; 3 | let count = 0; 4 | } -------------------------------------------------------------------------------- /test/scenarios/selectors/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | input: { 6 | a: 10, 7 | b: 2, 8 | c: { 9 | d: 30, 10 | }, 11 | }, 12 | output: 50, 13 | }, 14 | { 15 | templatePath: 'wild_cards.jt', 16 | input: { 17 | a: { d: 1 }, 18 | b: [{ d: 2 }, { d: 3 }], 19 | c: { c: { d: 4 } }, 20 | }, 21 | output: [ 22 | [1, 2, 3], 23 | [1, 2, 3, 4], 24 | ], 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /test/scenarios/selectors/template.jt: -------------------------------------------------------------------------------- 1 | ..d[0] + .a * .b -------------------------------------------------------------------------------- /test/scenarios/selectors/wild_cards.jt: -------------------------------------------------------------------------------- 1 | [.*.d, ..*.d].. 2 | -------------------------------------------------------------------------------- /test/scenarios/standard_functions/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | const input = { 4 | arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5 | obj: { 6 | foo: 1, 7 | bar: 2, 8 | baz: 3, 9 | quux: 4, 10 | }, 11 | }; 12 | 13 | export const data: Scenario[] = [ 14 | { 15 | template: '.arr.avg()', 16 | input, 17 | output: 5.5, 18 | }, 19 | { 20 | template: '.arr.first()', 21 | input, 22 | output: 1, 23 | }, 24 | { 25 | template: '.arr.index(0)', 26 | input, 27 | output: 1, 28 | }, 29 | { 30 | template: '.arr.index(9)', 31 | input, 32 | output: 10, 33 | }, 34 | { 35 | template: '.arr.last()', 36 | input, 37 | output: 10, 38 | }, 39 | { 40 | template: '.arr.length()', 41 | input, 42 | output: 10, 43 | }, 44 | { 45 | template: '.arr.min()', 46 | input, 47 | output: 1, 48 | }, 49 | { 50 | template: '.arr.max()', 51 | input, 52 | output: 10, 53 | }, 54 | { 55 | template: '.arr.stddev()', 56 | input, 57 | output: 2.8722813232690143, 58 | }, 59 | { 60 | template: '.arr.sum()', 61 | input, 62 | output: 55, 63 | }, 64 | { 65 | input, 66 | output: { 67 | sum: 55, 68 | sum2: 55, 69 | avg: 5.5, 70 | min: 1, 71 | max: 10, 72 | stddev: 2.8722813232690143, 73 | length: 10, 74 | first: 1, 75 | last: 10, 76 | keys: ['foo', 'bar', 'baz', 'quux'], 77 | }, 78 | }, 79 | ]; 80 | -------------------------------------------------------------------------------- /test/scenarios/standard_functions/template.jt: -------------------------------------------------------------------------------- 1 | const arr = .arr; 2 | const obj = .obj; 3 | const keys = obj.keys(); 4 | { 5 | sum: .arr.sum(), 6 | sum2: (arr.index(0) + arr.index(-1)) * arr.length() / 2, 7 | avg: arr.avg(), 8 | min: arr.min(), 9 | max: arr.max(), 10 | stddev: arr.stddev(), 11 | length: arr.length(), 12 | first: arr.first(), 13 | last: arr.last(), 14 | keys, 15 | } -------------------------------------------------------------------------------- /test/scenarios/statements/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | output: [15], 6 | }, 7 | ]; 8 | -------------------------------------------------------------------------------- /test/scenarios/statements/template.jt: -------------------------------------------------------------------------------- 1 | // To statements in one line 2 | let a = 1; let b = 2 3 | // empty statements 4 | ;;; 5 | 6 | let c = [3] 7 | // block statement 8 | ( 9 | let d = 4; 10 | let fn = function(a){ 11 | 5*a 12 | } 13 | [a + b + c[0] + d + fn(1)] 14 | ) 15 | -------------------------------------------------------------------------------- /test/scenarios/template_strings/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | input: { 6 | a: 'foo', 7 | }, 8 | bindings: { 9 | b: 'bar', 10 | }, 11 | output: 'Input a=foo, Binding b=bar', 12 | }, 13 | { 14 | template: '`unclosed template ${`', 15 | error: 'Invalid template expression', 16 | }, 17 | { 18 | template: '`invalid template expression ${.a + }`', 19 | error: 'Invalid template expression', 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /test/scenarios/template_strings/template.jt: -------------------------------------------------------------------------------- 1 | let a = `Input a=${.a}`; 2 | let b = `Binding b=${$.b}`; 3 | `${a}, ${b}`; -------------------------------------------------------------------------------- /test/scenarios/throw/data.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../../types'; 2 | 3 | export const data: Scenario[] = [ 4 | { 5 | templatePath: 'statement_after_throw.jt', 6 | error: 7 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', 8 | }, 9 | { 10 | input: 3, 11 | error: 'num must be even', 12 | }, 13 | { 14 | input: 2, 15 | output: 1, 16 | }, 17 | { 18 | templatePath: 'throw_without_condition.jt', 19 | error: 20 | 'return, throw, continue and break statements are only allowed as last statements in conditional expressions', 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /test/scenarios/throw/statement_after_throw.jt: -------------------------------------------------------------------------------- 1 | .num % 2 !== 0 ? { 2 | throw new Error("num must be even"); 3 | let count = 0; 4 | } -------------------------------------------------------------------------------- /test/scenarios/throw/template.jt: -------------------------------------------------------------------------------- 1 | (. % 2 !== 0) ? throw new Error("num must be even") : ./2; -------------------------------------------------------------------------------- /test/scenarios/throw/throw_without_condition.jt: -------------------------------------------------------------------------------- 1 | throw new Error("num must be even"); -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import type { EngineOptions, FlatMappingPaths } from '../src'; 2 | 3 | export type Scenario = { 4 | description?: string; 5 | input?: unknown; 6 | templatePath?: string; 7 | mappings?: FlatMappingPaths[]; 8 | mappingsPath?: string; 9 | template?: string; 10 | options?: EngineOptions; 11 | bindings?: Record | undefined; 12 | output?: unknown; 13 | error?: string; 14 | }; 15 | 16 | export namespace Scenario { 17 | export function getTemplatePath(scenario: Scenario): string { 18 | return scenario.templatePath || scenario.mappingsPath || scenario.template || 'template.jt'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './scenario'; 2 | -------------------------------------------------------------------------------- /test/utils/scenario.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import { FlatMappingPaths, JsonTemplateEngine, PathType } from '../../src'; 4 | import { Scenario } from '../types'; 5 | 6 | function getTemplate(scenarioDir: string, scenario: Scenario): string { 7 | const templatePath = join(scenarioDir, Scenario.getTemplatePath(scenario)); 8 | return readFileSync(templatePath, 'utf-8'); 9 | } 10 | 11 | function getDefaultPathType(scenario: Scenario): PathType { 12 | return scenario.mappingsPath || scenario.mappings ? PathType.JSON : PathType.SIMPLE; 13 | } 14 | 15 | function initializeScenario(scenarioDir: string, scenario: Scenario) { 16 | scenario.options = scenario.options || {}; 17 | scenario.options.defaultPathType = 18 | scenario.options.defaultPathType || getDefaultPathType(scenario); 19 | 20 | if (scenario.mappingsPath) { 21 | scenario.mappings = JSON.parse(getTemplate(scenarioDir, scenario)) as FlatMappingPaths[]; 22 | } 23 | if (scenario.mappings) { 24 | scenario.template = JsonTemplateEngine.convertMappingsToTemplate( 25 | scenario.mappings as FlatMappingPaths[], 26 | scenario.options, 27 | ); 28 | } 29 | if (scenario.template === undefined) { 30 | scenario.template = getTemplate(scenarioDir, scenario); 31 | } 32 | scenario.template = JsonTemplateEngine.reverseTranslate( 33 | JsonTemplateEngine.parse(scenario.template, scenario.options), 34 | scenario.options, 35 | ); 36 | } 37 | 38 | export function evaluateScenario(scenarioDir: string, scenario: Scenario): any { 39 | initializeScenario(scenarioDir, scenario); 40 | if (scenario.mappingsPath) { 41 | return JsonTemplateEngine.evaluateAsSync( 42 | scenario.template, 43 | scenario.options, 44 | scenario.input, 45 | scenario.bindings, 46 | ); 47 | } 48 | return JsonTemplateEngine.evaluate( 49 | scenario.template, 50 | scenario.options, 51 | scenario.input, 52 | scenario.bindings, 53 | ); 54 | } 55 | 56 | export function extractScenarios(scenarioDir: string): Scenario[] { 57 | const { data } = require(join(scenarioDir, 'data.ts')); 58 | return data as Scenario[]; 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */, 18 | "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | "rootDir": "./src" /* Specify the root folder within your source files. */, 30 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 31 | "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [ 34 | // "./src", 35 | // "./test" 36 | // ] /* Allow multiple folders to be treated as one when resolving modules. */, 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | "resolveJsonModule": true /* Enable importing .json files. */, 42 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 48 | 49 | /* Emit */ 50 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 51 | "declarationMap": true /* Create sourcemaps for d.ts files. */, 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 55 | "outDir": "./build" /* Specify an output folder for all emitted files. */, 56 | // "removeComments": true, /* Disable emitting comments. */ 57 | // "noEmit": true, /* Disable emitting files from a compilation. */ 58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 66 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 67 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 70 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 72 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | "strict": true /* Enable all strict type-checking options. */, 83 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 84 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 89 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 105 | }, 106 | "exclude": ["node_modules", "**/test/*"], 107 | "include": ["src/**/*.ts", "src/**/*.json"] 108 | } 109 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { resolve } from 'path'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: resolve(__dirname, 'src/index.ts'), 9 | formats: ['es'], 10 | fileName: () => 'json-template.js', 11 | }, 12 | outDir: 'build', 13 | }, 14 | resolve: { alias: { src: resolve('src/') } }, 15 | }); 16 | --------------------------------------------------------------------------------