├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ └── compile-lint-test.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .npmignore ├── .prettierrc.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.ts ├── package-lock.json ├── package.json ├── scripts ├── service.ts └── transform.js ├── src ├── asl-utils │ ├── asl │ │ ├── asl.ts │ │ ├── definitions.ts │ │ └── tests │ │ │ ├── branch.test.ts │ │ │ ├── definitions.test.ts │ │ │ ├── getAllChildren.test.ts │ │ │ ├── next.test.ts │ │ │ └── visit.test.ts │ ├── index.ts │ └── utils │ │ ├── autocomplete.ts │ │ ├── jsonata │ │ ├── functions.ts │ │ ├── index.ts │ │ └── jsonata.ts │ │ ├── tests │ │ ├── assignVariableTestData.ts │ │ ├── autocomplete.test.ts │ │ ├── jsonata.test.ts │ │ └── utils.test.ts │ │ └── utils.ts ├── completion │ ├── completeAsl.ts │ ├── completeJSONata.ts │ ├── completeSnippets.ts │ ├── completeStateNames.ts │ ├── completeVariables.ts │ └── utils │ │ ├── jsonataUtils.ts │ │ └── variableUtils.ts ├── constants │ ├── constants.ts │ └── diagnosticStrings.ts ├── json-schema │ ├── bundled.json │ └── partial │ │ ├── base.json │ │ ├── choice_state.json │ │ ├── common.json │ │ ├── fail_state.json │ │ ├── map_state.json │ │ ├── parallel_state.json │ │ ├── pass_state.json │ │ ├── states.json │ │ ├── succeed_state.json │ │ ├── task_state.json │ │ └── wait_state.json ├── jsonLanguageService.ts ├── service.ts ├── snippets │ ├── error_handling.json │ └── states.json ├── tests │ ├── aslUtilityFunctions.test.ts │ ├── completion.test.ts │ ├── json-strings │ │ ├── completionStrings.ts │ │ ├── jsonataStrings.ts │ │ ├── validationStrings.ts │ │ └── variableStrings.ts │ ├── jsonSchemaAsl.test.ts │ ├── service.test.ts │ ├── utils │ │ └── testUtilities.ts │ ├── validation.test.ts │ ├── yaml-strings │ │ ├── completionStrings.ts │ │ └── validationStrings.ts │ ├── yamlCompletion.test.ts │ ├── yamlFormat.test.ts │ └── yamlValidation.test.ts ├── utils │ └── astUtilityFunctions.ts ├── validation │ ├── utils │ │ ├── getDiagnosticsForNode.ts │ │ └── getPropertyNodeDiagnostic.ts │ ├── validateProperties.ts │ ├── validateStates.ts │ └── validationSchema.ts ├── yaml │ ├── aslYamlLanguageService.ts │ └── yamlUtils.ts └── yamlLanguageService.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | module.exports = { 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | plugins: ['@typescript-eslint'], 13 | 14 | rules: { 15 | '@typescript-eslint/no-explicit-any': 'off', 16 | 'no-console': 'warn', 17 | '@typescript-eslint/no-unused-vars': [ 18 | 'warn', 19 | { 20 | caughtErrors: 'none', 21 | argsIgnorePattern: '^_', 22 | }, 23 | ], 24 | '@typescript-eslint/no-unused-expressions': [ 25 | 'error', 26 | { 27 | allowShortCircuit: true, 28 | allowTernary: true, 29 | }, 30 | ], 31 | }, 32 | ignorePatterns: ['*.js', '*.d.ts', 'node_modules/', 'src/tests/yaml-strings', 'src/tests/json-strings'], 33 | } 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Reference: https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "npm" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | labels: 10 | - "pr/dependabot" 11 | open-pull-requests-limit: 5 12 | -------------------------------------------------------------------------------- /.github/workflows/compile-lint-test.yml: -------------------------------------------------------------------------------- 1 | name: compile-lint-test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | # To avoid compatibility problems, pin the CI node.js version to node version used by VSCode Electron 16 | # so integration testing will not rely on a later version of node than exists in the default VS Code execution env. 17 | # Full table with details here: https://github.com/ewanharris/vscode-versions 18 | node-version: [20.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | dist-cjs 5 | build 6 | coverage -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,yml,json,md}": "prettier --write", 3 | "*.ts": ["eslint --fix", "prettier --write"] 4 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source folder 2 | src/ 3 | test/ 4 | scripts/ 5 | 6 | # config files 7 | coverage/ 8 | 9 | .vscode/ 10 | .github/ 11 | .husky/ 12 | 13 | .gitignore 14 | babel.config.js 15 | jest.config.js 16 | .prettierrc.json 17 | .lintstagedrc 18 | .eslintrc.js 19 | 20 | # tests and source map from build output 21 | build/ 22 | 23 | dist-cjs/tests/ 24 | dist-cjs/**/*.js.map 25 | dist-cjs/*/*/*.d.ts 26 | dist/**/*.js.map 27 | dist/tests/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "tabWidth": 2, 4 | "trailingComma": "all", 5 | "semi": false, 6 | "useTabs": false, 7 | "singleQuote": true, 8 | "printWidth": 120 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Unit Tests ASL Service", 7 | "type": "node", 8 | "request": "launch", 9 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 10 | "stopOnEntry": false, 11 | "args": [ 12 | "--timeout", 13 | "999999", 14 | "--colors" 15 | ], 16 | "cwd": "${workspaceRoot}", 17 | "runtimeExecutable": null, 18 | "runtimeArgs": [], 19 | "env": {}, 20 | "sourceMaps": true, 21 | "outFiles": [ "${workspaceRoot}/out/**/*.js", "${workspaceRoot}/out/*.js" ], 22 | "preLaunchTask": "npm: watch" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "tslint.enable": true, 4 | "typescript.tsc.autoDetect": "off", 5 | "typescript.preferences.quoteStyle": "single", 6 | "languageServerExample.trace.server": "verbose" 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": [ 13 | "$tsc" 14 | ] 15 | }, 16 | { 17 | "type": "npm", 18 | "script": "watch", 19 | "isBackground": true, 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | }, 24 | "presentation": { 25 | "panel": "dedicated", 26 | "reveal": "never" 27 | }, 28 | "problemMatcher": [ 29 | "$tsc-watch" 30 | ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | ## Getting Started 47 | ### Setup 48 | To work on this project you need these environmental dependencies: 49 | - [NodeJS and NPM](https://nodejs.org/) (latest version of both) 50 | - [Git](https://git-scm.com/downloads) 51 | 52 | Then clone the repository and install the project dependencies with NPM: 53 | 54 | ``` 55 | git clone git@github.com:aws/amazon-states-language-service.git 56 | cd amazon-states-language-service 57 | npm install 58 | npm test 59 | ``` 60 | 61 | ### Link with AWS Toolkit for VS Code 62 | The [AWS Toolkit for Visual Studio 63 | Code](https://github.com/aws/aws-toolkit-vscode) extension uses the 64 | `amazon-states-language-service` language service to provide syntax validation & 65 | autocomplete for state machines. 66 | 67 | If you want to test or troubleshoot changes you are making to this service 68 | locally with the Visual Studio Code extension, this is how you link the two 69 | repos locally. 70 | 71 | 1) In your `amazon-states-language-service` repo root run: 72 | ``` 73 | npm link 74 | ``` 75 | 76 | This command makes the current project directory available for linking. 77 | 78 | 79 | 2) In your `aws-toolkit-vscode` repo root, run this: 80 | ``` 81 | npm link amazon-states-language-service 82 | ``` 83 | 84 | This command links the current project to the local language service. 85 | 86 | Optionally, if you want to verify that the `amazon-states-language-service` 87 | dependency points to your local disk location rather than the live service, you 88 | can check with this command: 89 | 90 | ``` 91 | npm ls amazon-states-language-service 92 | ``` 93 | 94 | ### Run 95 | You can now run the `aws-toolkit-vscode` extension from Visual Studio Code while 96 | calling your local development version of `amazon-states-language-service`: 97 | 98 | 1. Select the Run panel from the sidebar. 99 | 2. From the dropdown at the top of the Run pane, choose `Extension`. 100 | 3. Press `F5` to launch a new instance of Visual Studio Code with the extension 101 | installed and the debugger attached. 102 | 4. At this point `aws-toolkit-vscode` is using your local copy of the 103 | `amazon-states-language-service` on disk. 104 | 5. If you want to debug/step-through the attached service, in this same instance 105 | of VS Code with `aws-toolkit-vscode` open and running, go to the dropdown at 106 | the top of the `Run` pane and select `Attach to ASL Server`. 107 | 108 | If you want to reset your `aws-toolkit-vscode` repo to use the live 109 | `amazon-states-language` rather than the local development copy, navigate to 110 | the `aws-toolkit-vscode` repo and do this: 111 | 112 | ```console 113 | npm unlink amazon-states-language 114 | ``` 115 | 116 | ## Code of Conduct 117 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 118 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 119 | opensource-codeofconduct@amazon.com with any additional questions or comments. 120 | 121 | 122 | ## Security issue notifications 123 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 124 | 125 | 126 | ## Licensing 127 | 128 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 129 | 130 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Amazon States Language Service 2 | 3 | Amazon States Language Service is a wrapper around [vscode-json-languageservice](https://github.com/microsoft/vscode-json-languageservice). It extends its functionality with validation, completions and snippets specific to amazon states language. It preserves its interface and can be used as a drop-in replacement for it. 4 | 5 | ## License 6 | 7 | This library is licensed under the MIT License. See the LICENSE file. 8 | 9 | 10 | 11 | ![Build Status](https://codebuild.us-west-2.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoidVBIMnJWT0tzQ1E3ZmhGZGdyWXdQNWdVc01OVEp1bXBrUlhIOXdROUdsMkM0dVFNcVJzc242anFkdFg2WFdDNTh2OTdnbkNCSGYwdmJ3cVdCZ1gzNjQ4PSIsIml2UGFyYW1ldGVyU3BlYyI6IkV4UVlhZU9EWW5WWkhsNDUiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master) 12 | 13 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }]], 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 7 | 8 | const transformCodeInPackages = [] 9 | 10 | module.exports = { 11 | preset: 'ts-jest', 12 | testMatch: ['**/src/**/?(*.)+(spec|test).ts'], 13 | 14 | coverageReporters: ['cobertura', 'html', 'text'], 15 | collectCoverageFrom: ['src/**/*.ts'], 16 | coveragePathIgnorePatterns: ['^.*/scripts/.*$', '^.*/schema/.*$', '^.*/tests/.*$', 'interface.ts'], 17 | transformIgnorePatterns: [`/node_modules/(?!${transformCodeInPackages.join('|')}).+\\.js$`], 18 | transform: { 19 | '^.+\\.(ts|tsx)?$': 'ts-jest', 20 | '^.+\\.(js|jsx)$': [ 21 | 'babel-jest', 22 | { 23 | presets: ['@babel/preset-env'], 24 | plugins: [['@babel/transform-runtime']], 25 | }, 26 | ], 27 | }, 28 | clearMocks: true, 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-states-language-service", 3 | "description": "Amazon States Language Service", 4 | "author": "Amazon Web Services", 5 | "main": "dist-cjs/service.js", 6 | "module": "dist/service.js", 7 | "types": "dist/service.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/aws/amazon-states-language-service" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/aws/amazon-states-language-service" 14 | }, 15 | "license": "MIT", 16 | "version": "1.16.1", 17 | "publisher": "aws", 18 | "categories": [ 19 | "Programming Languages" 20 | ], 21 | "scripts": { 22 | "prepare": "husky", 23 | "prettify": "prettier 'src/**/*' --write", 24 | "prepublish": "npm run compile", 25 | "clean": "rm -rf build dist-cjs dist node_modules", 26 | "lint": "eslint --ext .ts .", 27 | "lint:fix": "eslint --ext .ts . --fix", 28 | "compile": "npm run lint && rm -rf dist-cjs/* dist/* && npm run bundle-schema && npm run build:cjs && npm run build:esm", 29 | "build:esm": "tsc --outDir dist --module esnext --target esnext --moduleResolution bundler", 30 | "build:cjs": "tsc --outDir dist-cjs --module commonjs", 31 | "watch": "tsc -b -w", 32 | "test-watch": "YAML_SERVICE=enabled jest --watch", 33 | "test": "jest --collectCoverage", 34 | "bundle-schema": "node ./scripts/transform.js && prettier src/json-schema/ --write" 35 | }, 36 | "devDependencies": { 37 | "@apidevtools/json-schema-ref-parser": "^11.7.3", 38 | "@babel/plugin-transform-runtime": "^7.25.9", 39 | "@babel/preset-env": "^7.26.0", 40 | "@types/jest": "^29.5.14", 41 | "@types/js-yaml": "^4.0.5", 42 | "@types/node": "^20.2.5", 43 | "@typescript-eslint/eslint-plugin": "^8.20.0", 44 | "babel-jest": "^29.7.0", 45 | "eslint": "^8.57.1", 46 | "husky": "^9.1.7", 47 | "jest": "^29.7.0", 48 | "lint-staged": "^15.4.0", 49 | "prettier": "^3.4.2", 50 | "ts-jest": "^29.2.3", 51 | "ts-node": "^10.9.2", 52 | "typescript": "^5.0.4" 53 | }, 54 | "dependencies": { 55 | "js-yaml": "^4.1.0", 56 | "jsonata": "2.0.5", 57 | "lodash": "^4.17.21", 58 | "vscode-json-languageservice": "3.4.9", 59 | "vscode-languageserver": "^9.0.0", 60 | "vscode-languageserver-textdocument": "^1.0.0", 61 | "vscode-languageserver-types": "^3.17.5", 62 | "yaml-language-server": "0.15.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scripts/service.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { 7 | Diagnostic, 8 | DiagnosticSeverity, 9 | getLanguageService as getLanguageServiceVscode, 10 | JSONSchema, 11 | LanguageService, 12 | LanguageServiceParams, 13 | } from 'vscode-json-languageservice' 14 | 15 | import aslSchema from './json-schema/bundled.json' 16 | 17 | import { ASLOptions, ASTTree, isObjectNode } from './utils/astUtilityFunctions' 18 | 19 | import completeAsl from './completion/completeAsl' 20 | import { LANGUAGE_IDS } from './constants/constants' 21 | import validateStates, { RootType } from './validation/validateStates' 22 | 23 | export * from 'vscode-json-languageservice' 24 | export * from './yaml/yamlLanguageService' 25 | 26 | export interface ASLLanguageServiceParams extends LanguageServiceParams { 27 | aslOptions?: ASLOptions 28 | } 29 | 30 | export const ASL_SCHEMA = aslSchema as JSONSchema 31 | export const doCompleteAsl = completeAsl 32 | 33 | export const getLanguageService = function (params: ASLLanguageServiceParams): LanguageService { 34 | const builtInParams = {} 35 | 36 | const languageService = getLanguageServiceVscode({ ...params, ...builtInParams }) 37 | const doValidation = languageService.doValidation.bind(languageService) as typeof languageService.doValidation 38 | const doComplete = languageService.doComplete.bind(languageService) as typeof languageService.doComplete 39 | 40 | languageService.configure({ 41 | validate: true, 42 | allowComments: false, 43 | schemas: [ 44 | { 45 | uri: LANGUAGE_IDS.JSON, 46 | fileMatch: ['*'], 47 | schema: aslSchema as JSONSchema, 48 | }, 49 | ], 50 | }) 51 | 52 | languageService.doValidation = async function (document, jsonDocument, documentSettings) { 53 | // vscode-json-languageservice will always set severity as warning for JSONSchema validation 54 | // there is no option to configure this behavior so severity needs to be overwritten as error 55 | const diagnostics = (await doValidation(document, jsonDocument, documentSettings)).map((diagnostic) => { 56 | // Non JSON Schema validation will have source: 'asl' 57 | if (diagnostic.source !== LANGUAGE_IDS.JSON) { 58 | return { ...diagnostic, severity: DiagnosticSeverity.Error } 59 | } 60 | 61 | return diagnostic 62 | }) as Diagnostic[] 63 | 64 | const rootNode = (jsonDocument as ASTTree).root 65 | 66 | if (rootNode && isObjectNode(rootNode)) { 67 | const aslDiagnostics = validateStates(rootNode, document, RootType.Root, params.aslOptions) 68 | 69 | return diagnostics.concat(aslDiagnostics) 70 | } 71 | 72 | return diagnostics 73 | } 74 | 75 | languageService.doComplete = async function (document, position, doc) { 76 | const jsonCompletions = await doComplete(document, position, doc) 77 | 78 | return completeAsl(document, position, doc, jsonCompletions, params.aslOptions) 79 | } 80 | 81 | return languageService 82 | } 83 | -------------------------------------------------------------------------------- /scripts/transform.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | const $RefParser = require('@apidevtools/json-schema-ref-parser') 7 | const path = require('path') 8 | const fs = require('fs') 9 | const util = require('util') 10 | 11 | const writeFile = util.promisify(fs.writeFile) 12 | 13 | const SCHEMA_PATH = '../src/json-schema/' 14 | const BUNDLED_FILE_NAME = 'bundled.json' 15 | 16 | async function parseSchema() { 17 | try { 18 | const bundled = await $RefParser.bundle(path.resolve(__dirname, SCHEMA_PATH, 'partial/base.json')) 19 | const bundledJSON = JSON.stringify(bundled, null, 2) 20 | 21 | writeFile(path.resolve(__dirname, SCHEMA_PATH, BUNDLED_FILE_NAME), bundledJSON) 22 | } catch (err) { 23 | console.log(err) 24 | } 25 | } 26 | 27 | parseSchema() 28 | -------------------------------------------------------------------------------- /src/asl-utils/asl/asl.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { 7 | Asl, 8 | AslWithStates, 9 | DistributedMapState, 10 | getProcessorDefinition, 11 | isAslWithStates, 12 | isChoice, 13 | isMap, 14 | isParallel, 15 | isTerminal, 16 | NextOrEnd, 17 | StateDefinition, 18 | StateId, 19 | } from './definitions' 20 | import { lastItem } from '../utils/utils' 21 | 22 | export type StateIdOrBranchIndex = StateId | number 23 | 24 | /** 25 | * Path to a state. 26 | * Each item can be a key (string) or item index (number). 27 | */ 28 | export type StatePath = [] | [...StateIdOrBranchIndex[], StateId] 29 | 30 | /** 31 | * Path to a branch. 32 | */ 33 | export type BranchPath = [] | StateIdOrBranchIndex[] 34 | 35 | export type StateAddress = { 36 | state: T | null 37 | path: StatePath | null 38 | parent: AslWithStates | null 39 | parentPath: BranchPath | null 40 | } 41 | 42 | export type LocatedState = { 43 | state: T 44 | parent: AslWithStates 45 | path: StatePath 46 | } 47 | 48 | export const STATE_NOT_FOUND = { state: null, parent: null, path: null, parentPath: null } 49 | 50 | /** 51 | * A function which is called for each state of an ASL. 52 | * 53 | * @return true to indicate visiting should continue, false to stop further visiting 54 | * @see {@link visitAllStates}. 55 | */ 56 | type StateVisitor = ( 57 | id: StateId, 58 | state: T, 59 | parent: AslWithStates, 60 | path: StatePath, 61 | ) => boolean 62 | 63 | /** 64 | * Visits all States in the given ASL and for each one calls fn with id, state and parent parameters. 65 | * When fn returns false the visiting stops. 66 | */ 67 | export function visitAllStates(asl: Asl, fn: StateVisitor): void { 68 | if (asl.States) { 69 | visitAllStatesInBranch(asl, [], fn) 70 | } 71 | } 72 | 73 | /** 74 | * In the scope of an ASL searches for the given stateId and returns the state and the state's parent. 75 | * parent is the branch which contains the state (i.e. you can find the given id in parent.States). 76 | */ 77 | export function findStateById( 78 | asl: Asl, 79 | stateId: StateId, 80 | branchPath: BranchPath = [], 81 | ): StateAddress { 82 | if (!isAslWithStates(asl)) { 83 | return STATE_NOT_FOUND 84 | } 85 | 86 | const state = asl.States[stateId] 87 | if (state) { 88 | return { state, parent: asl, parentPath: branchPath, path: [...branchPath, stateId] } 89 | } 90 | 91 | for (const [childStateId, childState] of Object.entries(asl.States)) { 92 | if (isMap(childState)) { 93 | const iteratorProcessor = getProcessorDefinition>(childState as DistributedMapState) 94 | 95 | const result = findStateById(iteratorProcessor, stateId, [...branchPath, childStateId]) 96 | if (result.state != null) { 97 | return result 98 | } 99 | } 100 | 101 | if (isParallel(childState) && childState.Branches) { 102 | for (let branchIndex = 0; branchIndex < childState.Branches.length; branchIndex++) { 103 | const branch = childState.Branches[branchIndex] 104 | const result = findStateById(branch, stateId, [...branchPath, childStateId, branchIndex]) 105 | if (result.state != null) { 106 | return result 107 | } 108 | } 109 | } 110 | } 111 | 112 | return STATE_NOT_FOUND 113 | } 114 | 115 | /** 116 | * Returns all states which are children of the given stateId at any depth. 117 | */ 118 | export function getAllChildren(asl: Asl, stateId: StateId): StateId[] { 119 | const { state } = findStateById(asl, stateId) 120 | if (state === null) { 121 | return [] 122 | } 123 | 124 | return getAllChildrenOfState(state) 125 | } 126 | 127 | function getAllChildrenOfState(state: StateDefinition | Asl): StateId[] { 128 | const result: StateId[] = [] 129 | 130 | if ('States' in state && state.States) { 131 | for (const childId of Object.keys(state.States)) { 132 | result.push(childId) 133 | const childState: StateDefinition = state.States[childId] 134 | result.push(...getAllChildrenOfState(childState)) 135 | } 136 | } 137 | 138 | if ('Branches' in state && state.Branches) { 139 | // parallel 140 | for (const branch of state.Branches) { 141 | result.push(...getAllChildrenOfState(branch)) 142 | } 143 | } 144 | 145 | if ('Iterator' in state && state.Iterator) { 146 | result.push(...getAllChildrenOfState(state.Iterator)) 147 | } 148 | 149 | if ('ItemProcessor' in state && state.ItemProcessor) { 150 | result.push(...getAllChildrenOfState(state.ItemProcessor)) 151 | } 152 | 153 | return result 154 | } 155 | 156 | /** 157 | * Returns the state which is directly after the given one. 158 | * For ErrorHandled states it means the Next (or End) property as opposed to any Catches 159 | * For Choice it is Default or if missing the first choice rule's Next 160 | */ 161 | export function getDirectNext(state: StateDefinition): NextOrEnd { 162 | if (isTerminal(state)) { 163 | return null 164 | } 165 | 166 | if (isChoice(state)) { 167 | if (state.Default) { 168 | return state.Default 169 | } 170 | 171 | if (state.Choices && state.Choices.length >= 1) { 172 | return state.Choices[0].Next || null 173 | } 174 | 175 | return null 176 | } 177 | 178 | return state.Next || null 179 | } 180 | 181 | /** 182 | * Returns all the state ids anywhere in the given ASL. 183 | */ 184 | export function getAllStateIds(asl: Asl): StateId[] { 185 | const result: StateId[] = [] 186 | visitAllStates(asl, (id) => { 187 | result.push(id) 188 | return true 189 | }) 190 | return result 191 | } 192 | 193 | export function isParallelBranch(path: BranchPath | null): boolean { 194 | return path !== null && path.length > 1 && typeof lastItem(path) === 'number' 195 | } 196 | 197 | export function visitAllStatesInBranch( 198 | parent: Asl, 199 | partialPath: StateIdOrBranchIndex[], 200 | fn: StateVisitor, 201 | ): boolean { 202 | if (isAslWithStates(parent)) { 203 | for (const [id, state] of Object.entries(parent.States)) { 204 | const path: StatePath = [...partialPath, id] 205 | // returning false indicates halting the visit to the rest 206 | const shouldContinueTheVisit = fn(id, state, parent, path) 207 | if (!shouldContinueTheVisit) { 208 | return false 209 | } 210 | 211 | if ('Iterator' in state && state.Iterator && state.Iterator.States) { 212 | const shouldContinue = visitAllStatesInBranch(state.Iterator as T, path, fn) 213 | if (!shouldContinue) { 214 | return false 215 | } 216 | } 217 | if ('ItemProcessor' in state && state.ItemProcessor && state.ItemProcessor.States) { 218 | const shouldContinue = visitAllStatesInBranch(state.ItemProcessor as T, path, fn) 219 | if (!shouldContinue) { 220 | return false 221 | } 222 | } 223 | 224 | if ('Branches' in state && state.Branches) { 225 | for (let branchIndex = 0; branchIndex < state.Branches.length; branchIndex++) { 226 | const branch = state.Branches[branchIndex] 227 | const shouldContinue = visitAllStatesInBranch(branch as T, [...path, branchIndex], fn) 228 | if (!shouldContinue) { 229 | return false 230 | } 231 | } 232 | } 233 | } 234 | } 235 | 236 | // true means continue visiting other states 237 | return true 238 | } 239 | 240 | /** 241 | * Get state ID given a branch path 242 | */ 243 | export function getStateIdFromBranchPath(path: BranchPath): StateId | undefined { 244 | for (let i = path.length - 1; i >= 0; i--) { 245 | const stateIdOrIndex = path[i] 246 | if (typeof stateIdOrIndex === 'string') { 247 | return stateIdOrIndex 248 | } 249 | } 250 | } 251 | 252 | /** 253 | * Get branch index given the branch name 254 | * @param branchName Branches[3] 255 | * @returns 3 256 | */ 257 | export function getBranchIndex(branchName: string): number | null { 258 | const pattern = /^Branches\[(\d+)\]$/ 259 | const match = branchName.match(pattern) 260 | if (match) { 261 | return parseInt(match[1], 10) 262 | } 263 | return null 264 | } 265 | -------------------------------------------------------------------------------- /src/asl-utils/asl/tests/branch.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { getBranchIndex, getStateIdFromBranchPath } from '../asl' 7 | 8 | describe('getStateIdFromBranchPath', () => { 9 | it('should return stateId from a Parallel branch path', () => { 10 | expect(getStateIdFromBranchPath(['ParallelState', 2])).toBe('ParallelState') 11 | }) 12 | 13 | it('should return stateId from a branch path', () => { 14 | expect(getStateIdFromBranchPath(['StateName'])).toBe('StateName') 15 | }) 16 | }) 17 | 18 | describe('getBranchIndex', () => { 19 | it('should return the branch index from the branch name', () => { 20 | expect(getBranchIndex('Branches[0]')).toEqual(0) 21 | expect(getBranchIndex('Branches[10]')).toEqual(10) 22 | }) 23 | 24 | it('should return null when it is not valid branch name', () => { 25 | expect(getBranchIndex('Branches')).toBeNull() 26 | expect(getBranchIndex('Branches[]')).toBeNull() 27 | expect(getBranchIndex('Branch[0]')).toBeNull() 28 | expect(getBranchIndex('branches[0]')).toBeNull() 29 | expect(getBranchIndex('')).toBeNull() 30 | expect(getBranchIndex('Branches[-1]')).toBeNull() 31 | expect(getBranchIndex('Branches[1.1]')).toBeNull() 32 | expect(getBranchIndex('Branches(0)')).toBeNull() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/asl-utils/asl/tests/definitions.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { 7 | DistributedMapProcessingMode, 8 | getItemSelectorDefinition, 9 | getItemSelectorFieldName, 10 | getProcessorDefinition, 11 | getProcessorFieldName, 12 | isAslWithStates, 13 | isChoice, 14 | isChoiceWithChoices, 15 | isDistributedMap, 16 | isErrorHandled, 17 | isFail, 18 | isMap, 19 | isMapWithIterator, 20 | isNonTerminal, 21 | isParallel, 22 | isParallelWithBranches, 23 | isWithErrorHandled, 24 | isWithVariables, 25 | isPass, 26 | isPlaceholder, 27 | isSucceed, 28 | isTask, 29 | isTerminal, 30 | isWait, 31 | isWithParameters, 32 | } from '../definitions' 33 | 34 | describe('ASL definition type guards', () => { 35 | it('checks state types correctly', () => { 36 | // TODO: Separate each function into its own 'it' block 37 | expect(isPass({ Type: 'Pass' })).toBeTruthy() 38 | expect(isWait({ Type: 'Wait' })).toBeTruthy() 39 | expect(isFail({ Type: 'Fail' })).toBeTruthy() 40 | expect(isSucceed({ Type: 'Succeed' })).toBeTruthy() 41 | expect(isTask({ Type: 'Task' })).toBeTruthy() 42 | expect(isChoice({ Type: 'Choice' })).toBeTruthy() 43 | expect(isMap({ Type: 'Map' })).toBeTruthy() 44 | expect(isParallel({ Type: 'Parallel' })).toBeTruthy() 45 | expect(isPlaceholder({ Type: 'Placeholder', PlaceholderLabel: 'x' })).toBeTruthy() 46 | 47 | expect(isPass({ Type: 'Map' })).toBeFalsy() 48 | expect(isWait({ Type: 'Map' })).toBeFalsy() 49 | expect(isFail({ Type: 'Map' })).toBeFalsy() 50 | expect(isSucceed({ Type: 'Map' })).toBeFalsy() 51 | expect(isTask({ Type: 'Map' })).toBeFalsy() 52 | expect(isChoice({ Type: 'Map' })).toBeFalsy() 53 | expect(isMap({ Type: 'Parallel' })).toBeFalsy() 54 | expect(isParallel({ Type: 'Map' })).toBeFalsy() 55 | expect(isPlaceholder({ Type: 'Map' })).toBeFalsy() 56 | 57 | expect(isTerminal({ Type: 'Fail' })).toBeTruthy() 58 | expect(isTerminal({ Type: 'Succeed' })).toBeTruthy() 59 | expect(isTerminal({ Type: 'Pass' })).toBeFalsy() 60 | expect(isNonTerminal({ Type: 'Pass' })).toBeTruthy() 61 | 62 | expect(isErrorHandled({ Type: 'Map' })).toBeTruthy() 63 | expect(isErrorHandled({ Type: 'Task' })).toBeTruthy() 64 | expect(isErrorHandled({ Type: 'Parallel' })).toBeTruthy() 65 | expect(isErrorHandled({ Type: 'Pass' })).toBeFalsy() 66 | 67 | expect(isWithParameters({ Type: 'Map' })).toBeTruthy() 68 | expect(isWithParameters({ Type: 'Task' })).toBeTruthy() 69 | expect(isWithParameters({ Type: 'Parallel' })).toBeTruthy() 70 | expect(isWithParameters({ Type: 'Pass' })).toBeTruthy() 71 | expect(isWithParameters({ Type: 'Succeed' })).toBeFalsy() 72 | 73 | expect(isAslWithStates(null)).toBeFalsy() 74 | expect(isAslWithStates(undefined)).toBeFalsy() 75 | expect(isAslWithStates({})).toBeFalsy() 76 | expect(isAslWithStates({ States: {} })).toBeTruthy() 77 | 78 | expect(isChoiceWithChoices(null)).toBeFalsy() 79 | expect(isChoiceWithChoices(undefined)).toBeFalsy() 80 | expect(isChoiceWithChoices({ Type: 'Choice' })).toBeFalsy() 81 | expect(isChoiceWithChoices({ Type: 'Pass' })).toBeFalsy() 82 | expect(isChoiceWithChoices({ Type: 'Choice', Choices: [] })).toBeTruthy() 83 | 84 | expect(isMapWithIterator(null)).toBeFalsy() 85 | expect(isMapWithIterator(undefined)).toBeFalsy() 86 | expect(isMapWithIterator({ Type: 'Map' })).toBeFalsy() 87 | expect(isMapWithIterator({ Type: 'Pass' })).toBeFalsy() 88 | expect(isMapWithIterator({ Type: 'Map', Iterator: {} })).toBeTruthy() 89 | 90 | expect(isParallelWithBranches(null)).toBeFalsy() 91 | expect(isParallelWithBranches(undefined)).toBeFalsy() 92 | expect(isParallelWithBranches({ Type: 'Parallel' })).toBeFalsy() 93 | expect(isParallelWithBranches({ Type: 'Pass' })).toBeFalsy() 94 | expect(isParallelWithBranches({ Type: 'Parallel', Branches: [] })).toBeTruthy() 95 | 96 | expect(isWithVariables({ Type: 'Map' })).toBeTruthy() 97 | expect(isWithVariables({ Type: 'Task' })).toBeTruthy() 98 | expect(isWithVariables({ Type: 'Choice' })).toBeTruthy() 99 | expect(isWithVariables({ Type: 'Parallel' })).toBeTruthy() 100 | expect(isWithVariables({ Type: 'Pass' })).toBeTruthy() 101 | expect(isWithVariables({ Type: 'Wait' })).toBeTruthy() 102 | expect(isWithVariables({ Type: 'Succeed' })).toBeFalsy() 103 | expect(isWithVariables({ Type: 'Fail' })).toBeFalsy() 104 | 105 | expect(isWithErrorHandled({ Type: 'Map' })).toBeTruthy() 106 | expect(isWithErrorHandled({ Type: 'Task' })).toBeTruthy() 107 | expect(isWithErrorHandled({ Type: 'Choice' })).toBeFalsy() 108 | expect(isWithErrorHandled({ Type: 'Map' })).toBeTruthy() 109 | expect(isWithErrorHandled({ Type: 'Pass' })).toBeFalsy() 110 | }) 111 | it('getProcessorDefinition', () => { 112 | expect( 113 | getProcessorDefinition({ 114 | Type: 'Map', 115 | ItemProcessor: { 116 | ProcessorConfig: { 117 | Mode: DistributedMapProcessingMode.Distributed, 118 | }, 119 | StartAt: 'parallel-b-3-1', 120 | States: { 121 | 'parallel-b-3-1': { 122 | Type: 'Pass', 123 | End: true, 124 | }, 125 | }, 126 | }, 127 | }), 128 | ).toStrictEqual({ 129 | ProcessorConfig: { 130 | Mode: DistributedMapProcessingMode.Distributed, 131 | }, 132 | StartAt: 'parallel-b-3-1', 133 | States: { 134 | 'parallel-b-3-1': { 135 | Type: 'Pass', 136 | End: true, 137 | }, 138 | }, 139 | }) 140 | expect( 141 | getProcessorDefinition({ 142 | Type: 'Map', 143 | Iterator: { 144 | ProcessorConfig: { 145 | Mode: DistributedMapProcessingMode.Distributed, 146 | }, 147 | StartAt: 'parallel-b-3-1', 148 | States: { 149 | 'parallel-b-3-1': { 150 | Type: 'Pass', 151 | End: true, 152 | }, 153 | }, 154 | }, 155 | }), 156 | ).toStrictEqual({ 157 | ProcessorConfig: { 158 | Mode: DistributedMapProcessingMode.Distributed, 159 | }, 160 | StartAt: 'parallel-b-3-1', 161 | States: { 162 | 'parallel-b-3-1': { 163 | Type: 'Pass', 164 | End: true, 165 | }, 166 | }, 167 | }) 168 | }) 169 | 170 | it('getItemSelectorDefinition', () => { 171 | expect( 172 | getItemSelectorDefinition({ 173 | Type: 'Map', 174 | ItemProcessor: { 175 | ProcessorConfig: { 176 | Mode: DistributedMapProcessingMode.Distributed, 177 | }, 178 | StartAt: 'parallel-b-3-1', 179 | States: { 180 | 'parallel-b-3-1': { 181 | Type: 'Pass', 182 | End: true, 183 | }, 184 | }, 185 | }, 186 | ItemSelector: { 187 | a: 'a', 188 | }, 189 | }), 190 | ).toStrictEqual({ 191 | a: 'a', 192 | }) 193 | }) 194 | 195 | expect( 196 | getItemSelectorDefinition({ 197 | Type: 'Map', 198 | Iterator: { 199 | ProcessorConfig: { 200 | Mode: DistributedMapProcessingMode.Distributed, 201 | }, 202 | StartAt: 'parallel-b-3-1', 203 | States: { 204 | 'parallel-b-3-1': { 205 | Type: 'Pass', 206 | End: true, 207 | }, 208 | }, 209 | }, 210 | Parameters: { 211 | a: 'a', 212 | }, 213 | }), 214 | ).toStrictEqual({ 215 | a: 'a', 216 | }) 217 | 218 | it('getItemSelectorFieldName', () => { 219 | expect( 220 | getItemSelectorFieldName({ 221 | Type: 'Map', 222 | ItemProcessor: { 223 | ProcessorConfig: { 224 | Mode: DistributedMapProcessingMode.Distributed, 225 | }, 226 | StartAt: 'parallel-b-3-1', 227 | States: { 228 | 'parallel-b-3-1': { 229 | Type: 'Pass', 230 | End: true, 231 | }, 232 | }, 233 | }, 234 | ItemSelector: { 235 | a: 'a', 236 | }, 237 | }), 238 | ).toStrictEqual('ItemSelector') 239 | }) 240 | 241 | expect( 242 | getItemSelectorFieldName({ 243 | Type: 'Map', 244 | Iterator: { 245 | ProcessorConfig: { 246 | Mode: DistributedMapProcessingMode.Distributed, 247 | }, 248 | StartAt: 'parallel-b-3-1', 249 | States: { 250 | 'parallel-b-3-1': { 251 | Type: 'Pass', 252 | End: true, 253 | }, 254 | }, 255 | }, 256 | Parameters: { 257 | a: 'a', 258 | }, 259 | }), 260 | ).toStrictEqual('Parameters') 261 | 262 | it('isDistributedMap', () => { 263 | expect( 264 | isDistributedMap({ 265 | Type: 'Map', 266 | Iterator: { 267 | ProcessorConfig: { 268 | Mode: DistributedMapProcessingMode.Distributed, 269 | }, 270 | StartAt: 'parallel-b-3-1', 271 | States: { 272 | 'parallel-b-3-1': { 273 | Type: 'Pass', 274 | End: true, 275 | }, 276 | }, 277 | }, 278 | }), 279 | ).toBe(true) 280 | expect( 281 | isDistributedMap({ 282 | Type: 'Map', 283 | Iterator: { 284 | StartAt: 'parallel-b-3-1', 285 | States: { 286 | 'parallel-b-3-1': { 287 | Type: 'Pass', 288 | End: true, 289 | }, 290 | }, 291 | }, 292 | }), 293 | ).toBe(true) 294 | }) 295 | it('getProcessorFieldName', () => { 296 | expect( 297 | getProcessorFieldName({ 298 | Type: 'Map', 299 | Iterator: { 300 | ProcessorConfig: { 301 | Mode: DistributedMapProcessingMode.Distributed, 302 | }, 303 | StartAt: 'parallel-b-3-1', 304 | States: { 305 | 'parallel-b-3-1': { 306 | Type: 'Pass', 307 | End: true, 308 | }, 309 | }, 310 | }, 311 | }), 312 | ).toBe('Iterator') 313 | expect( 314 | getProcessorFieldName({ 315 | Type: 'Map', 316 | ItemProcessor: { 317 | ProcessorConfig: { 318 | Mode: DistributedMapProcessingMode.Distributed, 319 | }, 320 | StartAt: 'parallel-b-3-1', 321 | States: { 322 | 'parallel-b-3-1': { 323 | Type: 'Pass', 324 | End: true, 325 | }, 326 | }, 327 | }, 328 | }), 329 | ).toBe('ItemProcessor') 330 | }) 331 | }) 332 | -------------------------------------------------------------------------------- /src/asl-utils/asl/tests/getAllChildren.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Asl } from '../definitions' 7 | import { getAllChildren } from '../asl' 8 | 9 | describe('getAllChildren', () => { 10 | it('returns empty when state not found', () => { 11 | const asl: Asl = { 12 | StartAt: 'a', 13 | States: { 14 | a: { 15 | Type: 'Pass', 16 | End: true, 17 | }, 18 | }, 19 | } 20 | 21 | const children = getAllChildren(asl, 'b') 22 | 23 | expect(children).toEqual([]) 24 | }) 25 | 26 | it('returns children', () => { 27 | const asl: Asl = { 28 | StartAt: 'a', 29 | States: { 30 | a: { 31 | Type: 'Map', 32 | Iterator: { 33 | StartAt: 'a-1', 34 | States: { 35 | 'a-1': { 36 | Type: 'Pass', 37 | Next: 'a-2', 38 | }, 39 | 'a-2': { 40 | Type: 'Parallel', 41 | Branches: [ 42 | { 43 | StartAt: 'a-2-1', 44 | States: { 45 | 'a-2-1': { 46 | Type: 'Pass', 47 | End: true, 48 | }, 49 | }, 50 | }, 51 | { 52 | StartAt: 'a-2-2', 53 | States: { 54 | 'a-2-2': { 55 | Type: 'Pass', 56 | End: true, 57 | }, 58 | }, 59 | }, 60 | ], 61 | End: true, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | } 68 | 69 | const children = getAllChildren(asl, 'a') 70 | 71 | expect(children).toStrictEqual(['a-1', 'a-2', 'a-2-1', 'a-2-2']) 72 | }) 73 | 74 | it('returns all children of a distributed Map', () => { 75 | const asl: Asl = { 76 | StartAt: 'a', 77 | States: { 78 | a: { 79 | Type: 'Map', 80 | ItemProcessor: { 81 | StartAt: 'a-1', 82 | States: { 83 | 'a-1': { 84 | Type: 'Pass', 85 | Next: 'a-2', 86 | }, 87 | 'a-2': { 88 | Type: 'Parallel', 89 | Branches: [ 90 | { 91 | StartAt: 'a-2-1', 92 | States: { 93 | 'a-2-1': { 94 | Type: 'Pass', 95 | End: true, 96 | }, 97 | }, 98 | }, 99 | { 100 | StartAt: 'a-2-2', 101 | States: { 102 | 'a-2-2': { 103 | Type: 'Pass', 104 | End: true, 105 | }, 106 | }, 107 | }, 108 | ], 109 | End: true, 110 | }, 111 | }, 112 | }, 113 | }, 114 | }, 115 | } 116 | 117 | const children = getAllChildren(asl, 'a') 118 | 119 | expect(children).toStrictEqual(['a-1', 'a-2', 'a-2-1', 'a-2-2']) 120 | }) 121 | 122 | it('returns all children of a nested distributed Map', () => { 123 | const asl: Asl = { 124 | StartAt: 'a', 125 | States: { 126 | a: { 127 | Type: 'Map', 128 | ItemProcessor: { 129 | StartAt: 'a-1', 130 | States: { 131 | 'a-1': { 132 | Type: 'Pass', 133 | Next: 'a-2', 134 | }, 135 | 'a-2': { 136 | Type: 'Map', 137 | ItemProcessor: { 138 | StartAt: 'a-2-1', 139 | States: { 140 | 'a-2-1': { 141 | Type: 'Parallel', 142 | Branches: [ 143 | { 144 | StartAt: 'a-2-1-1', 145 | States: { 146 | 'a-2-1-1': { 147 | Type: 'Pass', 148 | End: true, 149 | }, 150 | }, 151 | }, 152 | { 153 | StartAt: 'a-2-1-2', 154 | States: { 155 | 'a-2-1-2': { 156 | Type: 'Pass', 157 | End: true, 158 | }, 159 | }, 160 | }, 161 | ], 162 | End: true, 163 | }, 164 | }, 165 | }, 166 | End: true, 167 | }, 168 | }, 169 | }, 170 | }, 171 | }, 172 | } 173 | 174 | const children = getAllChildren(asl, 'a') 175 | 176 | expect(children).toStrictEqual(['a-1', 'a-2', 'a-2-1', 'a-2-1-1', 'a-2-1-2']) 177 | }) 178 | }) 179 | -------------------------------------------------------------------------------- /src/asl-utils/asl/tests/next.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { getDirectNext } from '../asl' 7 | 8 | describe('getDirectNext', () => { 9 | it('should return null for terminal states', () => { 10 | expect(getDirectNext({ Type: 'Succeed' })).toBeNull() 11 | }) 12 | 13 | it('should return Next for normal states', () => { 14 | expect(getDirectNext({ Type: 'Pass', Next: 'a' })).toBe('a') 15 | }) 16 | 17 | it('should return null for normal states when there is no Next', () => { 18 | expect(getDirectNext({ Type: 'Pass' })).toBeNull() 19 | }) 20 | 21 | it('should return Default for Choice', () => { 22 | expect(getDirectNext({ Type: 'Choice', Default: 'a' })).toBe('a') 23 | }) 24 | 25 | it('should return First choice Next when no default', () => { 26 | expect(getDirectNext({ Type: 'Choice', Choices: [{ Next: 'a' }] })).toBe('a') 27 | }) 28 | 29 | it('should return null when there is no Next in choice rule', () => { 30 | expect(getDirectNext({ Type: 'Choice', Choices: [{}] })).toBeNull() 31 | }) 32 | 33 | it('should return null when there is no Choice rule nor Default', () => { 34 | expect(getDirectNext({ Type: 'Choice' })).toBeNull() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/asl-utils/asl/tests/visit.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Asl } from '../definitions' 7 | import { StatePath, visitAllStates, visitAllStatesInBranch, getAllStateIds } from '../asl' 8 | 9 | describe('visitAllStates', () => { 10 | it('visits all states', () => { 11 | const asl: Asl = { 12 | StartAt: 'parallel', 13 | States: { 14 | parallel: { 15 | Type: 'Parallel', 16 | Branches: [ 17 | { 18 | StartAt: 'parallel-0-a', 19 | States: { 20 | 'parallel-0-a': { 21 | Type: 'Pass', 22 | Next: 'parallel-0-b', 23 | }, 24 | 'parallel-0-b': { 25 | Type: 'Map', 26 | Iterator: { 27 | StartAt: 'parallel-0-b-x', 28 | States: { 29 | 'parallel-0-b-x': { 30 | Type: 'Pass', 31 | Next: 'parallel-0-b-y', 32 | }, 33 | 'parallel-0-b-y': { 34 | Type: 'Pass', 35 | End: true, 36 | }, 37 | }, 38 | }, 39 | Next: 'parallel-0-c', 40 | }, 41 | 'parallel-0-c': { 42 | Type: 'Parallel', 43 | End: true, 44 | }, 45 | }, 46 | }, 47 | { 48 | StartAt: 'parallel-1-a', 49 | States: { 50 | 'parallel-1-a': { 51 | Type: 'Pass', 52 | End: true, 53 | }, 54 | }, 55 | }, 56 | ], 57 | Next: 'pass', 58 | }, 59 | pass: { 60 | Type: 'Pass', 61 | End: true, 62 | }, 63 | }, 64 | } 65 | 66 | const visitedStateInfo: { id: string; path: StatePath }[] = [] 67 | 68 | visitAllStates(asl, (id, _, __, path) => { 69 | visitedStateInfo.push({ id, path }) 70 | return true 71 | }) 72 | 73 | expect(visitedStateInfo).toEqual([ 74 | { id: 'parallel', path: ['parallel'] }, 75 | { id: 'parallel-0-a', path: ['parallel', 0, 'parallel-0-a'] }, 76 | { id: 'parallel-0-b', path: ['parallel', 0, 'parallel-0-b'] }, 77 | { id: 'parallel-0-b-x', path: ['parallel', 0, 'parallel-0-b', 'parallel-0-b-x'] }, 78 | { id: 'parallel-0-b-y', path: ['parallel', 0, 'parallel-0-b', 'parallel-0-b-y'] }, 79 | { id: 'parallel-0-c', path: ['parallel', 0, 'parallel-0-c'] }, 80 | { id: 'parallel-1-a', path: ['parallel', 1, 'parallel-1-a'] }, 81 | { id: 'pass', path: ['pass'] }, 82 | ]) 83 | }) 84 | 85 | it('visits orphan states', () => { 86 | const asl: Asl = { 87 | StartAt: 'a', 88 | States: { 89 | a: { 90 | Type: 'Pass', 91 | End: true, 92 | }, 93 | b: { 94 | Type: 'Pass', 95 | End: true, 96 | }, 97 | }, 98 | } 99 | 100 | const visitedStateIds: string[] = [] 101 | visitAllStates(asl, (id) => { 102 | visitedStateIds.push(id) 103 | return true 104 | }) 105 | 106 | expect(visitedStateIds).toEqual(['a', 'b']) 107 | }) 108 | 109 | it('does not loop', () => { 110 | const asl: Asl = { 111 | StartAt: 'a', 112 | States: { 113 | a: { 114 | Type: 'Pass', 115 | Next: 'b', 116 | }, 117 | b: { 118 | Type: 'Pass', 119 | Next: 'a', 120 | }, 121 | }, 122 | } 123 | 124 | const visitedStateIds: string[] = [] 125 | visitAllStates(asl, (id) => { 126 | visitedStateIds.push(id) 127 | return true 128 | }) 129 | 130 | expect(visitedStateIds).toEqual(['a', 'b']) 131 | }) 132 | 133 | it('stops when visitor returns false', () => { 134 | const asl: Asl = { 135 | StartAt: 'a', 136 | States: { 137 | a: { 138 | Type: 'Pass', 139 | Next: 'b', 140 | }, 141 | b: { 142 | Type: 'Pass', 143 | Next: 'c', 144 | }, 145 | c: { 146 | Type: 'Pass', 147 | End: true, 148 | }, 149 | }, 150 | } 151 | 152 | const visitedStateIds: string[] = [] 153 | visitAllStates(asl, (id) => { 154 | visitedStateIds.push(id) 155 | return id !== 'b' 156 | }) 157 | 158 | expect(visitedStateIds).toEqual(['a', 'b']) 159 | }) 160 | 161 | it('stops when visitor returns false in legacy Map', () => { 162 | const asl: Asl = { 163 | StartAt: 'a', 164 | States: { 165 | a: { 166 | Type: 'Pass', 167 | Next: 'b', 168 | }, 169 | map: { 170 | Type: 'Map', 171 | Iterator: { 172 | StartAt: 'x', 173 | States: { 174 | x: { 175 | Type: 'Pass', 176 | Next: 'y', 177 | }, 178 | y: { 179 | Type: 'Pass', 180 | End: true, 181 | }, 182 | }, 183 | }, 184 | Next: 'c', 185 | }, 186 | c: { 187 | Type: 'Pass', 188 | End: true, 189 | }, 190 | }, 191 | } 192 | 193 | const visitedStateIds: string[] = [] 194 | visitAllStates(asl, (id) => { 195 | visitedStateIds.push(id) 196 | return id !== 'x' 197 | }) 198 | 199 | expect(visitedStateIds).toEqual(['a', 'map', 'x']) 200 | }) 201 | 202 | it('stops when visitor returns false in Distributed Map', () => { 203 | const asl: Asl = { 204 | StartAt: 'Map', 205 | States: { 206 | map: { 207 | Type: 'Map', 208 | ItemProcessor: { 209 | StartAt: 'x', 210 | States: { 211 | x: { 212 | Type: 'Pass', 213 | Next: 'y', 214 | }, 215 | y: { 216 | Type: 'Pass', 217 | End: true, 218 | }, 219 | }, 220 | }, 221 | Next: 'c', 222 | }, 223 | c: { 224 | Type: 'Pass', 225 | End: true, 226 | }, 227 | }, 228 | } 229 | 230 | const visitedStateIds: string[] = [] 231 | const stopped = !visitAllStatesInBranch(asl, ['map'], (id) => { 232 | visitedStateIds.push(id) 233 | return id !== 'x' 234 | }) 235 | expect(stopped).toBe(true) 236 | 237 | const notStopped = visitAllStatesInBranch(asl, ['map'], (id) => { 238 | visitedStateIds.push(id) 239 | return true 240 | }) 241 | expect(notStopped).toBe(true) 242 | }) 243 | 244 | it('stops when visitor returns false in Parallel', () => { 245 | const asl: Asl = { 246 | StartAt: 'a', 247 | States: { 248 | a: { 249 | Type: 'Pass', 250 | Next: 'b', 251 | }, 252 | parallel: { 253 | Type: 'Parallel', 254 | Branches: [ 255 | { 256 | StartAt: 'x1', 257 | States: { 258 | x1: { 259 | Type: 'Pass', 260 | Next: 'y1', 261 | }, 262 | y1: { 263 | Type: 'Pass', 264 | End: true, 265 | }, 266 | }, 267 | }, 268 | { 269 | StartAt: 'x2', 270 | States: { 271 | x2: { 272 | Type: 'Pass', 273 | Next: 'y2', 274 | }, 275 | y2: { 276 | Type: 'Pass', 277 | End: true, 278 | }, 279 | }, 280 | }, 281 | ], 282 | Next: 'c', 283 | }, 284 | c: { 285 | Type: 'Pass', 286 | End: true, 287 | }, 288 | }, 289 | } 290 | 291 | const visitedStateIds: string[] = [] 292 | visitAllStates(asl, (id) => { 293 | visitedStateIds.push(id) 294 | return id !== 'x1' 295 | }) 296 | 297 | expect(visitedStateIds).toEqual(['a', 'parallel', 'x1']) 298 | }) 299 | 300 | it('should not visit if there is no States', () => { 301 | const asl: Asl = { 302 | StartAt: 'a', 303 | } 304 | 305 | const visitedStateIds: string[] = [] 306 | visitAllStates(asl, (id) => { 307 | visitedStateIds.push(id) 308 | return true 309 | }) 310 | 311 | expect(visitedStateIds).toEqual([]) 312 | }) 313 | 314 | it('should handle Iterator without States', () => { 315 | const asl: Asl = { 316 | StartAt: 'a', 317 | States: { 318 | a: { 319 | Type: 'Map', 320 | Iterator: {}, 321 | }, 322 | }, 323 | } 324 | 325 | const visitedStateIds: string[] = [] 326 | visitAllStates(asl, (id) => { 327 | visitedStateIds.push(id) 328 | return true 329 | }) 330 | 331 | expect(visitedStateIds).toEqual(['a']) 332 | }) 333 | 334 | it('should handle branch without States', () => { 335 | const asl: Asl = { 336 | StartAt: 'a', 337 | States: { 338 | a: { 339 | Type: 'Parallel', 340 | Branches: [{}], 341 | }, 342 | }, 343 | } 344 | 345 | const visitedStateIds: string[] = [] 346 | visitAllStates(asl, (id) => { 347 | visitedStateIds.push(id) 348 | return true 349 | }) 350 | 351 | expect(visitedStateIds).toEqual(['a']) 352 | }) 353 | }) 354 | 355 | describe('getAllStateIds', () => { 356 | it('should get all stateIds', () => { 357 | const asl: Asl = { 358 | StartAt: 'a', 359 | States: { 360 | a: { 361 | Type: 'Pass', 362 | Next: 'b', 363 | }, 364 | map: { 365 | Type: 'Map', 366 | Iterator: { 367 | StartAt: 'x', 368 | States: { 369 | x: { 370 | Type: 'Pass', 371 | Next: 'y', 372 | }, 373 | y: { 374 | Type: 'Pass', 375 | End: true, 376 | }, 377 | }, 378 | }, 379 | Next: 'c', 380 | }, 381 | c: { 382 | Type: 'Pass', 383 | End: true, 384 | }, 385 | }, 386 | } 387 | 388 | const statesIds = getAllStateIds(asl) 389 | 390 | expect(statesIds).toEqual(['a', 'map', 'x', 'y', 'c']) 391 | }) 392 | }) 393 | -------------------------------------------------------------------------------- /src/asl-utils/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export * from './asl/asl' 7 | export * from './asl/definitions' 8 | 9 | export * from './utils/autocomplete' 10 | export * from './utils/jsonata' 11 | -------------------------------------------------------------------------------- /src/asl-utils/utils/jsonata/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export * from './jsonata' 7 | export type { FunctionCategory, FunctionParam, FunctionType, JsonataFunctionsMap } from './functions' 8 | -------------------------------------------------------------------------------- /src/asl-utils/utils/jsonata/jsonata.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import type jsonata from 'jsonata' 7 | import type { FunctionParam, JsonataFunctionsMap } from './functions' 8 | 9 | // There are additional properties in the object not specified in the library's typescript interface 10 | export interface ExprNode extends Omit { 11 | lhs?: jsonata.ExprNode | jsonata.ExprNode[] 12 | body?: jsonata.ExprNode 13 | then?: jsonata.ExprNode 14 | else?: jsonata.ExprNode 15 | condition?: jsonata.ExprNode 16 | error?: jsonata.JsonataError 17 | } 18 | 19 | export interface JSONataASTResult { 20 | node: ExprNode 21 | parent: ExprNode | null 22 | } 23 | 24 | export const exprPropertiesToRecurse = [ 25 | 'arguments', 26 | 'procedure', 27 | 'expressions', 28 | 'stages', 29 | 'lhs', 30 | 'rhs', 31 | 'body', 32 | 'stages', 33 | 'steps', 34 | 'then', 35 | 'else', 36 | 'condition', 37 | ] satisfies (keyof ExprNode)[] 38 | 39 | export const JSONATA_TEMPLATE_WRAPPER = { 40 | start: '{%', 41 | end: '%}', 42 | } as const 43 | 44 | export const MAX_AST_DEPTH = 100 45 | 46 | function findNodeInASTRecursive( 47 | ast: ExprNode | ExprNode[], 48 | parent: ExprNode | null, 49 | position: number, 50 | depth = 0, 51 | ): JSONataASTResult | null { 52 | if (depth > MAX_AST_DEPTH) { 53 | return null 54 | } 55 | 56 | // Some AST children are arrays, so we should iterate through each of the children 57 | if (Array.isArray(ast)) { 58 | let currentNode: ExprNode | null = null 59 | for (const node of ast) { 60 | const currentPosition = node.position || node.error?.position 61 | if (!currentPosition) { 62 | const found = findNodeInASTRecursive(node, parent, position, depth + 1) 63 | if (found) { 64 | return found 65 | } 66 | 67 | continue 68 | } 69 | 70 | if (currentPosition > position) { 71 | continue 72 | } 73 | 74 | if (currentPosition === position) { 75 | return { 76 | node: node, 77 | parent, 78 | } 79 | } 80 | 81 | if (!currentNode?.position || (currentPosition < position && currentNode.position < currentPosition)) { 82 | currentNode = node 83 | } 84 | } 85 | 86 | if (!currentNode) { 87 | return null 88 | } 89 | 90 | return findNodeInASTRecursive(currentNode, parent, position, depth + 1) 91 | } 92 | 93 | if (ast.position && ast.position > position) { 94 | return null 95 | } 96 | 97 | if (ast.position === position) { 98 | return { 99 | node: ast, 100 | parent, 101 | } 102 | } 103 | 104 | for (const key of exprPropertiesToRecurse) { 105 | const value = ast[key] 106 | if (value) { 107 | const found = findNodeInASTRecursive(value, ast, position, depth + 1) 108 | if (found) { 109 | return found 110 | } 111 | } 112 | } 113 | 114 | return null 115 | } 116 | 117 | let jsonataLibrary: typeof import('jsonata') | null = null 118 | 119 | /** 120 | * Dynamically imports jsonata and gets the AST of the string 121 | * @param input JSONata string to parse 122 | * @returns The root node of the JSONata AST 123 | */ 124 | export async function getJSONataAST( 125 | input: string, 126 | params: jsonata.JsonataOptions = { 127 | recover: true, 128 | }, 129 | ): Promise { 130 | if (!jsonataLibrary) { 131 | jsonataLibrary = (await import('jsonata')).default 132 | } 133 | 134 | return jsonataLibrary(input, params).ast() 135 | } 136 | 137 | export async function getJSONataFunctionList(): Promise { 138 | return (await import('./functions')).jsonataFunctions 139 | } 140 | 141 | /** 142 | * Searches the JSONata AST to find the node at a specified position. If the AST has nodes 143 | * that have a position past the position parameter, this function will return null. 144 | * @param ast The JSONata AST provided by the JSONata library 145 | * @param position The position to search the JSONata AST 146 | * @returns The node at the position 147 | */ 148 | export function findNodeInJSONataAST(ast: ExprNode, position: number): JSONataASTResult | null { 149 | return findNodeInASTRecursive(ast, null, position) 150 | } 151 | 152 | /** 153 | * Generates the function argument string from a given list of JSONata function parameters. 154 | * Recursively surrounds optional arguments with [] and separates arguments with commas. 155 | * @param functionParams Function parameters properties used for generating the argument string 156 | * @param index The index of the parameter to start traversal at 157 | * @returns The function argument string 158 | */ 159 | export function getFunctionArguments(functionParams: ReadonlyArray, index = 0): string { 160 | if (index > functionParams.length - 1) { 161 | return '' 162 | } 163 | 164 | const prefix = index === 0 ? '' : ', ' 165 | const argument = functionParams[index].name 166 | 167 | const info = `${prefix}${argument}${getFunctionArguments(functionParams, index + 1)}` 168 | 169 | if (functionParams[index].optional) { 170 | return `[${info}]` 171 | } 172 | 173 | return info 174 | } 175 | -------------------------------------------------------------------------------- /src/asl-utils/utils/tests/assignVariableTestData.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Asl } from '../../asl/definitions' 7 | import { DistributedMapProcessingMode } from '../../asl/definitions' 8 | 9 | export const getMapAsl: (mode: DistributedMapProcessingMode) => Asl = (mode) => { 10 | return { 11 | Comment: 'Assign variable with map states', 12 | StartAt: 'Pass_Parent', 13 | States: { 14 | Pass_Parent: { 15 | Type: 'Pass', 16 | Next: 'Map', 17 | Assign: { 18 | var_parent: 1, 19 | }, 20 | }, 21 | Map: { 22 | Type: 'Map', 23 | Iterator: { 24 | StartAt: 'Pass_SubWorkflow1', 25 | States: { 26 | Pass_SubWorkflow1: { 27 | Type: 'Pass', 28 | Next: 'Pass_SubWorkflow2', 29 | Assign: { 30 | var_sub1: 1, 31 | }, 32 | }, 33 | Pass_SubWorkflow2: { 34 | Type: 'Pass', 35 | End: true, 36 | Assign: { 37 | var_sub2: 1, 38 | }, 39 | }, 40 | }, 41 | ProcessorConfig: { 42 | Mode: mode, 43 | ExecutionType: 'STANDARD', 44 | }, 45 | }, 46 | End: true, 47 | ItemsPath: '$', 48 | MaxConcurrency: 5, 49 | Assign: { 50 | map: 'map params', 51 | }, 52 | }, 53 | }, 54 | } 55 | } 56 | 57 | export const mockLocalScopeAsl: Asl = { 58 | Comment: 'A description of my state machine', 59 | StartAt: 'Pass_BeforeLambda', 60 | States: { 61 | Pass_BeforeLambda: { 62 | Type: 'Pass', 63 | Next: 'Lambda', 64 | Assign: { 65 | ValueEnteredInForm: '{\n "brancOne2.$": "$.branch1",\n}', 66 | var_pass_before: 1, 67 | var_nested: { 68 | array: [ 69 | { 70 | key1: 123, 71 | }, 72 | ], 73 | object: { 74 | nestedObjectKey: 1, 75 | }, 76 | }, 77 | }, 78 | }, 79 | Lambda: { 80 | Type: 'Task', 81 | Resource: 'arn:aws:states:::lambda:invoke', 82 | OutputPath: '$.Payload', 83 | Parameters: { 84 | 'Payload.$': '$', 85 | }, 86 | Assign: { 87 | var_lambda_pass: 1, 88 | }, 89 | Next: 'Pass_Success', 90 | Catch: [ 91 | { 92 | ErrorEquals: [], 93 | Next: 'Pass_ErrorFallback', 94 | Assign: { 95 | var_lambda_error: 1, 96 | }, 97 | }, 98 | ], 99 | }, 100 | Pass_Success: { 101 | Type: 'Pass', 102 | Assign: { 103 | var_pass_success: 1, 104 | }, 105 | Next: 'Pass_End', 106 | }, 107 | Pass_ErrorFallback: { 108 | Type: 'Pass', 109 | Next: 'Pass_End', 110 | Assign: { 111 | var_pass_error: 1, 112 | }, 113 | }, 114 | Pass_End: { 115 | Type: 'Pass', 116 | Next: 'Success', 117 | Assign: { 118 | var_pass_end: 1, 119 | }, 120 | }, 121 | Success: { 122 | Type: 'Succeed', 123 | }, 124 | }, 125 | } 126 | 127 | export const mockParallelAssign: Asl = { 128 | Comment: 'A description of my parallel state machine', 129 | StartAt: 'Pass_Parent', 130 | States: { 131 | Pass_Parent: { 132 | Type: 'Pass', 133 | Next: 'Parallel', 134 | Assign: { 135 | var_parent: 1, 136 | }, 137 | }, 138 | Parallel: { 139 | Type: 'Parallel', 140 | Branches: [ 141 | { 142 | StartAt: 'Branch2-1', 143 | States: { 144 | 'Branch2-1': { 145 | Type: 'Pass', 146 | Assign: { 147 | var_branch2_1: 1, 148 | }, 149 | End: true, 150 | }, 151 | }, 152 | }, 153 | { 154 | StartAt: 'Branch1-1', 155 | States: { 156 | 'Branch1-1': { 157 | Type: 'Task', 158 | Resource: 'arn:aws:states:::lambda:invoke', 159 | OutputPath: '$.Payload', 160 | Parameters: { 161 | 'Payload.$': '$', 162 | }, 163 | Assign: { 164 | var_branch1_1: 1, 165 | }, 166 | Next: 'Branch1-2', 167 | }, 168 | 'Branch1-2': { 169 | Type: 'Pass', 170 | Assign: { 171 | var_branch1_2: 1, 172 | }, 173 | End: true, 174 | }, 175 | }, 176 | }, 177 | ], 178 | End: true, 179 | Assign: { 180 | var_parallel: 1, 181 | }, 182 | }, 183 | }, 184 | } 185 | 186 | export const mockChoiceAssignAsl: Asl = { 187 | Comment: 'A description of my state machine', 188 | StartAt: 'Pass_Before_Choice', 189 | States: { 190 | Pass_Parent: { 191 | Type: 'Pass', 192 | Next: 'Choice', 193 | Assign: { 194 | var_pass_before: 1, 195 | }, 196 | }, 197 | Choice: { 198 | Type: 'Choice', 199 | Choices: [ 200 | { 201 | Next: 'Rule1-1', 202 | Assign: { 203 | var_rule1: 1, 204 | }, 205 | }, 206 | ], 207 | Default: 'Rule-default-1', 208 | Assign: { 209 | var_rule_default: 1, 210 | }, 211 | }, 212 | 'Rule1-1': { 213 | Type: 'Task', 214 | Resource: 'arn:aws:states:::lambda:invoke', 215 | OutputPath: '$.Payload', 216 | Parameters: { 217 | 'Payload.$': '$', 218 | }, 219 | Assign: { 220 | var_branch_1: 1, 221 | }, 222 | Next: 'Rule1-2', 223 | }, 224 | 'Rule1-2': { 225 | Type: 'Pass', 226 | Assign: { 227 | var_branch_2: 1, 228 | }, 229 | End: true, 230 | }, 231 | 'Rule-default-1': { 232 | Type: 'Pass', 233 | Assign: { 234 | var_default_1: 1, 235 | }, 236 | End: true, 237 | }, 238 | }, 239 | } 240 | 241 | export const mockManualLoopAsl: Asl = { 242 | Comment: 'A description of my state machine', 243 | StartAt: 'Wait_BeforeLoop', 244 | States: { 245 | Wait_BeforeLoop: { 246 | Type: 'Wait', 247 | Seconds: 5, 248 | Next: 'Pass_Before_Choice', 249 | Assign: { 250 | var_before_loop: 1, 251 | }, 252 | }, 253 | Pass_Before_Choice: { 254 | Type: 'Pass', 255 | Next: 'Choice', 256 | Assign: { 257 | var_pass_before: '$', 258 | }, 259 | }, 260 | Choice: { 261 | Type: 'Choice', 262 | Choices: [ 263 | { 264 | Next: 'Rule1-1', 265 | Assign: { 266 | var_rule1: 1, 267 | }, 268 | }, 269 | ], 270 | Default: 'Rule-default-1', 271 | Assign: { 272 | var_rule_default: 1, 273 | }, 274 | }, 275 | 'Rule1-1': { 276 | Type: 'Task', 277 | Resource: 'arn:aws:states:::lambda:invoke', 278 | OutputPath: '$.Payload', 279 | Parameters: { 280 | 'Payload.$': '$', 281 | }, 282 | Assign: { 283 | var_branch_1: 1, 284 | }, 285 | Next: 'Rule1-2', 286 | }, 287 | 'Rule1-2': { 288 | Type: 'Pass', 289 | Assign: { 290 | var_branch_2: 1, 291 | }, 292 | End: true, 293 | }, 294 | 'Rule-default-1': { 295 | Type: 'Pass', 296 | Assign: { 297 | var_default_1: 1, 298 | }, 299 | Next: 'Pass_Before_Choice', 300 | }, 301 | }, 302 | } 303 | -------------------------------------------------------------------------------- /src/asl-utils/utils/tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { deepClone, isJSONataExpression, isValidJSON, lastItem } from '../utils' 7 | 8 | describe('lastItem', () => { 9 | it('should return the last item of array', () => { 10 | expect(lastItem([])).toBeUndefined() 11 | expect(lastItem([1])).toBe(1) 12 | expect(lastItem([1, 2, 3])).toBe(3) 13 | }) 14 | }) 15 | describe('deepClone', () => { 16 | it('should deepClone objects', () => { 17 | const obj = { 18 | val: 1, 19 | prop: { 20 | val2: 0, 21 | val3: { 22 | name: 'a', 23 | condition: false, 24 | myArr: [1, 2, 3, 4], 25 | myFunc: () => { 26 | // noop 27 | }, 28 | [Symbol.iterator]: {}, 29 | }, 30 | }, 31 | } 32 | expect(JSON.stringify(deepClone(obj))).toBe(JSON.stringify(obj)) 33 | }) 34 | }) 35 | 36 | describe('isValidJSON', () => { 37 | it('should return true if json is valid', () => { 38 | const validJson = `{ 39 | "a": "b", 40 | "c": { 41 | "d": "e" 42 | } 43 | }` 44 | expect(isValidJSON(validJson)).toBe(true) 45 | }) 46 | 47 | it('should return false if json is invalid', () => { 48 | const invalidJson = `{ 49 | "a": "b 50 | }` 51 | const invalidJson2 = `{ 52 | 3: 5, 53 | }` 54 | 55 | expect(isValidJSON(invalidJson)).toBe(false) 56 | expect(isValidJSON(invalidJson2)).toBe(false) 57 | }) 58 | }) 59 | 60 | describe('isJSONataExpression', () => { 61 | it.each(['{% expression %}', '{%%}'])('should return true for %s', (expression) => { 62 | expect(isJSONataExpression(expression)).toBe(true) 63 | }) 64 | 65 | it('should return false if not a string', () => { 66 | expect(isJSONataExpression(123)).toBe(false) 67 | expect(isJSONataExpression(null)).toBe(false) 68 | expect(isJSONataExpression({})).toBe(false) 69 | }) 70 | 71 | it('should return false if string does not start with {%', () => { 72 | expect(isJSONataExpression('expression %}')).toBe(false) 73 | }) 74 | 75 | it.each(['%{ expression }%', ' {% expression %}', '{% expression %} '])( 76 | 'should return false if string is %s', 77 | (expression) => { 78 | expect(isJSONataExpression(expression)).toBe(false) 79 | }, 80 | ) 81 | 82 | it('should return false if string does not end with %}', () => { 83 | expect(isJSONataExpression('{% expression')).toBe(false) 84 | }) 85 | 86 | it('should return false if empty string', () => { 87 | expect(isJSONataExpression('')).toBe(false) 88 | }) 89 | 90 | it('should return false if string does not include both starting and ending expression', () => { 91 | expect(isJSONataExpression('{%}')).toBe(false) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /src/asl-utils/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import cloneDeep from 'lodash/cloneDeep' 7 | 8 | /** 9 | * Creates a deep clone of the given object. 10 | * Use this function to convert a frozen state to an immutable one. 11 | * 12 | * See {@link Store} 13 | */ 14 | export function deepClone(obj: T): T { 15 | // structuredClone does not copy functions/symbols, but lodash/cloneDeep does 16 | return cloneDeep(obj) 17 | } 18 | 19 | export function lastItem(arr: any[]): any | undefined { 20 | return arr.length > 0 ? arr[arr.length - 1] : undefined 21 | } 22 | 23 | /** 24 | * The value could be anything from the ASL. We first confirm if its a string and then check if its a JSONata expression 25 | * @param value any value from the definition 26 | * @returns true if the given value is a JSONata expression. 27 | */ 28 | export const isJSONataExpression = (value: unknown): value is `{%${string}%}` => 29 | typeof value === 'string' && value.startsWith('{%') && value.endsWith('%}') && value.length >= 4 30 | 31 | /** 32 | * Returns true if JSON is valid and false otherwise 33 | */ 34 | export const isValidJSON = (jsonString: string): boolean => { 35 | try { 36 | JSON.parse(jsonString) 37 | return true 38 | } catch (error) { 39 | return false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/completion/completeAsl.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { CompletionList, JSONDocument, Position, TextDocument } from 'vscode-json-languageservice' 7 | import { buildPreviousStatesMap, Asl, QueryLanguages } from '../asl-utils' 8 | import { ASLOptions, ASTTree, findNodeAtLocation, getStateInfo } from '../utils/astUtilityFunctions' 9 | import completeSnippets from './completeSnippets' 10 | import completeStateNames from './completeStateNames' 11 | import completeVariables from './completeVariables' 12 | import completeJSONata from './completeJSONata' 13 | import { LANGUAGE_IDS } from '../constants/constants' 14 | 15 | let asl: Asl = {} 16 | 17 | export default async function completeAsl( 18 | document: TextDocument, 19 | position: Position, 20 | doc: JSONDocument, 21 | jsonCompletions: CompletionList | null, 22 | aslOptions?: ASLOptions, 23 | ): Promise { 24 | const offset = document.offsetAt(position) 25 | const rootNode = (doc as ASTTree).root 26 | 27 | if (!rootNode) { 28 | return { 29 | isIncomplete: false, 30 | items: [], 31 | } 32 | } 33 | 34 | const node = findNodeAtLocation(rootNode, offset) 35 | 36 | const snippetsList = completeSnippets(node, offset, aslOptions) 37 | let completionList = completeStateNames(node, offset, document, aslOptions) ?? jsonCompletions 38 | 39 | if (completionList?.items) { 40 | completionList.items = completionList.items.concat(snippetsList) 41 | } else { 42 | completionList = { 43 | isIncomplete: false, 44 | items: snippetsList, 45 | } 46 | } 47 | 48 | if (document.languageId === LANGUAGE_IDS.JSON) { 49 | const text = document.getText() 50 | // we are using the last valid asl for autocompletion list generation 51 | // skip to store asl when it is invalid 52 | try { 53 | asl = JSON.parse(text) 54 | } catch (_err) { 55 | // noop 56 | } 57 | 58 | // prepare dynamic variable list 59 | buildPreviousStatesMap(asl) 60 | } 61 | 62 | const { queryLanguage } = (node && getStateInfo(node)) || {} 63 | const isJSONataState = queryLanguage === QueryLanguages.JSONata || asl.QueryLanguage === QueryLanguages.JSONata 64 | 65 | if (isJSONataState) { 66 | const jsonataList = await completeJSONata(node, offset, document, asl) 67 | 68 | if (jsonataList?.items) { 69 | completionList.items = completionList.items.concat(jsonataList.items) 70 | } 71 | } else { 72 | const variableList = completeVariables(node, offset, document, asl) 73 | 74 | if (variableList?.items) { 75 | completionList.items = completionList.items.concat(variableList.items) 76 | } 77 | } 78 | 79 | // Assign sort order for the completion items so we maintain order 80 | // and snippets are shown near the end of the completion list 81 | completionList.items.map((item, index) => ({ ...item, sortText: index.toString() })) 82 | 83 | return completionList 84 | } 85 | -------------------------------------------------------------------------------- /src/completion/completeJSONata.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | import { 6 | Asl, 7 | VARIABLE_PREFIX, 8 | JSONATA_TEMPLATE_WRAPPER, 9 | getFunctionArguments, 10 | JSONataASTResult, 11 | JsonataFunctionsMap, 12 | } from '../asl-utils' 13 | import { 14 | ASTNode, 15 | CompletionItem, 16 | CompletionItemKind, 17 | CompletionList, 18 | Position, 19 | Range, 20 | TextDocument, 21 | TextEdit, 22 | } from 'vscode-json-languageservice' 23 | import { LANGUAGE_IDS } from '../constants/constants' 24 | import { getStateInfo, isPropertyNode, isStringNode } from '../utils/astUtilityFunctions' 25 | import { getVariableCompletions } from './utils/variableUtils' 26 | import { getJSONataNodeData } from './utils/jsonataUtils' 27 | 28 | interface FunctionCompletionProperties { 29 | jsonataNode: JSONataASTResult 30 | node: ASTNode 31 | asl: Asl 32 | nodePosition: Position 33 | endPosition: Position 34 | } 35 | 36 | /** 37 | * Generates completion list for variables 38 | * @returns 39 | */ 40 | function getVariablesCompletionList({ 41 | node, 42 | value, 43 | asl, 44 | replaceRange, 45 | }: { 46 | node: ASTNode 47 | value: string 48 | asl: Asl 49 | replaceRange: Range 50 | }) { 51 | const variableCompletions = getVariableCompletions(node, value, asl) 52 | return ( 53 | variableCompletions?.varList.map((name) => { 54 | const item = CompletionItem.create(name) 55 | item.commitCharacters = ['.'] 56 | 57 | item.kind = CompletionItemKind.Variable 58 | const prefix = variableCompletions.completionList.parentPath 59 | const completeVal = (prefix ? `${VARIABLE_PREFIX}${prefix}.` : '') + name 60 | 61 | item.textEdit = TextEdit.replace(replaceRange, completeVal) 62 | item.filterText = completeVal 63 | item.label = name 64 | 65 | return item 66 | }) || [] 67 | ) 68 | } 69 | 70 | /** 71 | * Generate completion list for partial paths such as `$states.cont` 72 | * @returns CompletionList if a partial path exists, otherwise undefined 73 | */ 74 | function getPartialPathCompletion({ 75 | jsonataNode, 76 | node, 77 | asl, 78 | nodePosition, 79 | endPosition, 80 | }: FunctionCompletionProperties): CompletionList | undefined { 81 | const isJsonataPathNode = jsonataNode.node.type === 'name' && jsonataNode.node.position && jsonataNode.node.value 82 | 83 | const isParentNodePath = jsonataNode.parent?.type === 'path' 84 | const parentPath = jsonataNode.parent?.steps 85 | 86 | if (isJsonataPathNode && isParentNodePath && parentPath) { 87 | const parentPathKey: string = parentPath 88 | .map((step) => step.value) 89 | .filter((value) => typeof value === 'string' && value) 90 | .join('.') 91 | 92 | const range = Range.create( 93 | Position.create(nodePosition.line, endPosition.character - parentPathKey.length), 94 | endPosition, 95 | ) 96 | return { 97 | isIncomplete: true, 98 | items: getVariablesCompletionList({ 99 | node, 100 | value: parentPathKey, 101 | asl, 102 | replaceRange: range, 103 | }), 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * Generate completion list for incomplete paths such as `$states.` 110 | * @returns CompletionList if an incomplete path exists, otherwise undefined 111 | */ 112 | function getIncompletePathCompletion({ 113 | jsonataNode, 114 | nodePosition, 115 | endPosition, 116 | node, 117 | asl, 118 | }: FunctionCompletionProperties): CompletionList | undefined { 119 | const isErrorNode = jsonataNode.node.type === 'error' && jsonataNode.node.error && jsonataNode.node.error.position 120 | 121 | const isParentNodePath = jsonataNode.parent?.type === 'path' 122 | const parentPath = jsonataNode.parent?.steps 123 | if (isErrorNode && isParentNodePath && parentPath) { 124 | const parentPathKey: string = 125 | parentPath 126 | .map((step) => step.value) 127 | .filter((value) => typeof value === 'string' && value) 128 | .join('.') + '.' 129 | 130 | const range = Range.create( 131 | Position.create(nodePosition.line, endPosition.character - parentPathKey.length), 132 | endPosition, 133 | ) 134 | 135 | return { 136 | isIncomplete: true, 137 | items: getVariablesCompletionList({ 138 | node, 139 | value: parentPathKey, 140 | asl, 141 | replaceRange: range, 142 | }), 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * Generate completion list for functions 149 | * @returns CompletionList if a function exists, otherwise undefined 150 | */ 151 | function getFunctionCompletions({ 152 | jsonataNodeLength, 153 | jsonataFunctions, 154 | properties, 155 | }: { 156 | jsonataNodeLength: number 157 | jsonataFunctions: JsonataFunctionsMap 158 | properties: FunctionCompletionProperties 159 | }): CompletionList | undefined { 160 | const { jsonataNode, node, asl, nodePosition, endPosition } = properties 161 | 162 | const range = Range.create(Position.create(nodePosition.line, endPosition.character - jsonataNodeLength), endPosition) 163 | 164 | const functions = jsonataNode.node.type === 'variable' ? Array.from(jsonataFunctions.keys()) : [] 165 | 166 | const functionCompletionItems = functions.map((name) => { 167 | const item = CompletionItem.create(name) 168 | item.commitCharacters = ['('] 169 | 170 | item.kind = CompletionItemKind.Function 171 | const completeVal = name 172 | const functionProps = jsonataFunctions.get(name) 173 | 174 | item.textEdit = TextEdit.replace(range, completeVal) 175 | item.filterText = completeVal 176 | item.label = name 177 | 178 | item.detail = `${name}(${getFunctionArguments(functionProps?.params || [])})` 179 | 180 | item.documentation = functionProps?.description && { 181 | kind: 'markdown', 182 | value: functionProps?.description, 183 | } 184 | 185 | return item 186 | }) 187 | 188 | const variableCompletionItems = getVariablesCompletionList({ 189 | node, 190 | value: jsonataNode.node.value, 191 | asl, 192 | replaceRange: range, 193 | }).map( 194 | (item) => 195 | ({ 196 | ...item, 197 | // Place variables at the top of the list 198 | sortText: `!${item.label}`, 199 | }) as CompletionItem, 200 | ) 201 | 202 | return { 203 | isIncomplete: false, 204 | items: variableCompletionItems.concat(functionCompletionItems), 205 | } 206 | } 207 | 208 | export default async function completeJSONata( 209 | node: ASTNode | undefined, 210 | offset: number, 211 | document: TextDocument, 212 | asl: Asl, 213 | ): Promise { 214 | if (!node || document.languageId === LANGUAGE_IDS.YAML) { 215 | return 216 | } 217 | 218 | if (!node || !node.parent || !isPropertyNode(node.parent)) { 219 | return 220 | } 221 | 222 | const isValueNode = node.parent.valueNode?.offset === node.offset 223 | if (!isStringNode(node) || !isValueNode) { 224 | return 225 | } 226 | 227 | const stateInfo = getStateInfo(node) 228 | // don't generate JSONata autocomplete strings if not inside a state 229 | if (!stateInfo) { 230 | return 231 | } 232 | 233 | const jsonataNodeData = await getJSONataNodeData(document, offset, node) 234 | 235 | if (!jsonataNodeData) { 236 | return 237 | } 238 | 239 | const { jsonataNode, nodePosition, jsonataNodePosition, jsonataFunctions } = jsonataNodeData 240 | 241 | const jsonataNodeLength = jsonataNode.node.value ? String(jsonataNode?.node.value).length : 0 242 | 243 | const endPosition = Position.create( 244 | nodePosition.line, 245 | jsonataNodePosition + JSONATA_TEMPLATE_WRAPPER.start.length + nodePosition.character, 246 | ) 247 | 248 | const partialPathCompletion = getPartialPathCompletion({ 249 | jsonataNode, 250 | nodePosition, 251 | endPosition, 252 | node, 253 | asl, 254 | }) 255 | if (partialPathCompletion) { 256 | return partialPathCompletion 257 | } 258 | 259 | const incompletePathCompletion = getIncompletePathCompletion({ 260 | jsonataNode, 261 | nodePosition, 262 | endPosition, 263 | node, 264 | asl, 265 | }) 266 | if (incompletePathCompletion) { 267 | return incompletePathCompletion 268 | } 269 | 270 | const variableAndFunctionCompletions = getFunctionCompletions({ 271 | jsonataNodeLength, 272 | jsonataFunctions, 273 | properties: { 274 | jsonataNode, 275 | nodePosition, 276 | endPosition, 277 | node, 278 | asl, 279 | }, 280 | }) 281 | 282 | return variableAndFunctionCompletions 283 | } 284 | -------------------------------------------------------------------------------- /src/completion/completeSnippets.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { 7 | ASTNode, 8 | CompletionItem, 9 | CompletionItemKind, 10 | InsertTextFormat, 11 | PropertyASTNode, 12 | } from 'vscode-json-languageservice' 13 | 14 | import { findPropChildByName, insideStateNode, isChildOfStates, isObjectNode } from '../utils/astUtilityFunctions' 15 | 16 | import errorHandlingSnippetsRaw from '../snippets/error_handling.json' 17 | import stateSnippetsRaw from '../snippets/states.json' 18 | 19 | interface Snippet { 20 | name: string 21 | body: string[] 22 | description: string 23 | } 24 | 25 | const ERROR_HANDLING_STATES = ['Task', 'Parallel', 'Map'] 26 | export const stateSnippets = parseSnippetsFromJson(stateSnippetsRaw) 27 | export const errorHandlingSnippets = parseSnippetsFromJson(errorHandlingSnippetsRaw) 28 | 29 | function parseSnippetsFromJson(json: Snippet[]): CompletionItem[] { 30 | return json.map((snippet) => { 31 | const item = CompletionItem.create(snippet.name) 32 | item.kind = CompletionItemKind.Snippet 33 | item.insertTextFormat = InsertTextFormat.Snippet 34 | item.insertText = snippet.body.join('\n') 35 | item.documentation = snippet.description 36 | 37 | return item 38 | }) 39 | } 40 | 41 | function doesStateSupportErrorHandling(node: ASTNode): boolean { 42 | let typeNode: PropertyASTNode | undefined 43 | 44 | if (isObjectNode(node)) { 45 | typeNode = findPropChildByName(node, 'Type') 46 | } 47 | 48 | return ERROR_HANDLING_STATES.includes(typeNode?.valueNode?.value as string) 49 | } 50 | interface CompleteSnippetsOptions { 51 | shouldShowStateSnippets?: boolean 52 | shouldShowErrorSnippets?: { 53 | retry: boolean 54 | catch: boolean 55 | } 56 | } 57 | 58 | export default function completeSnippets( 59 | node: ASTNode | undefined, 60 | offset: number, 61 | options?: CompleteSnippetsOptions, 62 | ): CompletionItem[] { 63 | if (node) { 64 | const errorSnippetOptionsNotDefined = options?.shouldShowErrorSnippets === undefined 65 | // If the value of shouldShowStateSnippets is false prevent the snippets from being displayed 66 | const showStateSnippets = 67 | options?.shouldShowStateSnippets || (options?.shouldShowStateSnippets === undefined && isChildOfStates(node)) 68 | 69 | if (showStateSnippets) { 70 | return stateSnippets 71 | } 72 | 73 | if (errorSnippetOptionsNotDefined) { 74 | if (insideStateNode(node) && doesStateSupportErrorHandling(node)) { 75 | return errorHandlingSnippets 76 | } 77 | 78 | return [] 79 | } 80 | 81 | const errorSnippetsToShow: string[] = [] 82 | 83 | if (options?.shouldShowErrorSnippets?.catch) { 84 | errorSnippetsToShow.push('Catch') 85 | } 86 | 87 | if (options?.shouldShowErrorSnippets?.retry) { 88 | errorSnippetsToShow.push('Retry') 89 | } 90 | 91 | if (errorSnippetsToShow.length) { 92 | return errorHandlingSnippets.filter((snippet) => errorSnippetsToShow.includes(snippet.label)) 93 | } 94 | } 95 | 96 | return [] 97 | } 98 | -------------------------------------------------------------------------------- /src/completion/completeStateNames.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { 7 | ASTNode, 8 | CompletionItem, 9 | CompletionItemKind, 10 | CompletionList, 11 | PropertyASTNode, 12 | Range, 13 | TextDocument, 14 | TextEdit, 15 | } from 'vscode-json-languageservice' 16 | import { LANGUAGE_IDS } from '../constants/constants' 17 | 18 | import { 19 | ASLOptions, 20 | CompleteStateNameOptions, 21 | findClosestAncestorStateNode, 22 | getListOfStateNamesFromStateNode, 23 | isObjectNode, 24 | isPropertyNode, 25 | isStringNode, 26 | } from '../utils/astUtilityFunctions' 27 | import { isStateNameReservedYamlKeyword } from '../yaml/yamlUtils' 28 | 29 | function getStatesFromStartAtNode(node: PropertyASTNode, options?: ASLOptions): string[] { 30 | if (node.keyNode.value === 'StartAt') { 31 | if (node.parent && isObjectNode(node.parent)) { 32 | const statesNode = node.parent.properties.find((propNode) => propNode.keyNode.value === 'States') 33 | 34 | if (statesNode) { 35 | return getListOfStateNamesFromStateNode(statesNode, options?.ignoreColonOffset) 36 | } 37 | } 38 | } 39 | 40 | return [] 41 | } 42 | 43 | function getListOfItems(node: PropertyASTNode, options?: ASLOptions): string[] { 44 | const keyVal = node.keyNode.value 45 | 46 | switch (keyVal) { 47 | case 'StartAt': { 48 | return getStatesFromStartAtNode(node, options) 49 | } 50 | case 'Next': 51 | case 'Default': { 52 | const statesNode = findClosestAncestorStateNode(node) 53 | 54 | const stateItemNode = node.parent?.parent 55 | 56 | let stateItemName: string | undefined 57 | 58 | if (stateItemNode && isPropertyNode(stateItemNode)) { 59 | // The state name under cursor shouldn't be suggested - find the value 60 | stateItemName = stateItemNode.keyNode.value 61 | // If stateItemNode is not a property node go 3 levels up as it is Next within Choice state 62 | } else if (stateItemNode?.parent?.parent?.parent && isPropertyNode(stateItemNode.parent.parent.parent)) { 63 | stateItemName = stateItemNode.parent.parent.parent.keyNode.value 64 | } 65 | 66 | if (statesNode) { 67 | return getListOfStateNamesFromStateNode(statesNode, options?.ignoreColonOffset).filter( 68 | (name) => name !== stateItemName, 69 | ) 70 | } 71 | 72 | return [] 73 | } 74 | default: { 75 | return [] 76 | } 77 | } 78 | } 79 | 80 | function getCompletionList( 81 | items: string[], 82 | replaceRange: Range, 83 | languageId: string, 84 | options: CompleteStateNameOptions, 85 | ) { 86 | const { shouldAddLeftQuote, shouldAddRightQuote, shouldAddLeadingSpace, shoudlAddTrailingComma } = options 87 | 88 | const list: CompletionList = { 89 | isIncomplete: false, 90 | items: items.map((name) => { 91 | const shouldWrapStateNameInQuotes = languageId === LANGUAGE_IDS.YAML && isStateNameReservedYamlKeyword(name) 92 | const item = CompletionItem.create(name) 93 | item.commitCharacters = [','] 94 | 95 | item.kind = CompletionItemKind.Value 96 | 97 | const newText = 98 | (shouldAddLeadingSpace ? ' ' : '') + 99 | (shouldAddLeftQuote ? '"' : '') + 100 | (shouldWrapStateNameInQuotes ? "'" : '') + 101 | name + 102 | (shouldWrapStateNameInQuotes ? "'" : '') + 103 | (shouldAddRightQuote ? '"' : '') + 104 | (shoudlAddTrailingComma ? ',' : '') 105 | item.textEdit = TextEdit.replace(replaceRange, newText) 106 | item.filterText = name 107 | 108 | return item 109 | }), 110 | } 111 | 112 | return list 113 | } 114 | 115 | export default function completeStateNames( 116 | node: ASTNode | undefined, 117 | offset: number, 118 | document: TextDocument, 119 | options?: ASLOptions, 120 | ): CompletionList | undefined { 121 | // For property nodes 122 | if (node && isPropertyNode(node) && node.colonOffset) { 123 | const states = getListOfItems(node, options) 124 | 125 | if (states.length) { 126 | const colonPosition = document.positionAt(node.colonOffset + 1) 127 | let endPosition = document.positionAt(node.offset + node.length) 128 | 129 | // The range shouldn't span multiple lines, if lines are different it is due to 130 | // lack of comma and text should be inserted in place 131 | if (colonPosition.line !== endPosition.line) { 132 | endPosition = colonPosition 133 | } 134 | 135 | const range = Range.create(colonPosition, endPosition) 136 | 137 | const completeStateNameOptions = { 138 | shouldAddLeftQuote: true, 139 | shouldAddRightQuote: true, 140 | shouldAddLeadingSpace: true, 141 | shoudlAddTrailingComma: true, 142 | } 143 | 144 | return getCompletionList(states, range, document.languageId, completeStateNameOptions) 145 | } 146 | } 147 | 148 | // For string nodes that have a parent that is a property node 149 | if (node && node.parent && isPropertyNode(node.parent)) { 150 | const propNode = node.parent 151 | 152 | if (isStringNode(node)) { 153 | const states = getListOfItems(propNode, options) 154 | 155 | if (states.length) { 156 | // Text edit will only work when start position is higher than the node offset 157 | const startPosition = document.positionAt(node.offset + 1) 158 | const endPosition = document.positionAt(node.offset + node.length) 159 | 160 | const range = Range.create(startPosition, endPosition) 161 | if (document.languageId === LANGUAGE_IDS.YAML) { 162 | const completeStateNameOptions = { 163 | shouldAddLeftQuote: false, 164 | shouldAddRightQuote: false, 165 | shouldAddLeadingSpace: false, 166 | shoudlAddTrailingComma: false, 167 | } 168 | 169 | return getCompletionList(states, range, document.languageId, completeStateNameOptions) 170 | } else { 171 | const isCursorAtTheBeginning = offset === node.offset 172 | const completeStateNameOptions = { 173 | shouldAddLeftQuote: isCursorAtTheBeginning, 174 | shouldAddRightQuote: true, 175 | shouldAddLeadingSpace: false, 176 | shoudlAddTrailingComma: false, 177 | } 178 | 179 | return getCompletionList(states, range, document.languageId, completeStateNameOptions) 180 | } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/completion/completeVariables.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | import { Asl, VARIABLE_PREFIX } from '../asl-utils' 6 | import { 7 | ASTNode, 8 | CompletionItem, 9 | CompletionItemKind, 10 | CompletionList, 11 | Range, 12 | TextDocument, 13 | TextEdit, 14 | } from 'vscode-json-languageservice' 15 | import { LANGUAGE_IDS } from '../constants/constants' 16 | import { CompleteStateNameOptions, isPropertyNode, isStringNode } from '../utils/astUtilityFunctions' 17 | import { getVariableCompletions } from './utils/variableUtils' 18 | 19 | function getCompletionList( 20 | prefix: string, 21 | items: string[], 22 | replaceRange: Range, 23 | languageId: string, 24 | options: CompleteStateNameOptions, 25 | ) { 26 | const { shouldAddLeftQuote, shouldAddRightQuote, shouldAddLeadingSpace, shoudlAddTrailingComma } = options 27 | 28 | const list: CompletionList = { 29 | isIncomplete: false, 30 | items: items.map((name) => { 31 | const item = CompletionItem.create(name) 32 | item.commitCharacters = [','] 33 | 34 | item.kind = CompletionItemKind.Variable 35 | const completeVal = (prefix ? `${VARIABLE_PREFIX}${prefix}.` : '') + name 36 | 37 | const newText = 38 | (shouldAddLeadingSpace ? ' ' : '') + 39 | (shouldAddLeftQuote ? '"' : '') + 40 | completeVal + 41 | (shouldAddRightQuote ? '"' : '') + 42 | (shoudlAddTrailingComma ? ',' : '') 43 | item.textEdit = TextEdit.replace(replaceRange, newText) 44 | item.filterText = completeVal 45 | item.label = name 46 | 47 | return item 48 | }), 49 | } 50 | 51 | return list 52 | } 53 | 54 | export default function completeVariables( 55 | node: ASTNode | undefined, 56 | offset: number, 57 | document: TextDocument, 58 | asl: Asl, 59 | ): CompletionList | undefined { 60 | if (!node || document.languageId === LANGUAGE_IDS.YAML) { 61 | return 62 | } 63 | 64 | const variableCompletions = getVariableCompletions(node, node.value?.toString(), asl) 65 | if (!variableCompletions) { 66 | return 67 | } 68 | 69 | const { varList, completionList } = variableCompletions 70 | 71 | if (isPropertyNode(node) && node.colonOffset) { 72 | if (varList.length) { 73 | const colonPosition = document.positionAt(node.colonOffset + 1) 74 | let endPosition = document.positionAt(node.offset + node.length) 75 | 76 | // The range shouldn't span multiple lines, if lines are different it is due to 77 | // lack of comma and text should be inserted in place 78 | if (colonPosition.line !== endPosition.line) { 79 | endPosition = colonPosition 80 | } 81 | 82 | const range = Range.create(colonPosition, endPosition) 83 | 84 | const completeOptions = { 85 | shouldAddLeftQuote: true, 86 | shouldAddRightQuote: true, 87 | shouldAddLeadingSpace: true, 88 | shoudlAddTrailingComma: false, 89 | } 90 | 91 | return getCompletionList(completionList.parentPath, varList, range, document.languageId, completeOptions) 92 | } 93 | } 94 | 95 | // For string nodes that have a parent that is a property node 96 | if (node && node.parent && isPropertyNode(node.parent)) { 97 | const isValueNode = node.parent.valueNode?.offset === node.offset 98 | if (isStringNode(node) && isValueNode) { 99 | if (varList.length) { 100 | // Text edit will only work when start position is higher than the node offset 101 | const startPosition = document.positionAt(node.offset + 1) 102 | const endPosition = document.positionAt(node.offset + node.length - 1) 103 | 104 | const range = Range.create(startPosition, endPosition) 105 | 106 | const completeStateNameOptions = { 107 | shouldAddLeftQuote: false, 108 | shouldAddRightQuote: false, 109 | shouldAddLeadingSpace: false, 110 | shoudlAddTrailingComma: false, 111 | } 112 | return getCompletionList( 113 | completionList.parentPath, 114 | varList, 115 | range, 116 | document.languageId, 117 | completeStateNameOptions, 118 | ) 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/completion/utils/jsonataUtils.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | import { 6 | JSONATA_TEMPLATE_WRAPPER, 7 | JsonataFunctionsMap, 8 | ExprNode, 9 | getJSONataAST, 10 | getJSONataFunctionList, 11 | findNodeInJSONataAST, 12 | JSONataASTResult, 13 | } from '../../asl-utils' 14 | import { ASTNode, Position, TextDocument } from 'vscode-json-languageservice' 15 | 16 | export async function getJSONataNodeData( 17 | document: TextDocument, 18 | offset: number, 19 | node: ASTNode, 20 | ): Promise< 21 | | { 22 | jsonataNode: JSONataASTResult 23 | nodePosition: Position 24 | jsonataNodePosition: number 25 | jsonataFunctions: JsonataFunctionsMap 26 | } 27 | | undefined 28 | > { 29 | const { start: JSONATA_PREFIX, end: JSONATA_SUFFIX } = JSONATA_TEMPLATE_WRAPPER 30 | 31 | const nodeValue = node.value?.toString() 32 | 33 | if (!nodeValue || !nodeValue.startsWith(JSONATA_PREFIX) || !nodeValue.endsWith(JSONATA_SUFFIX)) { 34 | return 35 | } 36 | 37 | const cursorPosition = document.positionAt(offset) 38 | const nodePosition = document.positionAt(node.offset) 39 | 40 | const positionInString = cursorPosition.character - nodePosition.character - JSONATA_PREFIX.length - 1 41 | 42 | let jsonataFunctions: JsonataFunctionsMap 43 | let jsonataAst: ExprNode 44 | try { 45 | const jsonataStringCursorPosition = positionInString + JSONATA_PREFIX.length 46 | 47 | ;[jsonataAst, jsonataFunctions] = await Promise.all([ 48 | getJSONataAST(nodeValue.slice(JSONATA_PREFIX.length, jsonataStringCursorPosition)), 49 | getJSONataFunctionList(), 50 | ]) 51 | } catch (_err) { 52 | return 53 | } 54 | 55 | const jsonataNode = findNodeInJSONataAST(jsonataAst, positionInString) 56 | const jsonataNodePosition = jsonataNode?.node.position || jsonataNode?.node.error?.position 57 | 58 | if (!jsonataNode || !jsonataNodePosition) { 59 | return 60 | } 61 | 62 | return { 63 | jsonataNode, 64 | nodePosition, 65 | jsonataNodePosition, 66 | jsonataFunctions, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/completion/utils/variableUtils.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | import { Asl, MonacoCompletionsResult, getAssignCompletionList, getCompletionStrings, StateType } from '../../asl-utils' 6 | import { ASTNode } from 'vscode-json-languageservice' 7 | import { getStateInfo, findClosestAncestorNodeByName } from '../../utils/astUtilityFunctions' 8 | 9 | const FIELDS_INPUT = ['Parameters', 'InputPath', 'Arguments'] 10 | const FIELDS_SUCCESS = ['Output', 'Assign', 'ResultSelector', 'OutputPath'] 11 | const FIELDS_FAIL = ['Catch'] 12 | const FIELDS_MAP = ['ItemSelector'] 13 | 14 | const FIELDS_WITH_TEMPLATE = [...FIELDS_INPUT, ...FIELDS_SUCCESS, ...FIELDS_FAIL, ...FIELDS_MAP] 15 | 16 | export function getVariableCompletions( 17 | node: ASTNode, 18 | value: string | undefined, 19 | asl: Asl, 20 | ): 21 | | { 22 | varList: string[] 23 | completionList: MonacoCompletionsResult 24 | } 25 | | undefined { 26 | const { stateName, stateType } = getStateInfo(node) || {} 27 | // cannot generate a list of available varialbes if not inside a state 28 | if (!stateName) { 29 | return 30 | } 31 | 32 | const supportedAncestorNode = findClosestAncestorNodeByName(node, FIELDS_WITH_TEMPLATE) 33 | const isError = !!findClosestAncestorNodeByName(node, FIELDS_FAIL) 34 | if (!supportedAncestorNode) { 35 | return 36 | } 37 | 38 | const availableVariables = getAssignCompletionList(asl, stateName, stateName) 39 | 40 | const completionList = getCompletionStrings({ 41 | nodeVal: value || '', 42 | completionScope: availableVariables, 43 | reservedVariablesParams: { 44 | isError: isError, 45 | isSuccess: !isError && FIELDS_SUCCESS.includes(supportedAncestorNode.nodeName), 46 | isItemSelector: FIELDS_MAP.includes(supportedAncestorNode.nodeName), 47 | stateType: (stateType as StateType) || 'Task', // default to task node input if state type is undetermined 48 | }, 49 | }) 50 | const varList: string[] = completionList.items 51 | return { varList, completionList } 52 | } 53 | -------------------------------------------------------------------------------- /src/constants/constants.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export const LANGUAGE_IDS = { 7 | YAML: 'asl-yaml', 8 | JSON: 'asl', 9 | } as const 10 | 11 | export const FILE_EXTENSIONS = { 12 | YAML: 'asl.yaml', 13 | JSON: 'asl', 14 | } as const 15 | -------------------------------------------------------------------------------- /src/constants/diagnosticStrings.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export const MESSAGES = { 7 | INVALID_NEXT: 'The value of "Next" property must be the name of an existing state.', 8 | INVALID_DEFAULT: 'The value of "Default" property must be the name of an existing state.', 9 | INVALID_START_AT: 'The value of "StartAt" property must be the name of an existing state.', 10 | INVALID_JSON_PATH_OR_INTRINSIC: 'The value for the field must be a valid JSONPath or intrinsic function expression.', 11 | INVALID_JSON_PATH_OR_INTRINSIC_STRING_ONLY: 12 | 'The value for the field must be a valid JSONPath or an intrinsic function expression that returns a string (ArrayGetItem, Base64Encode, Base64Decode, Format, JsonToString, Hash, or UUID).', 13 | UNREACHABLE_STATE: 'The state cannot be reached. It must be referenced by at least one other state.', 14 | NO_TERMINAL_STATE: 15 | 'No terminal state. The state machine must have at least one terminal state (a state in which the "End" property is set to true).', 16 | INVALID_PROPERTY_NAME: 'Field is not supported.', 17 | MUTUALLY_EXCLUSIVE_CHOICE_PROPERTIES: 'Each Choice Rule can only have one comparison operator.', 18 | } as const 19 | 20 | export const YAML_PARSER_MESSAGES = { 21 | DUPLICATE_KEY: 'duplicate key', 22 | } as const 23 | -------------------------------------------------------------------------------- /src/json-schema/partial/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "Version": { 5 | "type": "string", 6 | "description": "The version of the Amazon States Language used in the state machine.", 7 | "enum": ["1.0"] 8 | }, 9 | "Comment": { 10 | "$ref": "common.json#/$defs/comment" 11 | }, 12 | "TimeoutSeconds": { 13 | "$ref": "common.json#/$defs/seconds", 14 | "description": "The maximum number of seconds an execution of the state machine can run. If it runs longer than the specified time, the execution fails with a States.Timeout error." 15 | }, 16 | "StartAt": { 17 | "type": "string", 18 | "description": "A string that must exactly match (is case sensitive) the name of a state.", 19 | "minLength": 1 20 | }, 21 | "States": { 22 | "$ref": "states.json#/$defs/states" 23 | }, 24 | "QueryLanguage": { 25 | "$ref": "common.json#/$defs/queryLanguage" 26 | } 27 | }, 28 | "required": ["States", "StartAt"], 29 | "$defs": {} 30 | } 31 | -------------------------------------------------------------------------------- /src/json-schema/partial/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "basics": { 4 | "type": "object", 5 | "properties": { 6 | "Type": { 7 | "type": "string", 8 | "description": "The state's type.", 9 | "minLength": 1, 10 | "enum": ["Pass", "Succeed", "Fail", "Task", "Choice", "Wait", "Parallel", "Map"] 11 | }, 12 | "QueryLanguage": { 13 | "$ref": "#/$defs/queryLanguage" 14 | }, 15 | "Comment": { 16 | "$ref": "#/$defs/comment" 17 | } 18 | }, 19 | "required": ["Type"] 20 | }, 21 | "inputOutputResult": { 22 | "properties": { 23 | "InputPath": { 24 | "description": "A path that selects a portion of the state's input to be passed to the state's task for processing. If omitted, it has the value $ which designates the entire input.", 25 | "oneOf": [ 26 | { 27 | "type": "string" 28 | }, 29 | { 30 | "type": "null" 31 | } 32 | ] 33 | }, 34 | "ResultPath": { 35 | "description": "A path that specifies where to place the Result, relative to the raw input. If the raw input has a field at the location addressed by the ResultPath value then in the output that field is discarded and overwritten by the state's result. Otherwise, a new field is created in the state output, with intervening fields constructed as necessary.", 36 | "oneOf": [ 37 | { 38 | "$ref": "#/$defs/referencePath" 39 | }, 40 | { 41 | "type": "null" 42 | } 43 | ] 44 | }, 45 | "OutputPath": { 46 | "description": "A path that selects a portion of the state's input to be passed to the state's output. If omitted, it has the value $ which designates the entire input.", 47 | "oneOf": [ 48 | { 49 | "type": "string" 50 | }, 51 | { 52 | "type": "null" 53 | } 54 | ] 55 | } 56 | } 57 | }, 58 | "inputOutput": { 59 | "properties": { 60 | "InputPath": { 61 | "description": "A path that selects a portion of the state's input to be passed to the state's task for processing. If omitted, it has the value $ which designates the entire input.", 62 | "oneOf": [ 63 | { 64 | "type": "string" 65 | }, 66 | { 67 | "type": "null" 68 | } 69 | ] 70 | }, 71 | "OutputPath": { 72 | "description": "A path that selects a portion of the state's input to be passed to the state's output. If omitted, it has the value $ which designates the entire input.", 73 | "oneOf": [ 74 | { 75 | "type": "string" 76 | }, 77 | { 78 | "type": "null" 79 | } 80 | ] 81 | } 82 | } 83 | }, 84 | "queryLanguage": { 85 | "type": "string", 86 | "description": "ASL mode configuration. Specify either JSONata or JSONPath.", 87 | "minLength": 1, 88 | "enum": ["JSONata", "JSONPath"] 89 | }, 90 | "comment": { 91 | "type": "string", 92 | "description": "A human-readable comment or description.", 93 | "minLength": 1 94 | }, 95 | "endOrTransition": { 96 | "oneOf": [ 97 | { 98 | "required": ["Next"] 99 | }, 100 | { 101 | "required": ["End"] 102 | } 103 | ], 104 | "properties": { 105 | "Next": { 106 | "type": "string", 107 | "description": "The name of the next state that is run when the current state finishes.", 108 | "minLength": 1 109 | }, 110 | "End": { 111 | "type": "boolean", 112 | "description": "Designates this state as a terminal state (ends the execution) if set to true. There can be any number of terminal states per state machine." 113 | } 114 | } 115 | }, 116 | "referencePath": { 117 | "type": "string", 118 | "minLength": 1 119 | }, 120 | "timestamp": { 121 | "type": "string", 122 | "format": "date-time" 123 | }, 124 | "errors": { 125 | "type": "array", 126 | "minItems": 1, 127 | "items": { 128 | "type": "string" 129 | } 130 | }, 131 | "seconds": { 132 | "type": "integer", 133 | "minimum": 0, 134 | "maximum": 99999999 135 | }, 136 | "path": { 137 | "type": "string", 138 | "pattern": "\\$.*" 139 | }, 140 | "jsonata": { 141 | "type": "string", 142 | "pattern": "^{%(.*)%}$" 143 | }, 144 | "parameters": { 145 | "type": ["number", "string", "boolean", "object", "array", "null"] 146 | }, 147 | "resultSelector": { 148 | "type": "object" 149 | }, 150 | "retry": { 151 | "type": "array", 152 | "description": "An array of objects, called Retriers, that define a retry policy if the state encounters runtime errors.", 153 | "items": { 154 | "type": "object", 155 | "properties": { 156 | "ErrorEquals": { 157 | "$ref": "#/$defs/errors", 158 | "description": "A non-empty array of Error Names. The retry policy for this Retrier is implemented if the reported error matches one of the Error Names." 159 | }, 160 | "IntervalSeconds": { 161 | "$ref": "#/$defs/seconds", 162 | "description": "A positive integer representing the number of seconds before the first retry attempt. (Default: 1, Maximum: 99999999)" 163 | }, 164 | "MaxAttempts": { 165 | "type": "integer", 166 | "description": "A non-negative integer representing the maximum number of retry attempts. (Default: 3, Maximum: 99999999)", 167 | "minimum": 0, 168 | "maximum": 99999999 169 | }, 170 | "BackoffRate": { 171 | "type": "number", 172 | "description": "A number (>= 1) which is the multiplier that increases the retry interval on each attempt. (Default: 2.0)", 173 | "minimum": 1 174 | }, 175 | "Comment": { 176 | "$ref": "#/$defs/comment" 177 | }, 178 | "MaxDelaySeconds": { 179 | "type": "integer", 180 | "description": "A positive integer representing the maximum number of seconds to wait before a retry attempt. If not specified, the limit is the maximum duration a state machine can run. (Maximum: 31622400)", 181 | "minimum": 1, 182 | "maximum": 31622400 183 | }, 184 | "JitterStrategy": { 185 | "type": "string", 186 | "description": "The Jitter Strategy to apply to the retry interval.", 187 | "minLength": 1, 188 | "enum": ["FULL", "NONE"] 189 | } 190 | }, 191 | "required": ["ErrorEquals"] 192 | } 193 | }, 194 | "catch": { 195 | "type": "array", 196 | "description": "An array of objects, called Catchers, that define a fallback state. This state is executed if the state encounters runtime errors and its retry policy is exhausted or isn't defined.", 197 | "items": { 198 | "type": "object", 199 | "properties": { 200 | "ErrorEquals": { 201 | "$ref": "#/$defs/errors", 202 | "description": "A non-empty array of Error Names. The state machine transitions to this Catcher's fallback state if the reported error matches one of the Error Names." 203 | }, 204 | "Next": { 205 | "type": "string", 206 | "description": "The state to transition to on Error Name match.", 207 | "minLength": 1 208 | }, 209 | "ResultPath": { 210 | "oneOf": [ 211 | { 212 | "$ref": "#/$defs/referencePath", 213 | "description": "A path that specifies where to place the Result, relative to the raw input. If the raw input has a field at the location addressed by the ResultPath value then in the output that field is discarded and overwritten by the state's result. Otherwise, a new field is created in the state output, with intervening fields constructed as necessary." 214 | }, 215 | { 216 | "type": "null" 217 | } 218 | ] 219 | }, 220 | "Output": { 221 | "$ref": "#/$defs/output" 222 | }, 223 | "Comment": { 224 | "$ref": "#/$defs/comment" 225 | }, 226 | "Assign": { 227 | "$ref": "#/$defs/assign" 228 | } 229 | }, 230 | "required": ["ErrorEquals", "Next"] 231 | } 232 | }, 233 | "arguments": { 234 | "oneOf": [ 235 | { 236 | "type": "object", 237 | "errorMessage": "Incorrect type. Expected one of object, JSONata expression." 238 | }, 239 | { 240 | "$ref": "common.json#/$defs/jsonata" 241 | } 242 | ], 243 | "description": "An object or JSONata expression to define the Argument of this task state." 244 | }, 245 | "output": { 246 | "type": ["number", "string", "boolean", "object", "array", "null"], 247 | "description": "A value to define the Output configuration of this task state." 248 | }, 249 | "assign": { 250 | "type": "object", 251 | "description": "An object to define the variables to be assigned." 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/json-schema/partial/fail_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "failState": { 4 | "description": "A Fail state stops the execution of the state machine and marks it as a failure.", 5 | "allOf": [ 6 | { 7 | "$ref": "common.json#/$defs/basics" 8 | }, 9 | { 10 | "properties": { 11 | "Type": { 12 | "enum": ["Fail"] 13 | }, 14 | "Comment": { 15 | "$ref": "common.json#/$defs/comment" 16 | }, 17 | "Error": { 18 | "type": "string", 19 | "description": "Provides an error name that can be used for error handling (Retry/Catch), operational, or diagnostic purposes.", 20 | "minLength": 1 21 | }, 22 | "Cause": { 23 | "type": "string", 24 | "description": "Provides a custom failure string that can be used for operational or diagnostic purposes.", 25 | "minLength": 1 26 | }, 27 | "ErrorPath": { 28 | "type": "string", 29 | "description": "Provides an error name that can be used for error handling (Retry/Catch), operational, or diagnostic purposes. Specified with JsonPath syntax or with Intrinsic Functions. The resulting value must be a string.", 30 | "minLength": 1 31 | }, 32 | "CausePath": { 33 | "type": "string", 34 | "description": "Provides a custom failure string that can be used for operational or diagnostic purposes. Specified with JsonPath syntax or with Intrinsic Functions. The resulting value must be a string.", 35 | "minLength": 1 36 | } 37 | } 38 | } 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/json-schema/partial/parallel_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "parallelState": { 4 | "description": "The Parallel state can be used to create parallel branches of execution in your state machine.", 5 | "allOf": [ 6 | { 7 | "$ref": "common.json#/$defs/basics" 8 | }, 9 | { 10 | "$ref": "common.json#/$defs/inputOutputResult" 11 | }, 12 | { 13 | "$ref": "common.json#/$defs/endOrTransition" 14 | }, 15 | { 16 | "properties": { 17 | "Type": { 18 | "enum": ["Parallel"] 19 | }, 20 | "Parameters": { 21 | "$ref": "common.json#/$defs/parameters", 22 | "description": "Used to pass information to the API actions of connected resources. The Parameters can use a mix of static JSON, JsonPath and intrinsic functions." 23 | }, 24 | "ResultSelector": { 25 | "$ref": "common.json#/$defs/resultSelector", 26 | "description": "Used to transform the result. The ResultSelector can use a mix of static JSON, JsonPath and intrinsic functions." 27 | }, 28 | "Branches": { 29 | "type": "array", 30 | "description": "An array of objects that specifies state machines to execute in parallel.", 31 | "minItems": 1, 32 | "items": { 33 | "type": "object", 34 | "properties": { 35 | "StartAt": { 36 | "type": "string", 37 | "description": "A string that must exactly match (is case sensitive) the name of one of the state objects." 38 | }, 39 | "States": { 40 | "$ref": "states.json#/$defs/states" 41 | } 42 | }, 43 | "required": ["StartAt", "States"] 44 | } 45 | }, 46 | "Retry": { 47 | "$ref": "common.json#/$defs/retry", 48 | "description": "Contains an array of objects, called Retriers, that define a retry policy if the state encounters runtime errors." 49 | }, 50 | "Catch": { 51 | "$ref": "common.json#/$defs/catch", 52 | "description": "Contains an array of objects, called Catchers, that define a fallback state. This state is executed if the state encounters runtime errors and its retry policy is exhausted or isn't defined." 53 | }, 54 | "Arguments": { 55 | "$ref": "common.json#/$defs/arguments" 56 | }, 57 | "Output": { 58 | "$ref": "common.json#/$defs/output" 59 | } 60 | }, 61 | "required": ["Branches"] 62 | } 63 | ] 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/json-schema/partial/pass_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "passState": { 4 | "description": "A Pass state passes its input to its output, without performing work. Pass states are useful when constructing and debugging state machines.", 5 | "allOf": [ 6 | { 7 | "$ref": "common.json#/$defs/basics" 8 | }, 9 | { 10 | "$ref": "common.json#/$defs/inputOutputResult" 11 | }, 12 | { 13 | "$ref": "common.json#/$defs/endOrTransition" 14 | }, 15 | { 16 | "properties": { 17 | "Type": { 18 | "enum": ["Pass"] 19 | }, 20 | "Parameters": { 21 | "$ref": "common.json#/$defs/parameters", 22 | "description": "Used to pass information to the API actions of connected resources. The Parameters can use a mix of static JSON, JsonPath and intrinsic functions." 23 | }, 24 | "Output": { 25 | "$ref": "common.json#/$defs/output" 26 | } 27 | } 28 | } 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/json-schema/partial/states.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "states": { 4 | "type": "object", 5 | "description": "An object containing a comma-delimited set of states.", 6 | "additionalProperties": { 7 | "oneOf": [ 8 | { 9 | "$ref": "pass_state.json#/$defs/passState" 10 | }, 11 | { 12 | "$ref": "succeed_state.json#/$defs/succeedState" 13 | }, 14 | { 15 | "$ref": "fail_state.json#/$defs/failState" 16 | }, 17 | { 18 | "$ref": "task_state.json#/$defs/taskState" 19 | }, 20 | { 21 | "$ref": "choice_state.json#/$defs/choiceState" 22 | }, 23 | { 24 | "$ref": "wait_state.json#/$defs/waitState" 25 | }, 26 | { 27 | "$ref": "parallel_state.json#/$defs/parallelState" 28 | }, 29 | { 30 | "$ref": "map_state.json#/$defs/mapState" 31 | } 32 | ] 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/json-schema/partial/succeed_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "succeedState": { 4 | "description": "A Succeed state stops an execution successfully. The Succeed state is a useful target for Choice state branches that don't do anything but stop the execution.", 5 | "allOf": [ 6 | { 7 | "$ref": "common.json#/$defs/basics" 8 | }, 9 | { 10 | "$ref": "common.json#/$defs/inputOutput" 11 | }, 12 | { 13 | "properties": { 14 | "Type": { 15 | "enum": ["Succeed"] 16 | }, 17 | "Output": { 18 | "$ref": "common.json#/$defs/output" 19 | } 20 | } 21 | } 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/json-schema/partial/task_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "taskState": { 4 | "description": "A Task state represents a single unit of work performed by a state machine.\nAll work in your state machine is done by tasks. A task performs work identified by the state’s Resource field, which is often an AWS Lambda function or other Step Functions service integrations.", 5 | "allOf": [ 6 | { 7 | "$ref": "common.json#/$defs/basics" 8 | }, 9 | { 10 | "$ref": "common.json#/$defs/inputOutputResult" 11 | }, 12 | { 13 | "$ref": "common.json#/$defs/endOrTransition" 14 | }, 15 | { 16 | "properties": { 17 | "Type": { 18 | "enum": ["Task"] 19 | }, 20 | "Resource": { 21 | "type": "string", 22 | "description": "A URI, especially an ARN that uniquely identifies the specific task to execute.", 23 | "examples": [ 24 | "arn:aws:states:::batch:submitJob", 25 | "arn:aws:states:::batch:submitJob.sync", 26 | "arn:aws:states:::dynamodb:deleteItem", 27 | "arn:aws:states:::dynamodb:getItem", 28 | "arn:aws:states:::dynamodb:putItem", 29 | "arn:aws:states:::dynamodb:updateItem", 30 | "arn:aws:states:::ecs:runTask", 31 | "arn:aws:states:::ecs:runTask.sync", 32 | "arn:aws:states:::ecs:runTask.waitForTaskToken", 33 | "arn:aws:states:::elasticmapreduce:addStep", 34 | "arn:aws:states:::elasticmapreduce:addStep.sync", 35 | "arn:aws:states:::elasticmapreduce:cancelStep", 36 | "arn:aws:states:::elasticmapreduce:createCluster", 37 | "arn:aws:states:::elasticmapreduce:createCluster.sync", 38 | "arn:aws:states:::elasticmapreduce:modifyInstanceFleetByName", 39 | "arn:aws:states:::elasticmapreduce:modifyInstanceGroupByName", 40 | "arn:aws:states:::elasticmapreduce:setClusterTerminationProtection", 41 | "arn:aws:states:::elasticmapreduce:terminateCluster", 42 | "arn:aws:states:::elasticmapreduce:terminateCluster.sync", 43 | "arn:aws:states:::events:putEvents", 44 | "arn:aws:states:::events:putEvents.waitForTaskToken", 45 | "arn:aws:states:::glue:startJobRun", 46 | "arn:aws:states:::glue:startJobRun.sync", 47 | "arn:aws:states:::lambda:invoke", 48 | "arn:aws:states:::lambda:invoke.waitForTaskToken", 49 | "arn:aws:states:::mediaconvert:createJob", 50 | "arn:aws:states:::mediaconvert:createJob.sync", 51 | "arn:aws:states:::sagemaker:createEndpoint", 52 | "arn:aws:states:::sagemaker:createEndpointConfig", 53 | "arn:aws:states:::sagemaker:createHyperParameterTuningJob", 54 | "arn:aws:states:::sagemaker:createHyperParameterTuningJob.sync", 55 | "arn:aws:states:::sagemaker:createLabelingJob", 56 | "arn:aws:states:::sagemaker:createLabelingJob.sync", 57 | "arn:aws:states:::sagemaker:createModel", 58 | "arn:aws:states:::sagemaker:createTrainingJob", 59 | "arn:aws:states:::sagemaker:createTrainingJob.sync", 60 | "arn:aws:states:::sagemaker:createTransformJob", 61 | "arn:aws:states:::sagemaker:createTransformJob.sync", 62 | "arn:aws:states:::sagemaker:updateEndpoint", 63 | "arn:aws:states:::sns:publish", 64 | "arn:aws:states:::sns:publish.waitForTaskToken", 65 | "arn:aws:states:::sqs:sendMessage", 66 | "arn:aws:states:::sqs:sendMessage.waitForTaskToken", 67 | "arn:aws:states:::states:startExecution", 68 | "arn:aws:states:::states:startExecution.sync", 69 | "arn:aws:states:::states:startExecution.waitForTaskToken", 70 | "arn:aws:states:::http:invoke" 71 | ] 72 | }, 73 | "Parameters": { 74 | "$ref": "common.json#/$defs/parameters", 75 | "description": "Used to pass information to the API actions of connected resources. The Parameters can use a mix of static JSON, JsonPath and intrinsic functions." 76 | }, 77 | "ResultSelector": { 78 | "$ref": "common.json#/$defs/resultSelector", 79 | "description": "Used to transform the result. The ResultSelector can use a mix of static JSON, JsonPath and intrinsic functions." 80 | }, 81 | "TimeoutSeconds": { 82 | "oneOf": [ 83 | { 84 | "$ref": "common.json#/$defs/seconds", 85 | "errorMessage": "Incorrect type. Expected one of integer, JSONata expression." 86 | }, 87 | { 88 | "$ref": "common.json#/$defs/jsonata" 89 | } 90 | ], 91 | "description": "If the task runs longer than the specified seconds, this state fails with a States.Timeout error name. Must be a positive, non-zero integer, or a jsonata expression. If not provided, the default value is 99999999. The count begins after the task has been started, for example, when ActivityStarted or LambdaFunctionStarted are logged in the Execution event history." 92 | }, 93 | "TimeoutSecondsPath": { 94 | "$ref": "common.json#/$defs/path", 95 | "description": "If the task runs longer than the specified seconds, this state fails with a States.Timeout error. Specified using JsonPath syntax, to select the value from the state's input data." 96 | }, 97 | "HeartbeatSeconds": { 98 | "oneOf": [ 99 | { 100 | "$ref": "common.json#/$defs/seconds", 101 | "errorMessage": "Incorrect type. Expected one of integer, JSONata expression." 102 | }, 103 | { 104 | "$ref": "common.json#/$defs/jsonata" 105 | } 106 | ], 107 | "description": "If more time than the specified seconds elapses between heartbeats from the task, this state fails with a States.Timeout error name. Must be a positive, non-zero integer less than the number of seconds specified in the TimeoutSeconds field, or a jsonata expression. If not provided, the default value is 99999999. For Activities, the count begins when GetActivityTask receives a token and ActivityStarted is logged in the Execution event history." 108 | }, 109 | "HeartbeatSecondsPath": { 110 | "$ref": "common.json#/$defs/path", 111 | "description": "If more time than the specified seconds elapses between heartbeats from the task, this state fails with a States.Timeout error. Specified using JsonPath syntax, to select the value from the state's input data." 112 | }, 113 | "Retry": { 114 | "$ref": "common.json#/$defs/retry", 115 | "description": "An array of objects, called Retriers, that define a retry policy if the state encounters runtime errors." 116 | }, 117 | "Catch": { 118 | "$ref": "common.json#/$defs/catch", 119 | "description": "An array of objects, called Catchers, that define a fallback state. This state is executed if the state encounters runtime errors and its retry policy is exhausted or isn't defined." 120 | }, 121 | "Arguments": { 122 | "$ref": "common.json#/$defs/arguments" 123 | }, 124 | "Output": { 125 | "$ref": "common.json#/$defs/output" 126 | }, 127 | "Assign": { 128 | "$ref": "common.json#/$defs/assign", 129 | "description": "An object to define the variables to be assigned." 130 | } 131 | }, 132 | "required": ["Resource"] 133 | } 134 | ] 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/json-schema/partial/wait_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "waitState": { 4 | "description": "A Wait state delays the state machine from continuing for a specified time. You can choose either a relative time, specified in seconds from when the state begins, or an absolute end time, specified as a timestamp.", 5 | "allOf": [ 6 | { 7 | "$ref": "common.json#/$defs/basics" 8 | }, 9 | { 10 | "$ref": "common.json#/$defs/inputOutput" 11 | }, 12 | { 13 | "$ref": "common.json#/$defs/endOrTransition" 14 | }, 15 | { 16 | "properties": { 17 | "Type": { 18 | "enum": ["Wait"] 19 | }, 20 | "Seconds": { 21 | "oneOf": [ 22 | { 23 | "$ref": "common.json#/$defs/seconds", 24 | "errorMessage": "Incorrect type. Expected one of integer, JSONata expression." 25 | }, 26 | { 27 | "$ref": "common.json#/$defs/jsonata" 28 | } 29 | ], 30 | "description": "A time, in seconds, to wait before beginning the state specified in the \"Next\" field." 31 | }, 32 | "Timestamp": { 33 | "oneOf": [ 34 | { 35 | "$ref": "common.json#/$defs/timestamp", 36 | "errorMessage": "Incorrect type. Expected one of timestamp, JSONata expression." 37 | }, 38 | { 39 | "$ref": "common.json#/$defs/jsonata" 40 | } 41 | ], 42 | "description": "An absolute time to wait until beginning the state specified in the \"Next\" field.\n\nTimestamps must conform to the RFC3339 profile of ISO 8601, with the further restrictions that an uppercase T must separate the date and time portions, and an uppercase Z must denote that a numeric time zone offset is not present, for example, 2016-08-18T17:33:00Z." 43 | }, 44 | "SecondsPath": { 45 | "$ref": "common.json#/$defs/referencePath", 46 | "description": "A time, in seconds, to wait before beginning the state specified in the \"Next\" field, specified using a path from the state's input data." 47 | }, 48 | "TimestampPath": { 49 | "$ref": "common.json#/$defs/referencePath", 50 | "description": "An absolute time to wait until beginning the state specified in the \"Next\" field, specified using a path from the state's input data." 51 | }, 52 | "Output": { 53 | "$ref": "common.json#/$defs/output" 54 | } 55 | }, 56 | "oneOf": [ 57 | { 58 | "required": ["Type", "Seconds"] 59 | }, 60 | { 61 | "required": ["Type", "Timestamp"] 62 | }, 63 | { 64 | "required": ["Type", "SecondsPath"] 65 | }, 66 | { 67 | "required": ["Type", "TimestampPath"] 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/jsonLanguageService.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { 7 | Diagnostic, 8 | DiagnosticSeverity, 9 | getLanguageService as getLanguageServiceVscode, 10 | JSONSchema, 11 | LanguageService, 12 | LanguageServiceParams, 13 | } from 'vscode-json-languageservice' 14 | import { ASLOptions, ASTTree, isObjectNode } from './utils/astUtilityFunctions' 15 | import aslSchema from './json-schema/bundled.json' 16 | import { LANGUAGE_IDS } from './constants/constants' 17 | import validateStates, { RootType } from './validation/validateStates' 18 | import completeAsl from './completion/completeAsl' 19 | 20 | export interface ASLLanguageServiceParams extends LanguageServiceParams { 21 | aslOptions?: ASLOptions 22 | } 23 | 24 | export const ASL_SCHEMA = aslSchema as JSONSchema 25 | export const doCompleteAsl = completeAsl 26 | 27 | export const getLanguageService = function (params: ASLLanguageServiceParams): LanguageService { 28 | const builtInParams = {} 29 | 30 | const languageService = getLanguageServiceVscode({ ...params, ...builtInParams }) 31 | const doValidation = languageService.doValidation.bind(languageService) as typeof languageService.doValidation 32 | const doComplete = languageService.doComplete.bind(languageService) as typeof languageService.doComplete 33 | 34 | languageService.configure({ 35 | validate: true, 36 | allowComments: false, 37 | schemas: [ 38 | { 39 | uri: LANGUAGE_IDS.JSON, 40 | fileMatch: ['*'], 41 | schema: ASL_SCHEMA, 42 | }, 43 | ], 44 | }) 45 | 46 | languageService.doValidation = async function (document, jsonDocument, documentSettings) { 47 | // vscode-json-languageservice will always set severity as warning for JSONSchema validation 48 | // there is no option to configure this behavior so severity needs to be overwritten as error 49 | const diagnostics = (await doValidation(document, jsonDocument, documentSettings)).map((diagnostic) => { 50 | // Non JSON Schema validation will have source: 'asl' 51 | if (diagnostic.source !== LANGUAGE_IDS.JSON) { 52 | return { ...diagnostic, severity: DiagnosticSeverity.Error } 53 | } 54 | 55 | return diagnostic 56 | }) as Diagnostic[] 57 | 58 | const rootNode = (jsonDocument as ASTTree).root 59 | 60 | if (rootNode && isObjectNode(rootNode)) { 61 | const aslDiagnostics = validateStates(rootNode, document, RootType.Root, params.aslOptions) 62 | 63 | return diagnostics.concat(aslDiagnostics) 64 | } 65 | 66 | return diagnostics 67 | } 68 | 69 | languageService.doComplete = async function (document, position, doc) { 70 | const jsonCompletions = await doComplete(document, position, doc) 71 | 72 | return completeAsl(document, position, doc, jsonCompletions, params.aslOptions) 73 | } 74 | 75 | return languageService 76 | } 77 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export * from 'vscode-json-languageservice' 7 | 8 | export * from './yamlLanguageService' 9 | export { getLanguageService, doCompleteAsl, ASL_SCHEMA, ASLLanguageServiceParams } from './jsonLanguageService' 10 | -------------------------------------------------------------------------------- /src/snippets/error_handling.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Retry", 4 | "body": [ 5 | "\"Retry\": [", 6 | "\t{", 7 | "\t\t\"ErrorEquals\": [", 8 | "\t\t\t\"ErrorA\",", 9 | "\t\t\t\"ErrorB\"", 10 | "\t\t],", 11 | "\t\t\"IntervalSeconds\": 1,", 12 | "\t\t\"BackoffRate\": 2,", 13 | "\t\t\"MaxAttempts\": 2,", 14 | "\t\t\"MaxDelaySeconds\": 10,", 15 | "\t\t\"JitterStrategy\": \"FULL\"", 16 | "\t},", 17 | "\t{", 18 | "\t\t\"ErrorEquals\": [", 19 | "\t\t\t\"ErrorC\"", 20 | "\t\t],", 21 | "\t\t\"IntervalSeconds\": 5", 22 | "\t}", 23 | "]" 24 | ], 25 | "description": "Code snippet for a Retry field.\n\nUse a Retry field to retry a task state after an error occurs." 26 | }, 27 | { 28 | "name": "Catch", 29 | "body": [ 30 | "\"Catch\": [", 31 | "\t{", 32 | "\t\t\"ErrorEquals\": [", 33 | "\t\t\t\"${2:States.ALL}\"", 34 | "\t\t],", 35 | "\t\t\"Next\": \"${1:NextState}\"", 36 | "\t}", 37 | "]" 38 | ], 39 | "description": "Code snippet for a Catch field.\n\nUse a Catch field to catch errors and revert to a fallback state." 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /src/snippets/states.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Pass State", 4 | "body": [ 5 | "\"${1:PassState}\": {", 6 | "\t\"Type\": \"Pass\",", 7 | "\t\"Result\": {", 8 | "\t\t\"data1\": 0.5,", 9 | "\t\t\"data2\": 1.5", 10 | "\t},", 11 | "\t\"ResultPath\": \"$.result\",", 12 | "\t\"Next\": \"${2:NextState}\"", 13 | "}" 14 | ], 15 | "description": "Code snippet for a Pass state.\n\nA Pass state passes its input to its output, without performing work." 16 | }, 17 | { 18 | "name": "Lambda Task State", 19 | "body": [ 20 | "\"${1:Invoke Lambda function}\": {", 21 | "\t\"Type\": \"Task\",", 22 | "\t\"Resource\": \"arn:aws:states:::lambda:invoke\",", 23 | "\t\"Parameters\": {", 24 | "\t\t\"FunctionName\": \"${3:arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME}\",", 25 | "\t\t\"Payload\": {", 26 | "\t\t\t\"Input.$\": \"$\"", 27 | "\t\t}", 28 | "\t},", 29 | "\t\"Next\": \"${2:NextState}\"", 30 | "}" 31 | ], 32 | "description": "Code snippet for a Lambda Task state.\n\nCalls the AWS Lambda Invoke API to invoke a function." 33 | }, 34 | { 35 | "name": "EventBridge Task State", 36 | "body": [ 37 | "\"${1:Send an EventBridge custom event}\": {", 38 | "\t\"Type\": \"Task\",", 39 | "\t\"Resource\": \"arn:aws:states:::events:putEvents\",", 40 | "\t\"Parameters\": {", 41 | "\t\t\"Entries\": [", 42 | "\t\t\t{", 43 | "\t\t\t\t\"Detail\": {", 44 | "\t\t\t\t\t\"Message\": \"${4:Hello from Step Functions!}\"", 45 | "\t\t\t\t},", 46 | "\t\t\t\t\"DetailType\": \"${5:MyDetailType}\",", 47 | "\t\t\t\t\"EventBusName\": \"${6:MyEventBusName}\",", 48 | "\t\t\t\t\"Source\": \"${7:MySource}\"", 49 | "\t\t\t}", 50 | "\t\t]", 51 | "\t},", 52 | "\t\"Next\": \"${2:NextState}\"", 53 | "}" 54 | ], 55 | "description": "Code snippet for an EventBridge Task state.\n\nCalls the Amazon EventBridge PutEvents API to send a custom event to an event bus." 56 | }, 57 | { 58 | "name": "MediaConvert Task State", 59 | "body": [ 60 | "\"${1:Create a MediaConvert Transcoding Job}\": {", 61 | "\t\"Type\": \"Task\",", 62 | "\t\"Resource\": \"arn:aws:states:::mediaconvert:createJob\",", 63 | "\t\"Parameters\": {", 64 | "\t\t\"Role\": \"${3:arn:aws:iam::ACCOUNT_ID:role/MyServiceRole}\",", 65 | "\t\t\"Settings\": {", 66 | "\t\t\t\"Inputs\": [],", 67 | "\t\t\t\"OutputGroups\": []", 68 | "\t\t}", 69 | "\t},", 70 | "\t\"Next\": \"${2:NextState}\"", 71 | "}" 72 | ], 73 | "description": "Code snippet for an MediaConvert Create Job Task state.\n\nCalls the AWS MediaConvert CreateJob API to create a Transcoding Job." 74 | }, 75 | { 76 | "name": "SNS Task State", 77 | "body": [ 78 | "\"${1:Send message to SNS}\": {", 79 | "\t\"Type\": \"Task\",", 80 | "\t\"Resource\": \"arn:aws:states:::sns:publish\",", 81 | "\t\"Parameters\": {", 82 | "\t\t\"TopicArn\": \"${3:arn:aws:sns:REGION:ACCOUNT_ID:myTopic}\",", 83 | "\t\t\"Message\": {", 84 | "\t\t\t\"Input\": \"${4:Hello from Step Functions!}\"", 85 | "\t\t}", 86 | "\t},", 87 | "\t\"Next\": \"${2:NextState}\"", 88 | "}" 89 | ], 90 | "description": "Code snippet for an SNS Publish Task state.\n\nCalls the Amazon SNS Publish API to send a message to a destination." 91 | }, 92 | { 93 | "name": "Batch Task State", 94 | "body": [ 95 | "\"${1:Manage Batch task}\": {", 96 | "\t\"Type\": \"Task\",", 97 | "\t\"Resource\": \"arn:aws:states:::batch:submitJob.sync\",", 98 | "\t\"Parameters\": {", 99 | "\t\t\"JobDefinition\": \"${3:arn:aws:batch:REGION:ACCOUNT_ID:job-definition/testJobDefinition}\",", 100 | "\t\t\"JobName\": \"${4:myJobName}\",", 101 | "\t\t\"JobQueue\": \"${5:arn:aws:batch:REGION:ACCOUNT_ID:job-queue/testQueue}\"", 102 | "\t},", 103 | "\t\"Next\": \"${2:NextState}\"", 104 | "}" 105 | ], 106 | "description": "Code snippet for a Batch job Task state.\n\nCalls the AWS Batch SubmitJob API and resumes the execution once the job is complete." 107 | }, 108 | { 109 | "name": "ECS Task State", 110 | "body": [ 111 | "\"${1:Manage ECS task}\": {", 112 | "\t\"Type\": \"Task\",", 113 | "\t\"Resource\": \"arn:aws:states:::ecs:runTask.sync\",", 114 | "\t\"Parameters\": {", 115 | "\t\t\"LaunchType\": \"FARGATE\",", 116 | "\t\t\"Cluster\": \"${3:arn:aws:ecs:REGION:ACCOUNT_ID:cluster/MyECSCluster}\",", 117 | "\t\t\"TaskDefinition\": \"${4:arn:aws:ecs:REGION:ACCOUNT_ID:task-definition/MyTaskDefinition:1}\"", 118 | "\t},", 119 | "\t\"Next\": \"${2:NextState}\"", 120 | "}" 121 | ], 122 | "description": "Code snippet for an ECS RunTask Task state.\n\nCalls the Amazon ECS RunTask API and resumes the execution once the ECS task is complete." 123 | }, 124 | { 125 | "name": "SQS Task State", 126 | "body": [ 127 | "\"${1:Send message to SQS}\": {", 128 | "\t\"Type\": \"Task\",", 129 | "\t\"Resource\": \"arn:aws:states:::sqs:sendMessage\",", 130 | "\t\"Parameters\": {", 131 | "\t\t\"QueueUrl\": \"${3:https://sqs.REGION.amazonaws.com/ACCOUNT_ID/myQueue}\",", 132 | "\t\t\"MessageBody\": {", 133 | "\t\t\t\"Input\": \"${4:Hello from Step Functions!}\"", 134 | "\t\t}", 135 | "\t},", 136 | "\t\"Next\": \"${2:NextState}\"", 137 | "}" 138 | ], 139 | "description": "Code snippet for an SQS Publish Task state.\n\nCalls the Amazon SQS SendMessage API to send a message to a queue." 140 | }, 141 | { 142 | "name": "Choice State", 143 | "body": [ 144 | "\"${1:ChoiceState}\": {", 145 | "\t\"Type\": \"Choice\",", 146 | "\t\"Choices\": [", 147 | "\t\t{", 148 | "\t\t\t\"Variable\": \"$.${2:variable}\",", 149 | "\t\t\t\"BooleanEquals\": true,", 150 | "\t\t\t\"Next\": \"TrueState\"", 151 | "\t\t},", 152 | "\t\t{", 153 | "\t\t\t\"Variable\": \"$.${2:variable}\",", 154 | "\t\t\t\"BooleanEquals\": false,", 155 | "\t\t\t\"Next\": \"FalseState\"", 156 | "\t\t}", 157 | "\t],", 158 | "\t\"Default\": \"${3:DefaultState}\"", 159 | "}" 160 | ], 161 | "description": "Code snippet for a Choice state.\n\nA Choice state adds branching logic to a state machine." 162 | }, 163 | { 164 | "name": "Wait State", 165 | "body": [ 166 | "\"${1:WaitState}\": {", 167 | "\t\"Type\": \"Wait\",", 168 | "\t\"Seconds\": 10,", 169 | "\t\"Next\": \"${2:NextState}\"", 170 | "}" 171 | ], 172 | "description": "Code snippet for a Wait state.\n\nA Wait state delays the state machine from continuing for a specified time." 173 | }, 174 | { 175 | "name": "Succeed State", 176 | "body": ["\"${1:SuccessState}\": {", "\t\"Type\": \"Succeed\"", "}"], 177 | "description": "Code snippet for a Succeed state.\n\nA Succeed state stops an execution successfully." 178 | }, 179 | { 180 | "name": "Fail State", 181 | "body": [ 182 | "\"${1:FailState}\": {", 183 | "\t\"Type\": \"Fail\",", 184 | "\t\"Cause\": \"${2:Invalid response.}\",", 185 | "\t\"Error\": \"${3:ErrorA}\"", 186 | "}" 187 | ], 188 | "description": "Code snippet for a Fail state.\n\nA Fail state stops the execution of the state machine and marks it as a failure." 189 | }, 190 | { 191 | "name": "Parallel State", 192 | "body": [ 193 | "\"${1:ParallelState}\": {", 194 | "\t\"Type\": \"Parallel\",", 195 | "\t\"Branches\": [", 196 | "\t\t{", 197 | "\t\t\t\"StartAt\": \"State1\",", 198 | "\t\t\t\"States\": {", 199 | "\t\t\t\t\"State1\": {", 200 | "\t\t\t\t\t\"Type\": \"Pass\",", 201 | "\t\t\t\t\t\"End\": true", 202 | "\t\t\t\t}", 203 | "\t\t\t}", 204 | "\t\t},", 205 | "\t\t{", 206 | "\t\t\t\"StartAt\": \"State2\",", 207 | "\t\t\t\"States\": {", 208 | "\t\t\t\t\"State2\": {", 209 | "\t\t\t\t\t\"Type\": \"Pass\",", 210 | "\t\t\t\t\t\"End\": true", 211 | "\t\t\t\t}", 212 | "\t\t\t}", 213 | "\t\t}", 214 | "\t],", 215 | "\t\"Next\": \"${2:NextState}\"", 216 | "}" 217 | ], 218 | "description": "Code snippet for a Parallel state.\n\nA Parallel state can be used to create parallel branches of execution in your state machine." 219 | }, 220 | { 221 | "name": "Map State", 222 | "body": [ 223 | "\"${1:MapState}\": {", 224 | "\t\"Type\": \"Map\",", 225 | "\t\"ItemsPath\": \"$.array\",", 226 | "\t\"MaxConcurrency\": 0,", 227 | "\t\"Iterator\": {", 228 | "\t\t\"StartAt\": \"Pass\",", 229 | "\t\t\"States\": {", 230 | "\t\t\t\"Pass\": {", 231 | "\t\t\t\t\"Type\": \"Pass\",", 232 | "\t\t\t\t\"Result\": \"Done!\",", 233 | "\t\t\t\t\"End\": true", 234 | "\t\t\t}", 235 | "\t\t}", 236 | "\t},", 237 | "\t\"ResultPath\": \"$.output\",", 238 | "\t\"Next\": \"${2:NextState}\"", 239 | "}" 240 | ], 241 | "description": "Code snippet for a Map state.\n\nThe Map state can be used to run a set of steps for each element of an input array. While the Parallel state executes multiple branches of steps using the same input, a Map state will execute the same steps for multiple entries of an array in the state input." 242 | } 243 | ] 244 | -------------------------------------------------------------------------------- /src/tests/aslUtilityFunctions.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import assert from 'assert' 7 | import { ASTNode } from 'vscode-json-languageservice' 8 | import { getLanguageService, PropertyASTNode, TextDocument } from '../service' 9 | import { 10 | ASTTree, 11 | findClosestAncestorStateNode, 12 | findNodeAtLocation, 13 | getListOfStateNamesFromStateNode, 14 | insideStateNode, 15 | isChildOfStates, 16 | getStateInfo, 17 | findClosestAncestorNodeByName, 18 | } from '../utils/astUtilityFunctions' 19 | import { documentWithAssignAndCatch } from './json-strings/variableStrings' 20 | import { QueryLanguages } from '../asl-utils' 21 | 22 | const document = ` 23 | { 24 | "Comment": "A comment", 25 | "States": { 26 | "FirstState": {}, 27 | "ChoiceState": {}, 28 | "FirstMatchState": {}, 29 | "SecondMatchState": {}, 30 | "DefaultState": {}, 31 | "NextState": {}, 32 | "MapState1": { 33 | "Type": "Map", 34 | "Iterator": { 35 | "States": { 36 | "MapState2": { 37 | "Type": "Map", 38 | "Iterator": { 39 | "States": { 40 | "State1": { 41 | "Type": "Task", 42 | }, 43 | "State2": { 44 | "Type": "Task", 45 | }, 46 | "State3": { 47 | "Type": "Task", 48 | }, 49 | "State4": { 50 | "Type": "Task", 51 | }, 52 | } 53 | }, 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | ` 61 | 62 | const documentJSONata = ` 63 | { 64 | "Comment": "A comment", 65 | "QueryLanguage": "JSONata", 66 | "States": { 67 | "FirstState": {} 68 | } 69 | } 70 | ` 71 | 72 | const documentJSONPath = ` 73 | { 74 | "Comment": "A comment", 75 | "QueryLanguage": "JSONPath", 76 | "States": { 77 | "FirstState": { 78 | "QueryLanguage": "JSONata" 79 | }, 80 | "SecondState": { 81 | "QueryLanguage": "JSONPath" 82 | } 83 | } 84 | } 85 | ` 86 | 87 | // Invalid state name property 88 | const documentInvalid = ` 89 | { 90 | "States": { 91 | "FirstState": { 92 | }, 93 | "SecondState": { 94 | }, 95 | "Invalid: 96 | ` 97 | 98 | function toDocument(text: string): { textDoc: TextDocument; jsonDoc: ASTTree } { 99 | const textDoc = TextDocument.create('foo://bar/file.asl', 'json', 0, text) 100 | 101 | const ls = getLanguageService({}) 102 | const jsonDoc = ls.parseJSONDocument(textDoc) as ASTTree 103 | 104 | return { textDoc, jsonDoc } 105 | } 106 | 107 | describe('Utility functions for extracting data from AST Tree', () => { 108 | test('getListOfStateNamesFromStateNode - retrieves list of states from state node', async () => { 109 | const { jsonDoc } = toDocument(document) 110 | const stateNode = jsonDoc.root?.children?.[1] as PropertyASTNode 111 | const stateNames = getListOfStateNamesFromStateNode(stateNode) 112 | 113 | const expectedStateNames = [ 114 | 'FirstState', 115 | 'ChoiceState', 116 | 'FirstMatchState', 117 | 'SecondMatchState', 118 | 'DefaultState', 119 | 'NextState', 120 | 'MapState1', 121 | ] 122 | 123 | assert.strictEqual(stateNames?.length, expectedStateNames.length) 124 | assert.deepEqual(stateNames, expectedStateNames) 125 | }) 126 | 127 | test('getListOfStateNamesFromStateNode - throws an error when property named "States" is not provided', async () => { 128 | const { jsonDoc } = toDocument(document) 129 | const stateNode = jsonDoc.root?.children?.[0] as PropertyASTNode 130 | 131 | assert.throws(() => getListOfStateNamesFromStateNode(stateNode), { message: 'Not a state name property node' }) 132 | }) 133 | 134 | test('getListOfStateNamesFromStateNode - retrieves only valid states', () => { 135 | const { jsonDoc } = toDocument(documentInvalid) 136 | const stateNode = jsonDoc.root?.children?.[0] as PropertyASTNode 137 | const stateNames = getListOfStateNamesFromStateNode(stateNode) 138 | 139 | const expectedStateNames = ['FirstState', 'SecondState'] 140 | 141 | assert.strictEqual(stateNames?.length, expectedStateNames.length) 142 | assert.deepEqual(stateNames, expectedStateNames) 143 | }) 144 | 145 | test('findNodeAtLocation - finds a correct node at a given location', () => { 146 | const { jsonDoc } = toDocument(document) 147 | const location = document.indexOf('MapState2') + 1 148 | 149 | const node = findNodeAtLocation(jsonDoc.root, location) 150 | 151 | assert.ok(!!node) 152 | 153 | const nodeText = document.slice(node?.offset, node?.offset + node?.length) 154 | 155 | assert.strictEqual(nodeText, '"MapState2"') 156 | }) 157 | 158 | test('findClosestAncestorStateNode - finds the closest ancestor property node called "States"', () => { 159 | const { jsonDoc } = toDocument(document) 160 | const location = document.indexOf('State4') + 1 161 | 162 | const node = findNodeAtLocation(jsonDoc.root, location) 163 | assert(!!node) 164 | const statesNode = findClosestAncestorStateNode(node) 165 | 166 | assert(!!statesNode) 167 | const nodeText = statesNode?.keyNode.value 168 | 169 | assert.strictEqual(nodeText, 'States') 170 | 171 | const stateNames = getListOfStateNamesFromStateNode(statesNode) 172 | 173 | assert.deepEqual(stateNames, ['State1', 'State2', 'State3', 'State4']) 174 | }) 175 | 176 | test('isChildOfStates - should return True if the location is a child node of a "States" node', () => { 177 | const { jsonDoc } = toDocument(document) 178 | const location = document.indexOf('MapState1') - 2 179 | 180 | const node = findNodeAtLocation(jsonDoc.root, location) 181 | assert.strictEqual(isChildOfStates(node as ASTNode), true) 182 | }) 183 | 184 | test('isChildOfStates - should return False if the location is not a child node of a "States" node', () => { 185 | const { jsonDoc } = toDocument(document) 186 | const location = document.indexOf('SecondMatchState') 187 | 188 | const node = findNodeAtLocation(jsonDoc.root, location) 189 | assert.strictEqual(isChildOfStates(node as ASTNode), false) 190 | }) 191 | 192 | test('insideStateNode - should return True if the location is inside a State node', () => { 193 | const { jsonDoc } = toDocument(document) 194 | const location = document.indexOf('SecondMatchState') + 1 195 | 196 | const node = findNodeAtLocation(jsonDoc.root, location) 197 | assert.strictEqual(insideStateNode(node as ASTNode), true) 198 | }) 199 | 200 | test('insideStateNode - should return True if the location is a child node of a "States" node', () => { 201 | const { jsonDoc } = toDocument(document) 202 | const location = document.indexOf('MapState1') - 2 203 | 204 | const node = findNodeAtLocation(jsonDoc.root, location) 205 | assert.strictEqual(insideStateNode(node as ASTNode), false) 206 | }) 207 | 208 | test('getStateInfo - should return name and type inside a map state', () => { 209 | const { jsonDoc } = toDocument(document) 210 | const location = document.indexOf('MapState1') + 2 211 | 212 | const node = findNodeAtLocation(jsonDoc.root, location) 213 | assert(node) 214 | const result = getStateInfo(node) 215 | 216 | assert.equal(result?.stateName, 'MapState1') 217 | assert.equal(result?.stateType, 'Map') 218 | assert.equal(result?.queryLanguage, undefined) 219 | }) 220 | 221 | test('getStateInfo - should return name and type inside a task state', () => { 222 | const { jsonDoc } = toDocument(document) 223 | const location = document.indexOf('State3') + 2 224 | 225 | const node = findNodeAtLocation(jsonDoc.root, location) 226 | assert(node) 227 | const result = getStateInfo(node) 228 | 229 | assert.equal(result?.stateName, 'State3') 230 | assert.equal(result?.stateType, 'Task') 231 | assert.equal(result?.queryLanguage, undefined) 232 | }) 233 | 234 | test('getStateInfo - should return undefined queryLanguage if not set', () => { 235 | const { jsonDoc } = toDocument(documentJSONata) 236 | const location = documentJSONata.indexOf('FirstState') + 2 237 | 238 | const node = findNodeAtLocation(jsonDoc.root, location) 239 | assert(node) 240 | const result = getStateInfo(node) 241 | 242 | assert.equal(result?.stateName, 'FirstState') 243 | assert.equal(result?.queryLanguage, undefined) 244 | }) 245 | 246 | test('getStateInfo - should return JSONata query language of node', () => { 247 | const { jsonDoc } = toDocument(documentJSONPath) 248 | const location = documentJSONPath.indexOf('FirstState') + 2 249 | 250 | const node = findNodeAtLocation(jsonDoc.root, location) 251 | assert(node) 252 | const result = getStateInfo(node) 253 | 254 | assert.equal(result?.stateName, 'FirstState') 255 | assert.equal(result?.queryLanguage, QueryLanguages.JSONata) 256 | }) 257 | 258 | test('findClosestAncestorNodeByName - should return Assign ancestor node', () => { 259 | const { jsonDoc } = toDocument(documentWithAssignAndCatch) 260 | const location = documentWithAssignAndCatch.indexOf('var_lambda2') 261 | 262 | const node = findNodeAtLocation(jsonDoc.root, location) 263 | assert(node) 264 | const result = findClosestAncestorNodeByName(node, ['Assign', 'Parameters']) 265 | 266 | assert(result?.node) 267 | assert.equal(result?.nodeName, 'Assign') 268 | }) 269 | 270 | test('findClosestAncestorNodeByName - should return Catch ancestor node', () => { 271 | const { jsonDoc } = toDocument(documentWithAssignAndCatch) 272 | const location = documentWithAssignAndCatch.indexOf('error') 273 | 274 | const node = findNodeAtLocation(jsonDoc.root, location) 275 | assert(node) 276 | const result = findClosestAncestorNodeByName(node, ['Catch', 'Parameters']) 277 | 278 | assert(result?.node) 279 | assert.equal(result?.nodeName, 'Catch') 280 | }) 281 | 282 | test('findClosestAncestorNodeByName - should return innermost ancestor node', () => { 283 | const { jsonDoc } = toDocument(documentWithAssignAndCatch) 284 | const location = documentWithAssignAndCatch.indexOf('error') 285 | 286 | const node = findNodeAtLocation(jsonDoc.root, location) 287 | assert(node) 288 | const result = findClosestAncestorNodeByName(node, ['Catch', 'Assign']) 289 | 290 | assert(result?.node) 291 | assert.equal(result?.nodeName, 'Assign') 292 | }) 293 | }) 294 | -------------------------------------------------------------------------------- /src/tests/json-strings/completionStrings.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export const document1 = ` 7 | { 8 | "StartAt":, 9 | "States": { 10 | "FirstState": {}, 11 | "ChoiceState": {}, 12 | "FirstMatchState": {}, 13 | "SecondMatchState": {}, 14 | "DefaultState": {}, 15 | "NextState": { 16 | "Next":, 17 | }, 18 | "ChoiceStateX": { 19 | "Type": "Choice", 20 | "Choices": [ 21 | { 22 | "Next": "" 23 | }, 24 | { 25 | "Next": "FirstState" 26 | }, 27 | { 28 | "Next": "NextState" 29 | } 30 | ], 31 | "Default": "" 32 | }, 33 | } 34 | } 35 | ` 36 | 37 | export const document2 = ` 38 | { 39 | "StartAt": , 40 | "States": { 41 | "FirstState": {}, 42 | "ChoiceState": {}, 43 | "FirstMatchState": {}, 44 | "SecondMatchState": {}, 45 | "DefaultState": {}, 46 | "NextState": { 47 | "Next": , 48 | }, 49 | "ChoiceStateX": { 50 | "Type": "Choice", 51 | "Choices": [ 52 | { 53 | "Next": "" 54 | }, 55 | { 56 | "Next": "FirstState" 57 | }, 58 | { 59 | "Next": "NextState" 60 | } 61 | ], 62 | "Default": 63 | }, 64 | } 65 | } 66 | ` 67 | 68 | export const document3 = ` 69 | { 70 | "StartAt": ", 71 | "States": { 72 | "FirstState": {}, 73 | "ChoiceState": {}, 74 | "FirstMatchState": {}, 75 | "SecondMatchState": {}, 76 | "DefaultState": {}, 77 | "NextState": { 78 | "Next": ", 79 | }, 80 | "ChoiceStateX": {} 81 | } 82 | } 83 | ` 84 | 85 | export const document4 = ` 86 | { 87 | "StartAt": "", 88 | "States": { 89 | "FirstState": {}, 90 | "ChoiceState": {}, 91 | "FirstMatchState": {}, 92 | "SecondMatchState": {}, 93 | "DefaultState": {}, 94 | "NextState": { 95 | "Next": "", 96 | }, 97 | "ChoiceStateX": {} 98 | } 99 | } 100 | ` 101 | export const document5 = ` 102 | { 103 | "StartAt": "First", 104 | "States": { 105 | "FirstState": {}, 106 | "ChoiceState": {}, 107 | "FirstMatchState": {}, 108 | "SecondMatchState": {}, 109 | "DefaultState": {}, 110 | "NextState": { 111 | "Next": "First", 112 | }, 113 | "ChoiceStateX": {} 114 | } 115 | } 116 | ` 117 | 118 | export const document6 = ` 119 | { 120 | "StartAt": "First", 121 | "States": { 122 | "FirstState": { 123 | "Type": "Task", 124 | }, 125 | "ChoiceState": {}, 126 | "FirstMatchState": {}, 127 | "SecondMatchState": {}, 128 | "DefaultState": {}, 129 | "NextState": { 130 | "Next": "First", 131 | }, 132 | "Success State": {} 133 | } 134 | } 135 | ` 136 | 137 | export const documentNested = ` 138 | { 139 | "StartAt": "First", 140 | "States": { 141 | "FirstState": {}, 142 | "MapState": { 143 | "Iterator": { 144 | "StartAt":, 145 | "States": { 146 | "Nested1": {}, 147 | "Nested2": {}, 148 | "Nested3": {}, 149 | "Nested4": { 150 | "Next":, 151 | }, 152 | } 153 | }, 154 | }, 155 | } 156 | } 157 | ` 158 | export const completionsEdgeCase1 = `{ 159 | "Comment": "An example of the Amazon States Language using a map state to process elements of an array with a max concurrency of 2.", 160 | "StartAt": "Map", 161 | "States": { 162 | "Map": { 163 | "Type": "Map", 164 | "ItemsPath": "$.array", 165 | "ResultPath": """"$$$$$.array$$$", 166 | "MaxConcurrency": 2, 167 | "Next": "Final State", 168 | "Iterator": { 169 | "StartAt": "Pass", 170 | "States": { 171 | "Pass": { 172 | "Type": "Pass", 173 | "Result": "Done!", 174 | "End": true 175 | } 176 | } 177 | } 178 | }, 179 | " 180 | "Net 2": { 181 | "Type": "Fail", 182 | "Next": "Final State" 183 | }, 184 | "Final State": { 185 | "Type": "Pass", 186 | "Next": "Net 2" 187 | } 188 | } 189 | }` 190 | 191 | export const completionsEdgeCase2 = `{ 192 | "StartAt": "MyState", 193 | "States": { 194 | " 195 | } 196 | }` 197 | 198 | export const itemLabels = [ 199 | 'FirstState', 200 | 'ChoiceState', 201 | 'FirstMatchState', 202 | 'SecondMatchState', 203 | 'DefaultState', 204 | 'NextState', 205 | 'ChoiceStateX', 206 | ] 207 | export const nestedItemLabels = ['Nested1', 'Nested2', 'Nested3', 'Nested4'] 208 | -------------------------------------------------------------------------------- /src/tests/json-strings/jsonataStrings.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export const globalJsonataDocument = ` 7 | { 8 | "Comment": "{% $ %}", 9 | "StartAt": "Lambda1", 10 | "QueryLanguage": "JSONata", 11 | "States": { 12 | "Lambda1": { 13 | "Type": "Task", 14 | "Resource": "arn:aws:states:::lambda:invoke", 15 | "Next": "Lambda2", 16 | "Assign": { 17 | "var_lambda1": 123 18 | } 19 | }, 20 | "Lambda2": { 21 | "Type": "Task", 22 | "Resource": "arn:aws:states:::lambda:invoke", 23 | "Arguments": { 24 | "Payload": "{% $ %}", 25 | "FunctionName": "{% myFunction" 26 | }, 27 | "Assign": { 28 | "var_lambda2": "{% $states. %}", 29 | "var_lambda3": "{% $states.context. %}", 30 | "var_lambda4": "{% $states.cont %}", 31 | "var_lambda5": "{% $states.context.Ex %}", 32 | "var_lambda6": "{% ,.$/$ %}" 33 | } 34 | } 35 | } 36 | } 37 | ` 38 | 39 | export const stateLevelJsonataDocument = ` 40 | { 41 | "Comment": "A description of my state machine", 42 | "StartAt": "Lambda1", 43 | "QueryLanguage": "JSONPath", 44 | "States": { 45 | "Lambda1": { 46 | "Type": "Task", 47 | "Resource": "arn:aws:states:::lambda:invoke", 48 | "Next": "Lambda2", 49 | "Parameters": { 50 | "Payload": "{% $ %}" 51 | }, 52 | "Assign": { 53 | "var_lambda1": 123 54 | } 55 | }, 56 | "Lambda2": { 57 | "Type": "Task", 58 | "QueryLanguage": "JSONata", 59 | "Resource": "arn:aws:states:::lambda:invoke", 60 | "Arguments": { 61 | "Payload": "{% $ %}" 62 | }, 63 | "End": true 64 | } 65 | } 66 | } 67 | ` 68 | -------------------------------------------------------------------------------- /src/tests/json-strings/variableStrings.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export const documentWithAssignAndCatch = ` 7 | { 8 | "Comment": "A description of my state machine", 9 | "StartAt": "Lambda1", 10 | "States": { 11 | "Lambda1": { 12 | "Type": "Task", 13 | "Resource": "arn:aws:states:::lambda:invoke", 14 | "OutputPath": "$.Payload", 15 | "Next": "Lambda2", 16 | "Assign": { 17 | "var_lambda1": 123 18 | } 19 | }, 20 | "Lambda2": { 21 | "Type": "Task", 22 | "Resource": "arn:aws:states:::lambda:invoke", 23 | "OutputPath": "$.Payload", 24 | "Parameters": { 25 | "Payload.$": "$states." 26 | }, 27 | "End": true, 28 | "Assign": { 29 | "var_lambda2": "" 30 | }, 31 | "Catch": [ 32 | { 33 | "ErrorEquals": [], 34 | "Assign": { 35 | "error": "$states." 36 | } 37 | } 38 | ] 39 | } 40 | } 41 | } 42 | ` 43 | 44 | export const documentWithAssignAndCatchInvalidJson = ` 45 | { 46 | "Comment": "A description of my state machine", 47 | "StartAt": "Lambda1", 48 | "States": { 49 | "Lambda1": { 50 | "Type": "Task", 51 | "Resource": "arn:aws:states:::lambda:invoke", 52 | "OutputPath": "$.Payload", 53 | "Next": "Lambda2", 54 | "Assign": { 55 | "var_lambda1": 123 56 | } 57 | }, 58 | "Lambda2": { 59 | "Type": "Task", 60 | "Resource": "arn:aws:states:::lambda:invoke", 61 | "OutputPath": "$.Payload", 62 | "Parameters": { 63 | "Payload.$": "$states." 64 | }, 65 | "End": true, 66 | "Assign": { 67 | "var_lambda2": "", 68 | "var_colon": 69 | }, 70 | "Catch": [ 71 | { 72 | "ErrorEquals": [], 73 | "Assign": { 74 | "error": "$states." 75 | } 76 | } 77 | ] 78 | } 79 | } 80 | } 81 | ` 82 | 83 | export const documentInvalidFailWithAssign = `{ 84 | "Comment": "An example of the Amazon States Language using a Fail state with Assign field", 85 | "StartAt": "HelloWorld", 86 | "States": { 87 | "HelloWorld": { 88 | "Type": "Fail", 89 | "Assign": {} 90 | } 91 | } 92 | }` 93 | -------------------------------------------------------------------------------- /src/tests/jsonSchemaAsl.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import * as assert from 'assert' 7 | import { getLanguageService, JSONDocument, TextDocument } from '../service' 8 | 9 | function toDocument(text: string): { textDoc: TextDocument; jsonDoc: JSONDocument } { 10 | const textDoc = TextDocument.create('foo://bar/file.asl', 'json', 0, text) 11 | 12 | const ls = getLanguageService({}) 13 | const jsonDoc = ls.parseJSONDocument(textDoc) as JSONDocument 14 | 15 | return { textDoc, jsonDoc } 16 | } 17 | 18 | describe('JSON Schema Validation for ASL', () => { 19 | test('JSON Schema Validation works', async () => { 20 | const { textDoc, jsonDoc } = toDocument('{}') 21 | 22 | const ls = getLanguageService({}) 23 | const res = await ls.doValidation(textDoc, jsonDoc) 24 | assert.strictEqual(res.length, 2) 25 | assert.ok(res.some((item) => item.message === 'Missing property "States".')) 26 | assert.ok(res.some((item) => item.message === 'Missing property "StartAt".')) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/tests/service.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import * as service from '../service' 7 | 8 | describe('Service', () => { 9 | test('Should expoert JSON and YAML services in GitHub environment', async () => { 10 | const namedExports = Object.keys(service) 11 | expect(namedExports).toContain('getLanguageService') 12 | expect(namedExports).toContain('doCompleteAsl') 13 | 14 | if (process.env.YAML_SERVICE !== 'disabled') { 15 | expect(namedExports).toContain('getYamlLanguageService') 16 | } 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/tests/utils/testUtilities.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import * as assert from 'assert' 7 | import { InsertReplaceEdit, TextEdit } from 'vscode-languageserver-types' 8 | import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver' 9 | import { FILE_EXTENSIONS, LANGUAGE_IDS } from '../../constants/constants' 10 | import { 11 | getLanguageService, 12 | JSONDocument, 13 | TextDocument, 14 | ASLLanguageServiceParams, 15 | Position, 16 | Range, 17 | } from '../../service' 18 | 19 | export function toDocument(text: string, isYaml?: boolean): { textDoc: TextDocument; jsonDoc: JSONDocument } { 20 | const textDoc = TextDocument.create( 21 | `foo://bar/file.${isYaml ? FILE_EXTENSIONS.YAML : FILE_EXTENSIONS.JSON}`, 22 | isYaml ? LANGUAGE_IDS.YAML : LANGUAGE_IDS.JSON, 23 | 0, 24 | text, 25 | ) 26 | 27 | const ls = getLanguageService({}) 28 | const jsonDoc = ls.parseJSONDocument(textDoc) as JSONDocument 29 | 30 | return { textDoc, jsonDoc } 31 | } 32 | 33 | export function asTextEdit(item: TextEdit | InsertReplaceEdit | undefined): TextEdit | undefined { 34 | if (TextEdit.is(item)) { 35 | return item 36 | } 37 | 38 | return undefined 39 | } 40 | 41 | export interface TestValidationOptions { 42 | json: string 43 | diagnostics: { 44 | message: string 45 | start: [number, number] 46 | end: [number, number] 47 | code?: string | number | undefined 48 | }[] 49 | filterMessages?: string[] 50 | } 51 | 52 | export async function getValidations(json: string, params: ASLLanguageServiceParams = {}) { 53 | const { textDoc, jsonDoc } = toDocument(json) 54 | const ls = getLanguageService(params) 55 | 56 | return await ls.doValidation(textDoc, jsonDoc) 57 | } 58 | 59 | export async function testValidations(options: TestValidationOptions, params: ASLLanguageServiceParams = {}) { 60 | const { json, diagnostics, filterMessages } = options 61 | 62 | let res = await getValidations(json, params) 63 | 64 | res = res.filter((diagnostic) => { 65 | if (filterMessages && filterMessages.find((message) => message === diagnostic.message)) { 66 | return false 67 | } 68 | 69 | return true 70 | }) 71 | 72 | assert.strictEqual(res.length, diagnostics.length) 73 | 74 | res.forEach((item, index) => { 75 | const leftPos = Position.create(...diagnostics[index].start) 76 | const rightPos = Position.create(...diagnostics[index].end) 77 | 78 | const diagnostic = Diagnostic.create( 79 | Range.create(leftPos, rightPos), 80 | diagnostics[index].message, 81 | DiagnosticSeverity.Error, 82 | diagnostics[index].code, 83 | ) 84 | 85 | assert.deepStrictEqual(item, diagnostic) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /src/tests/yaml-strings/completionStrings.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export const emptyDocument = '' 7 | 8 | export const documentWithPartialTopLevel = ` 9 | St 10 | ` 11 | 12 | export const documentWithStates = ` 13 | 14 | StartAt: 15 | 16 | States: 17 | \u0020\u0020 18 | ` 19 | 20 | export const document1 = ` 21 | StartAt: 22 | States: 23 | FirstState: 24 | ChoiceState: 25 | FirstMatchState: 26 | SecondMatchState: 27 | DefaultState: 28 | NextState: 29 | Next: "" 30 | ChoiceStateX: 31 | Type: Choice 32 | Choices: 33 | - Next: "" 34 | - Next: FirstState 35 | - Next: NextState 36 | Default: "" 37 | ` 38 | export const document2 = ` 39 | StartAt: 40 | States: 41 | FirstState: 42 | ChoiceState: 43 | FirstMatchState: 44 | SecondMatchState: 45 | DefaultState: 46 | NextState: 47 | Next: "" 48 | ChoiceStateX: 49 | Type: Choice, 50 | Choices: 51 | - Next: "" 52 | - Next: FirstState 53 | - Next: NextState 54 | Default: "" 55 | ` 56 | 57 | export const document3 = ` 58 | StartAt: "" 59 | States: 60 | FirstState: 61 | ChoiceState: 62 | FirstMatchState: 63 | SecondMatchState: 64 | DefaultState: 65 | NextState: 66 | Next: "" 67 | ChoiceStateX: 68 | ` 69 | export const document4 = ` 70 | StartAt: First 71 | States: 72 | FirstState: 73 | ChoiceState: 74 | FirstMatchState: 75 | SecondMatchState: 76 | DefaultState: 77 | NextState: 78 | Next: First 79 | ChoiceStateX: 80 | ` 81 | 82 | export const documentNested = ` 83 | StartAt: First 84 | States: 85 | FirstState: 86 | MapState: 87 | Iterator: 88 | StartAt: "" 89 | States: 90 | Nested1: 91 | Nested2: 92 | Nested3: 93 | Nested4: 94 | Next: "" 95 | ` 96 | export const completionsEdgeCase1 = ` 97 | Comment: An example of the Amazon States Language using a map state to process elements of an array with a max concurrency of 2. 98 | StartAt: Map 99 | States: 100 | Map: 101 | Type: Map 102 | ItemsPath: $.array 103 | ResultPath: \\\$$$$$.array$$$ 104 | MaxConcurrency: 2 105 | Next: Final State 106 | Iterator: 107 | StartAt: Pass 108 | States: 109 | Pass: 110 | Type: Pass 111 | Result: Done! 112 | End: true 113 | "Net 2": 114 | Type: Fail 115 | Next: Final State 116 | Final State: 117 | Type: Pass 118 | Next: "Net 2" 119 | ` 120 | 121 | export const completionsEdgeCase2 = ` 122 | StartAt: MyState 123 | States: 124 | 125 | ` 126 | 127 | export const snippetsCompletionCase1 = ` 128 | StartAt: Hello 129 | States: 130 | \u0020\u0020 131 | Hello: 132 | Type: Pass 133 | Result: Hello 134 | Next: World 135 | 136 | World: 137 | Type: Pass 138 | Result: World 139 | End: true 140 | ` 141 | 142 | export const snippetsCompletionCase2 = ` 143 | StartAt: Hello 144 | States: 145 | Hello: 146 | Type: Pass 147 | Result: Hello 148 | Next: World 149 | World: 150 | Type: Pass 151 | Result: World 152 | End: true 153 | ` 154 | 155 | export const snippetsCompletionCase3 = ` 156 | StartAt: Hello 157 | States: 158 | Hello: 159 | Type: Pass 160 | Result: Hello 161 | Next: World 162 | \u0020\u0020 163 | 164 | World: 165 | Type: Pass 166 | Result: World 167 | End: true 168 | ` 169 | 170 | export const snippetsCompletionCase4 = ` 171 | StartAt: Hello 172 | States: 173 | Hello: 174 | Type: Pass 175 | Result: Hello 176 | Next: World 177 | 178 | World: 179 | Type: Pass 180 | Result: World 181 | End: true 182 | \u0020\u0020\u0020\u0020 183 | ` 184 | 185 | export const snippetsCompletionCase5 = ` 186 | StartAt: Hello 187 | States: 188 | Hello: 189 | Type: Pass 190 | Result: Hello 191 | Next: World 192 | 193 | World: 194 | Type: Pass 195 | Result: World 196 | End: true 197 | 198 | 199 | \u0020\u0020 200 | ` 201 | 202 | export const snippetsCompletionWithinMap = ` 203 | StartAt: Map 204 | States: 205 | Map: 206 | Type: Map 207 | Next: Final State 208 | Iterator: 209 | StartAt: Pass 210 | States: 211 | Pass: 212 | Type: Pass 213 | Result: Done! 214 | End: true 215 | \u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020 216 | Final State: 217 | Type: Pass 218 | End: true 219 | ` 220 | 221 | export const snippetsCompletionWithinParallel = ` 222 | StartAt: Parallel 223 | States: 224 | Parallel: 225 | Type: Parallel 226 | Next: Final State 227 | Branches: 228 | - StartAt: Wait 20s 229 | States: 230 | Wait 20s: 231 | Type: Wait 232 | Seconds: 20 233 | End: true 234 | \u0020\u0020\u0020\u0020\u0020\u0020\u0020\u0020 235 | Final State: 236 | Type: Pass 237 | End: true 238 | ` 239 | 240 | export const catchRetrySnippetsCompletionWithinMap = ` 241 | StartAt: Map 242 | States: 243 | Map: 244 | Type: Map 245 | Next: Final State 246 | \u0020\u0020\u0020\u0020 247 | Iterator: 248 | StartAt: Pass 249 | States: 250 | Pass: 251 | Type: Pass 252 | Result: Done! 253 | End: true 254 | Final State: 255 | Type: Pass 256 | End: true 257 | ` 258 | 259 | export const catchRetrySnippetsCompletionWithinParallel = ` 260 | StartAt: Parallel 261 | States: 262 | Parallel: 263 | \u0020\u0020\u0020\u0020 264 | Type: Parallel 265 | Next: Final State 266 | Branches: 267 | - StartAt: Wait 20s 268 | States: 269 | Wait 20s: 270 | Type: Wait 271 | Seconds: 20 272 | End: true 273 | Final State: 274 | Type: Pass 275 | End: true 276 | \u0020\u0020\u0020\u0020 277 | \u0020\u0020 278 | ` 279 | 280 | export const catchRetrySnippetsCompletionWithinTask = ` 281 | StartAt: FirstState 282 | States: 283 | FirstState: 284 | Type: Task 285 | 286 | \u0020\u0020\u0020\u0020 287 | 288 | Resource: 'arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME' 289 | End: true 290 | ` 291 | 292 | export const topLevelLabels = ['Version', 'Comment', 'TimeoutSeconds', 'StartAt', 'States', 'QueryLanguage'] 293 | export const stateSnippetLabels = [ 294 | 'Pass State', 295 | 'Lambda Task State', 296 | 'EventBridge Task State', 297 | 'MediaConvert Task State', 298 | 'SNS Task State', 299 | 'Batch Task State', 300 | 'ECS Task State', 301 | 'SQS Task State', 302 | 'Choice State', 303 | 'Wait State', 304 | 'Succeed State', 305 | 'Fail State', 306 | 'Parallel State', 307 | 'Map State', 308 | ] 309 | 310 | export const stateNameLabels = [ 311 | 'FirstState', 312 | 'ChoiceState', 313 | 'FirstMatchState', 314 | 'SecondMatchState', 315 | 'DefaultState', 316 | 'NextState', 317 | 'ChoiceStateX', 318 | ] 319 | export const nestedItemLabels = ['Nested1', 'Nested2', 'Nested3', 'Nested4'] 320 | 321 | export const passSnippetYaml = 322 | '${1:PassState}:\n\tType: Pass\n\tResult:\n\t\tdata1: 0.5\n\t\tdata2: 1.5\n\tResultPath: $.result\n\tNext: ${2:NextState}\n' 323 | -------------------------------------------------------------------------------- /src/tests/yamlFormat.test.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import * as assert from 'assert' 7 | import { FormattingOptions, TextEdit } from 'vscode-languageserver-types' 8 | import { Position, Range } from '../service' 9 | import { toDocument } from './utils/testUtilities' 10 | import { getYamlLanguageService } from '../yamlLanguageService' 11 | 12 | async function getFormat(yaml: string, range: Range, formattingOptions: FormattingOptions): Promise { 13 | const { textDoc } = toDocument(yaml, true) 14 | const ls = getYamlLanguageService({}) 15 | 16 | return ls.format(textDoc, range, formattingOptions) 17 | } 18 | 19 | describe('ASL YAML format', () => { 20 | test('Format does not remove comments', async () => { 21 | const yaml = ` 22 | # this is my comment 23 | hello: world # this is another comment 24 | ` 25 | const formattedTextEdits = await getFormat( 26 | yaml, 27 | Range.create(Position.create(0, 0), Position.create(yaml.length, yaml.length)), 28 | { 29 | tabSize: 4, 30 | insertSpaces: true, 31 | }, 32 | ) 33 | 34 | assert.equal( 35 | formattedTextEdits[0].newText, 36 | '# this is my comment\nhello: world # this is another comment\n', 37 | 'Expected comments to not be removed', 38 | ) 39 | }) 40 | 41 | test('Format removes unnecessary spaces', async () => { 42 | const yaml = ` 43 | hello: world 44 | ` 45 | const formattedTextEdits = await getFormat( 46 | yaml, 47 | Range.create(Position.create(0, 0), Position.create(yaml.length, yaml.length)), 48 | { 49 | tabSize: 4, 50 | insertSpaces: true, 51 | }, 52 | ) 53 | 54 | assert.equal(formattedTextEdits[0].newText, 'hello: world\n', 'Expected unnecessary spaces to be removed') 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/utils/astUtilityFunctions.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { QueryLanguages } from '../asl-utils' 7 | import { 8 | ArrayASTNode, 9 | ASTNode, 10 | JSONDocument, 11 | ObjectASTNode, 12 | PropertyASTNode, 13 | StringASTNode, 14 | } from 'vscode-json-languageservice' 15 | import { Position } from 'vscode-languageserver-types' 16 | 17 | export interface ASTTree extends JSONDocument { 18 | root: ASTNode 19 | } 20 | 21 | export interface ASLOptions { 22 | ignoreColonOffset?: boolean 23 | shouldShowStateSnippets?: boolean 24 | shouldShowErrorSnippets?: { 25 | retry: boolean 26 | catch: boolean 27 | } 28 | } 29 | 30 | export interface CompleteStateNameOptions { 31 | shouldAddLeftQuote?: boolean 32 | shouldAddRightQuote?: boolean 33 | shouldAddLeadingSpace?: boolean 34 | shoudlAddTrailingComma?: boolean 35 | } 36 | 37 | export interface ProcessYamlDocForCompletionOutput { 38 | modifiedDocText: string 39 | tempPositionForCompletions: Position 40 | startPositionForInsertion: Position 41 | endPositionForInsertion: Position 42 | shouldPrependSpace: boolean 43 | } 44 | 45 | export function isStringNode(node: ASTNode): node is StringASTNode { 46 | return node.type === 'string' 47 | } 48 | 49 | export function isPropertyNode(node: ASTNode): node is PropertyASTNode { 50 | return node.type === 'property' 51 | } 52 | 53 | export function isObjectNode(node: ASTNode): node is ObjectASTNode { 54 | return node.type === 'object' 55 | } 56 | 57 | export function isArrayNode(node: ASTNode): node is ArrayASTNode { 58 | return node.type === 'array' 59 | } 60 | 61 | export function findPropChildByName(rootNode: ObjectASTNode, name: string): PropertyASTNode | undefined { 62 | return rootNode.properties.find((propNode) => propNode.keyNode.value === name) 63 | } 64 | 65 | function isLocationInNodeRange(node: ASTNode, loc: number) { 66 | return loc >= node.offset && loc <= node.offset + node.length 67 | } 68 | 69 | export function isChildOfStates(node: ASTNode): boolean { 70 | return !!node.parent && isPropertyNode(node.parent) && node.parent.keyNode.value === 'States' 71 | } 72 | 73 | export function insideStateNode(node: ASTNode): boolean { 74 | const greatGrandParentNode = node.parent?.parent?.parent 75 | 76 | return ( 77 | !!greatGrandParentNode && isPropertyNode(greatGrandParentNode) && greatGrandParentNode.keyNode?.value === 'States' 78 | ) 79 | } 80 | 81 | /** Finds the relevant node at a given location in respective json string */ 82 | export function findNodeAtLocation(rootNode: ASTNode, loc: number): ASTNode | undefined { 83 | if (isLocationInNodeRange(rootNode, loc)) { 84 | const { children } = rootNode 85 | 86 | if (children?.length) { 87 | const nodeInRange = children.find((node) => isLocationInNodeRange(node, loc)) 88 | 89 | if (nodeInRange) { 90 | return findNodeAtLocation(nodeInRange, loc) 91 | } 92 | } 93 | 94 | return rootNode 95 | } 96 | } 97 | 98 | /** Finds the closest ancestor property node named "States" */ 99 | export function findClosestAncestorStateNode(node: ASTNode): PropertyASTNode | undefined { 100 | if (isPropertyNode(node) && (node as PropertyASTNode).keyNode.value === 'States') { 101 | return node 102 | } else if (!node.parent) { 103 | return undefined 104 | } 105 | return findClosestAncestorStateNode(node.parent) 106 | } 107 | 108 | /** Extracts the list of state names from given property node named "States" */ 109 | export function getListOfStateNamesFromStateNode(node: PropertyASTNode, ignoreColonOffset = false): string[] { 110 | const nodeName = node.keyNode.value 111 | 112 | if (nodeName === 'States') { 113 | // The first object node will contain property nodes containing state names 114 | const objNode = node.children.find(isObjectNode) 115 | 116 | return ( 117 | objNode?.children 118 | // Filter out property nodes that do not have colonOffset. They are invalid. 119 | .filter( 120 | (childNode) => 121 | isPropertyNode(childNode) && 122 | childNode.colonOffset && 123 | (ignoreColonOffset || (!ignoreColonOffset && childNode.colonOffset >= 0)), 124 | ) 125 | .map((propNode) => (propNode as PropertyASTNode).keyNode.value) ?? [] 126 | ) 127 | } else { 128 | throw new Error('Not a state name property node') 129 | } 130 | } 131 | 132 | interface AncestorASTNodeInfo { 133 | node: PropertyASTNode 134 | nodeName: string 135 | } 136 | /** Finds the closest ancestor property given node names */ 137 | export function findClosestAncestorNodeByName(node: ASTNode, NodeName: string[]): AncestorASTNodeInfo | undefined { 138 | if (isPropertyNode(node) && NodeName.includes(node.keyNode.value)) { 139 | return { node, nodeName: node.keyNode.value } 140 | } 141 | if (!node.parent) { 142 | return undefined 143 | } 144 | 145 | return findClosestAncestorNodeByName(node.parent, NodeName) 146 | } 147 | 148 | /** 149 | * Get the state name, type, and query language of current node 150 | */ 151 | export function getStateInfo( 152 | node: ASTNode, 153 | ): { stateName: string; stateType: string | undefined; queryLanguage: QueryLanguages | undefined } | undefined { 154 | const parent = node.parent 155 | if (isPropertyNode(node) && parent && isChildOfStates(node.parent)) { 156 | let stateType: string | undefined 157 | let queryLanguage: QueryLanguages | undefined = undefined 158 | if (node.valueNode && isObjectNode(node.valueNode)) { 159 | const typeProperty = node.valueNode.properties.find((property) => property.keyNode.value === 'Type') 160 | const queryLanguageValue = node.valueNode.properties 161 | .find((property) => property.keyNode.value === 'QueryLanguage') 162 | ?.valueNode?.value?.toString() 163 | stateType = typeProperty?.valueNode?.value?.toString() ?? undefined 164 | if (queryLanguageValue === QueryLanguages.JSONata) { 165 | queryLanguage = QueryLanguages.JSONata 166 | } else if (queryLanguageValue === QueryLanguages.JSONPath) { 167 | queryLanguage = QueryLanguages.JSONPath 168 | } 169 | } 170 | 171 | return { 172 | stateName: node.keyNode.value, 173 | stateType, 174 | queryLanguage, 175 | } 176 | } else if (!parent) { 177 | return undefined 178 | } 179 | 180 | return getStateInfo(node.parent) 181 | } 182 | -------------------------------------------------------------------------------- /src/validation/utils/getDiagnosticsForNode.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { 7 | ArrayASTNode, 8 | ASTNode, 9 | Diagnostic, 10 | ObjectASTNode, 11 | PropertyASTNode, 12 | TextDocument, 13 | } from 'vscode-json-languageservice' 14 | 15 | import { isArrayNode, isObjectNode } from '../../utils/astUtilityFunctions' 16 | 17 | import getPropertyNodeDiagnostic from './getPropertyNodeDiagnostic' 18 | 19 | import { MESSAGES } from '../../constants/diagnosticStrings' 20 | import schema from '../validationSchema' 21 | 22 | const referenceTypes = schema.ReferenceTypes 23 | 24 | interface SchemaObject { 25 | [property: string]: SchemaObject | boolean | string 26 | } 27 | 28 | function isObject(obj: unknown): obj is object { 29 | return obj === Object(obj) 30 | } 31 | 32 | function getDiagnosticsForArrayOfSchema(rootNode: ArrayASTNode, document: TextDocument, arraySchema: string) { 33 | const newSchemaPart = referenceTypes[arraySchema] as SchemaObject 34 | let diagnostics: Diagnostic[] = [] 35 | 36 | if (isObject(newSchemaPart)) { 37 | rootNode.items.forEach((itemNode) => { 38 | if (isObjectNode(itemNode)) { 39 | diagnostics = diagnostics.concat(getDiagnosticsForNode(itemNode, document, newSchemaPart)) 40 | } 41 | }) 42 | } 43 | 44 | return diagnostics 45 | } 46 | 47 | function getDiagnosticsForOneOfSchema( 48 | rootNode: ObjectASTNode, 49 | document: TextDocument, 50 | schemaPart: SchemaObject, 51 | oneOfSchema: string, 52 | ) { 53 | const mutuallyExclusiveProperties: unknown = referenceTypes[oneOfSchema] 54 | const mutuallyExclusivePropertiesPresent: { propNode: PropertyASTNode; schemaValue: unknown }[] = [] 55 | let diagnostics: Diagnostic[] = [] 56 | 57 | rootNode.properties.forEach((prop) => { 58 | if (!isObject(mutuallyExclusiveProperties)) { 59 | return 60 | } 61 | 62 | const propName = prop.keyNode.value 63 | const propertySchema = mutuallyExclusiveProperties[propName] 64 | 65 | // If the property is one of mutually exclusive properties 66 | if (propertySchema) { 67 | mutuallyExclusivePropertiesPresent.push({ propNode: prop, schemaValue: propertySchema }) 68 | // If the property is neither in the set nor in the schema props 69 | } else if (!schemaPart[propName]) { 70 | diagnostics.push(getPropertyNodeDiagnostic(prop, document, MESSAGES.INVALID_PROPERTY_NAME)) 71 | } 72 | }) 73 | 74 | // if there is more than one item mark them all as invalid 75 | if (mutuallyExclusivePropertiesPresent.length > 1) { 76 | mutuallyExclusivePropertiesPresent.forEach((oneOfProp) => { 77 | diagnostics.push( 78 | getPropertyNodeDiagnostic(oneOfProp.propNode, document, MESSAGES.MUTUALLY_EXCLUSIVE_CHOICE_PROPERTIES), 79 | ) 80 | }) 81 | // if there is only one item and it is an object 82 | // recursively continue on with validation 83 | } else if (mutuallyExclusivePropertiesPresent.length) { 84 | const { schemaValue, propNode } = mutuallyExclusivePropertiesPresent[0] 85 | const { valueNode } = propNode 86 | if (valueNode && isObject(schemaValue)) { 87 | diagnostics = diagnostics.concat(getDiagnosticsForNode(valueNode, document, schemaValue as SchemaObject)) 88 | } 89 | } 90 | 91 | return diagnostics 92 | } 93 | 94 | function getDiagnosticsForRegularProperties(rootNode: ObjectASTNode, document: TextDocument, schemaPart: SchemaObject) { 95 | const diagnostics: Diagnostic[] = [] 96 | 97 | if (schemaPart.properties) { 98 | schemaPart = schemaPart.properties as SchemaObject 99 | } 100 | 101 | rootNode.properties.forEach((prop) => { 102 | const propName = prop.keyNode.value 103 | const propertySchema: unknown = schemaPart[propName] 104 | 105 | if (!propertySchema) { 106 | diagnostics.push(getPropertyNodeDiagnostic(prop, document, MESSAGES.INVALID_PROPERTY_NAME)) 107 | } else if (prop.valueNode && isObject(propertySchema)) { 108 | // evaluate nested schema 109 | diagnostics.push(...getDiagnosticsForNode(prop.valueNode, document, propertySchema as SchemaObject)) 110 | } 111 | }) 112 | 113 | return diagnostics 114 | } 115 | 116 | export default function getDiagnosticsForNode( 117 | rootNode: ASTNode, 118 | document: TextDocument, 119 | schemaPart: SchemaObject, 120 | ): Diagnostic[] { 121 | const arrayOfType = schemaPart['Fn:ArrayOf'] 122 | const oneOfType = schemaPart['Fn:OneOf'] 123 | const valueOfType = schemaPart['Fn:ValueOf'] 124 | 125 | // Fn:ArrayOf 126 | // if it contains Fn:ArrayOf property all the other values will be ignored 127 | if (typeof arrayOfType === 'string' && isArrayNode(rootNode)) { 128 | return getDiagnosticsForArrayOfSchema(rootNode, document, arrayOfType) 129 | // Fn:OneOf 130 | } else if (typeof oneOfType === 'string' && isObjectNode(rootNode)) { 131 | return getDiagnosticsForOneOfSchema(rootNode, document, schemaPart, oneOfType) 132 | // Fn:ValueOf 133 | } else if (typeof valueOfType === 'string' && isObjectNode(rootNode)) { 134 | const newSchemaPart = referenceTypes[valueOfType] as SchemaObject 135 | 136 | return getDiagnosticsForNode(rootNode, document, newSchemaPart) 137 | // Regular properties 138 | } else if (isObjectNode(rootNode)) { 139 | return getDiagnosticsForRegularProperties(rootNode, document, schemaPart) 140 | } 141 | 142 | return [] 143 | } 144 | -------------------------------------------------------------------------------- /src/validation/utils/getPropertyNodeDiagnostic.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Diagnostic, DiagnosticSeverity, PropertyASTNode, Range, TextDocument } from 'vscode-json-languageservice' 7 | 8 | export default function getPropertyNodeDiagnostic( 9 | propNode: PropertyASTNode, 10 | document: TextDocument, 11 | message: string, 12 | ): Diagnostic { 13 | const { length, offset } = propNode.keyNode 14 | const range = Range.create(document.positionAt(offset), document.positionAt(offset + length)) 15 | 16 | return Diagnostic.create(range, message, DiagnosticSeverity.Error) 17 | } 18 | -------------------------------------------------------------------------------- /src/validation/validateProperties.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Diagnostic, ObjectASTNode, TextDocument } from 'vscode-json-languageservice' 7 | 8 | import { findPropChildByName } from '../utils/astUtilityFunctions' 9 | 10 | import getDiagnosticsForNode from './utils/getDiagnosticsForNode' 11 | 12 | import schema from './validationSchema' 13 | 14 | export default function (oneStateValueNode: ObjectASTNode, document: TextDocument): Diagnostic[] { 15 | // Get the type of state 16 | const stateType = findPropChildByName(oneStateValueNode, 'Type')?.valueNode?.value 17 | const diagnostics: Diagnostic[] = [] 18 | 19 | if (typeof stateType === 'string') { 20 | const hasCommonProperties = !!schema.StateTypes[stateType]?.hasCommonProperties 21 | const stateProperties = schema.StateTypes[stateType]?.Properties 22 | 23 | if (!stateProperties) { 24 | return [] 25 | } 26 | 27 | const allowedProperties = hasCommonProperties ? { ...schema.Common, ...stateProperties } : { ...stateProperties } 28 | 29 | diagnostics.push(...getDiagnosticsForNode(oneStateValueNode, document, allowedProperties)) 30 | } 31 | 32 | return diagnostics 33 | } 34 | -------------------------------------------------------------------------------- /src/validation/validationSchema.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export default { 7 | Common: { 8 | Next: true, 9 | End: true, 10 | Comment: true, 11 | InputPath: true, 12 | OutputPath: true, 13 | QueryLanguage: true, 14 | Type: true, 15 | }, 16 | StateTypes: { 17 | Pass: { 18 | Properties: { 19 | Assign: true, 20 | Result: true, 21 | ResultPath: true, 22 | Parameters: true, 23 | Output: true, 24 | }, 25 | hasCommonProperties: true, 26 | }, 27 | Task: { 28 | Properties: { 29 | Assign: true, 30 | Resource: true, 31 | Parameters: true, 32 | Credentials: true, 33 | ResultSelector: true, 34 | ResultPath: true, 35 | TimeoutSeconds: true, 36 | TimeoutSecondsPath: true, 37 | HeartbeatSeconds: true, 38 | HeartbeatSecondsPath: true, 39 | Retry: { 40 | 'Fn:ArrayOf': 'Retrier', 41 | }, 42 | Catch: { 43 | 'Fn:ArrayOf': 'Catcher', 44 | }, 45 | Arguments: true, 46 | Output: true, 47 | }, 48 | hasCommonProperties: true, 49 | }, 50 | Choice: { 51 | Properties: { 52 | Comment: true, 53 | InputPath: true, 54 | OutputPath: true, 55 | QueryLanguage: true, 56 | Type: true, 57 | Choices: { 58 | 'Fn:ArrayOf': 'ChoiceRule', 59 | }, 60 | Output: true, 61 | Assign: true, 62 | Default: true, 63 | }, 64 | }, 65 | Wait: { 66 | Properties: { 67 | Assign: true, 68 | Seconds: true, 69 | Timestamp: true, 70 | SecondsPath: true, 71 | TimestampPath: true, 72 | Output: true, 73 | }, 74 | hasCommonProperties: true, 75 | }, 76 | Succeed: { 77 | Properties: { 78 | QueryLanguage: true, 79 | Type: true, 80 | Comment: true, 81 | InputPath: true, 82 | OutputPath: true, 83 | Output: true, 84 | }, 85 | }, 86 | Fail: { 87 | Properties: { 88 | Error: true, 89 | ErrorPath: true, 90 | Cause: true, 91 | CausePath: true, 92 | Comment: true, 93 | QueryLanguage: true, 94 | Type: true, 95 | }, 96 | }, 97 | Parallel: { 98 | Properties: { 99 | QueryLanguage: true, 100 | Assign: true, 101 | Branches: true, 102 | ResultPath: true, 103 | Parameters: true, 104 | ResultSelector: true, 105 | Arguments: true, 106 | Output: true, 107 | Retry: { 108 | 'Fn:ArrayOf': 'Retrier', 109 | }, 110 | Catch: { 111 | 'Fn:ArrayOf': 'Catcher', 112 | }, 113 | }, 114 | hasCommonProperties: true, 115 | }, 116 | Map: { 117 | Properties: { 118 | QueryLanguage: true, 119 | Assign: true, 120 | Iterator: true, 121 | ItemsPath: true, 122 | Items: true, 123 | MaxConcurrency: true, 124 | MaxConcurrencyPath: true, 125 | ResultPath: true, 126 | Output: true, 127 | Parameters: true, 128 | ResultSelector: true, 129 | ItemReader: true, 130 | ItemSelector: true, 131 | ItemBatcher: true, 132 | ResultWriter: true, 133 | ItemProcessor: true, 134 | ToleratedFailurePercentage: true, 135 | ToleratedFailurePercentagePath: true, 136 | ToleratedFailureCount: true, 137 | ToleratedFailureCountPath: true, 138 | Label: true, 139 | Retry: { 140 | 'Fn:ArrayOf': 'Retrier', 141 | }, 142 | Catch: { 143 | 'Fn:ArrayOf': 'Catcher', 144 | }, 145 | }, 146 | hasCommonProperties: true, 147 | }, 148 | }, 149 | ReferenceTypes: { 150 | ComparisonOperators: { 151 | And: { 152 | 'Fn:ArrayOf': 'NestedChoiceRule', 153 | }, 154 | Not: { 155 | 'Fn:ValueOf': 'NestedChoiceRule', 156 | }, 157 | Or: { 158 | 'Fn:ArrayOf': 'NestedChoiceRule', 159 | }, 160 | BooleanEquals: true, 161 | NumericEquals: true, 162 | NumericGreaterThan: true, 163 | NumericGreaterThanEquals: true, 164 | NumericLessThan: true, 165 | NumericLessThanEquals: true, 166 | StringEquals: true, 167 | StringGreaterThan: true, 168 | StringGreaterThanEquals: true, 169 | StringLessThan: true, 170 | StringLessThanEquals: true, 171 | TimestampEquals: true, 172 | TimestampGreaterThan: true, 173 | TimestampGreaterThanEquals: true, 174 | TimestampLessThan: true, 175 | TimestampLessThanEquals: true, 176 | BooleanEqualsPath: true, 177 | NumericEqualsPath: true, 178 | NumericGreaterThanPath: true, 179 | NumericGreaterThanEqualsPath: true, 180 | NumericLessThanPath: true, 181 | NumericLessThanEqualsPath: true, 182 | StringEqualsPath: true, 183 | StringGreaterThanPath: true, 184 | StringGreaterThanEqualsPath: true, 185 | StringLessThanPath: true, 186 | StringLessThanEqualsPath: true, 187 | TimestampEqualsPath: true, 188 | TimestampGreaterThanPath: true, 189 | TimestampGreaterThanEqualsPath: true, 190 | TimestampLessThanPath: true, 191 | TimestampLessThanEqualsPath: true, 192 | StringMatches: true, 193 | IsNull: true, 194 | IsPresent: true, 195 | IsNumeric: true, 196 | IsString: true, 197 | IsBoolean: true, 198 | IsTimestamp: true, 199 | }, 200 | ChoiceRule: { 201 | 'Fn:OneOf': 'ComparisonOperators', 202 | Assign: true, 203 | Variable: true, 204 | Condition: true, 205 | Output: true, 206 | Next: true, 207 | Comment: true, 208 | }, 209 | NestedChoiceRule: { 210 | 'Fn:OneOf': 'ComparisonOperators', 211 | Assign: true, 212 | Variable: true, 213 | Comment: true, 214 | }, 215 | Catcher: { 216 | ErrorEquals: true, 217 | ResultPath: true, 218 | Output: true, 219 | Next: true, 220 | Comment: true, 221 | Assign: true, 222 | }, 223 | Retrier: { 224 | ErrorEquals: true, 225 | IntervalSeconds: true, 226 | MaxAttempts: true, 227 | BackoffRate: true, 228 | Comment: true, 229 | MaxDelaySeconds: true, 230 | JitterStrategy: true, 231 | }, 232 | }, 233 | Root: { 234 | Comment: true, 235 | StartAt: true, 236 | TimeoutSeconds: true, 237 | Version: true, 238 | QueryLanguage: true, 239 | States: true, 240 | }, 241 | // State machines nested within Map and Parallel states 242 | NestedParallelRoot: { 243 | Comment: true, 244 | StartAt: true, 245 | QueryLanguage: true, 246 | States: true, 247 | }, 248 | NestedMapRoot: { 249 | Comment: true, 250 | StartAt: true, 251 | QueryLanguage: true, 252 | States: true, 253 | ProcessorConfig: true, 254 | }, 255 | } 256 | -------------------------------------------------------------------------------- /src/yaml/aslYamlLanguageService.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { 7 | CompletionItemKind, 8 | Diagnostic, 9 | getLanguageService as getLanguageServiceVscode, 10 | JSONSchema, 11 | LanguageService, 12 | LanguageServiceParams, 13 | } from 'vscode-json-languageservice' 14 | import { TextDocument } from 'vscode-languageserver-textdocument' 15 | import { 16 | CompletionItem, 17 | CompletionList, 18 | DocumentSymbol, 19 | FormattingOptions, 20 | Hover, 21 | Position, 22 | Range, 23 | SymbolInformation, 24 | TextEdit, 25 | } from 'vscode-languageserver-types' 26 | import { 27 | parse as parseYAML, 28 | YAMLDocument, 29 | } from 'yaml-language-server/out/server/src/languageservice/parser/yamlParser07' 30 | import { YAMLCompletion } from 'yaml-language-server/out/server/src/languageservice/services/yamlCompletion' 31 | import { YAMLFormatter } from 'yaml-language-server/out/server/src/languageservice/services/yamlFormatter' 32 | import { YAMLSchemaService } from 'yaml-language-server/out/server/src/languageservice/services/yamlSchemaService' 33 | import { matchOffsetToDocument } from 'yaml-language-server/out/server/src/languageservice/utils/arrUtils' 34 | import { YAMLDocDiagnostic } from 'yaml-language-server/out/server/src/languageservice/utils/parseUtils' 35 | import doCompleteAsl from '../completion/completeAsl' 36 | import { LANGUAGE_IDS } from '../constants/constants' 37 | import { YAML_PARSER_MESSAGES } from '../constants/diagnosticStrings' 38 | import { ASLOptions } from '../utils/astUtilityFunctions' 39 | import { convertJsonSnippetToYaml, getOffsetData, processYamlDocForCompletion } from './yamlUtils' 40 | 41 | const CATCH_INSERT = 'Catch:\n\t- ' 42 | const RETRY_INSERT = 'Retry:\n\t- ' 43 | 44 | function convertYAMLDiagnostic(yamlDiagnostic: YAMLDocDiagnostic, textDocument: TextDocument): Diagnostic { 45 | const startLoc = yamlDiagnostic.location.start 46 | let endLoc = yamlDiagnostic.location.end 47 | let severity = yamlDiagnostic.severity 48 | 49 | // Duplicate positioning returns incorrect end position and needs to be ovewritten 50 | if (yamlDiagnostic.message === YAML_PARSER_MESSAGES.DUPLICATE_KEY) { 51 | const text = textDocument.getText() 52 | // Update severity to error 53 | severity = 1 54 | 55 | for (let loc = yamlDiagnostic.location.start; loc < text.length; loc++) { 56 | // Colon and whitespace character signal the end of the key. 57 | if (text.slice(loc, loc + 2).match(/:\s/)) { 58 | endLoc = loc 59 | } else if (text[loc] === '\n') { 60 | break 61 | } 62 | } 63 | } 64 | 65 | const startPos = textDocument.positionAt(startLoc) 66 | const endPos = textDocument.positionAt(endLoc) 67 | 68 | return { 69 | range: Range.create(startPos, endPos), 70 | message: yamlDiagnostic.message, 71 | severity, 72 | } 73 | } 74 | 75 | export const getLanguageService = function ( 76 | params: LanguageServiceParams, 77 | schema: JSONSchema, 78 | aslLanguageService: LanguageService, 79 | ): LanguageService { 80 | const builtInParams = {} 81 | 82 | const languageService = getLanguageServiceVscode({ 83 | ...params, 84 | ...builtInParams, 85 | }) 86 | 87 | const requestServiceMock = async function (_uri: string): Promise { 88 | return new Promise((c) => { 89 | c(JSON.stringify(schema)) 90 | }) 91 | } 92 | const schemaService = new YAMLSchemaService(requestServiceMock, params.workspaceContext) 93 | // initialize schema 94 | schemaService.registerExternalSchema(LANGUAGE_IDS.YAML, ['*'], schema) 95 | schemaService.getOrAddSchemaHandle(LANGUAGE_IDS.YAML, schema) 96 | 97 | const completer = new YAMLCompletion(schemaService) 98 | const formatter = new YAMLFormatter() 99 | 100 | languageService.doValidation = async function (textDocument: TextDocument) { 101 | const yamlDocument: YAMLDocument = parseYAML(textDocument.getText()) 102 | const validationResult: Diagnostic[] = [] 103 | 104 | for (const currentYAMLDoc of yamlDocument.documents) { 105 | const validation = await aslLanguageService.doValidation(textDocument, currentYAMLDoc) 106 | validationResult.push( 107 | ...currentYAMLDoc.errors.concat(currentYAMLDoc.warnings).map((err) => convertYAMLDiagnostic(err, textDocument)), 108 | ) 109 | validationResult.push(...validation) 110 | } 111 | 112 | return validationResult 113 | } 114 | 115 | languageService.doComplete = async function (document: TextDocument, position: Position): Promise { 116 | const { 117 | modifiedDocText, 118 | tempPositionForCompletions, 119 | startPositionForInsertion, 120 | endPositionForInsertion, 121 | shouldPrependSpace, 122 | } = processYamlDocForCompletion(document, position) 123 | 124 | const processedDocument = TextDocument.create(document.uri, document.languageId, document.version, modifiedDocText) 125 | 126 | const offsetIntoOriginalDocument = document.offsetAt(position) 127 | const offsetIntoProcessedDocument = processedDocument.offsetAt(tempPositionForCompletions) 128 | 129 | const processedYamlDoc: YAMLDocument = parseYAML(modifiedDocText) 130 | const currentDoc = matchOffsetToDocument(offsetIntoProcessedDocument, processedYamlDoc) 131 | 132 | if (!currentDoc) { 133 | return { items: [], isIncomplete: false } 134 | } 135 | 136 | const positionForDoComplete = { ...tempPositionForCompletions } // Copy position to new object since doComplete modifies the position 137 | const yamlCompletions = await completer.doComplete(processedDocument, positionForDoComplete, false) 138 | // yaml-language-server does not output correct completions for retry/catch 139 | // we need to overwrite the text 140 | function updateCompletionText(item: CompletionItem, text: string) { 141 | item.insertText = text 142 | 143 | if (item.textEdit) { 144 | item.textEdit.newText = text 145 | } 146 | } 147 | 148 | yamlCompletions.items.forEach((item) => { 149 | if (item.label === 'Catch') { 150 | updateCompletionText(item, CATCH_INSERT) 151 | } else if (item.label === 'Retry') { 152 | updateCompletionText(item, RETRY_INSERT) 153 | } 154 | }) 155 | 156 | const { isDirectChildOfStates, isWithinCatchRetryState, hasCatchPropSibling, hasRetryPropSibling } = getOffsetData( 157 | document, 158 | offsetIntoOriginalDocument, 159 | ) 160 | 161 | const aslOptions: ASLOptions = { 162 | ignoreColonOffset: true, 163 | shouldShowStateSnippets: isDirectChildOfStates, 164 | shouldShowErrorSnippets: { 165 | retry: isWithinCatchRetryState && !hasRetryPropSibling, 166 | catch: isWithinCatchRetryState && !hasCatchPropSibling, 167 | }, 168 | } 169 | 170 | const aslCompletions: CompletionList = await doCompleteAsl( 171 | processedDocument, 172 | tempPositionForCompletions, 173 | currentDoc, 174 | yamlCompletions, 175 | aslOptions, 176 | ) 177 | 178 | const modifiedAslCompletionItems: CompletionItem[] = aslCompletions.items.map((completionItem) => { 179 | const completionItemCopy = { ...completionItem } // Copy completion to new object to avoid overwriting any snippets 180 | 181 | if ( 182 | completionItemCopy.insertText && 183 | completionItemCopy.kind === CompletionItemKind.Snippet && 184 | document.languageId === LANGUAGE_IDS.YAML 185 | ) { 186 | completionItemCopy.insertText = convertJsonSnippetToYaml(completionItemCopy.insertText) 187 | } else { 188 | const currentTextEdit = completionItemCopy.textEdit 189 | 190 | if (currentTextEdit) { 191 | if (shouldPrependSpace) { 192 | if (currentTextEdit.newText && currentTextEdit.newText.charAt(0) !== ' ') { 193 | currentTextEdit.newText = ' ' + currentTextEdit.newText 194 | } 195 | if (completionItemCopy.insertText && completionItemCopy.insertText.charAt(0) !== ' ') { 196 | completionItemCopy.insertText = ' ' + completionItemCopy.insertText 197 | } 198 | } 199 | 200 | if (TextEdit.is(currentTextEdit)) { 201 | currentTextEdit.range.start = startPositionForInsertion 202 | currentTextEdit.range.end = endPositionForInsertion 203 | // Completions that include both a key and a value should replace everything right of the cursor. 204 | if (completionItemCopy.kind === CompletionItemKind.Property) { 205 | currentTextEdit.range.end = { 206 | line: endPositionForInsertion.line, 207 | character: document.getText().length, 208 | } 209 | } 210 | } 211 | } 212 | } 213 | 214 | return completionItemCopy 215 | }) 216 | 217 | const modifiedAslCompletions: CompletionList = { 218 | isIncomplete: aslCompletions.isIncomplete, 219 | items: modifiedAslCompletionItems, 220 | } 221 | 222 | return Promise.resolve(modifiedAslCompletions) 223 | } 224 | 225 | languageService.doHover = function (document: TextDocument, position: Position): Thenable { 226 | const doc = parseYAML(document.getText()) 227 | const offset = document.offsetAt(position) 228 | const currentDoc = matchOffsetToDocument(offset, doc) 229 | if (!currentDoc) { 230 | // tslint:disable-next-line: no-null-keyword 231 | return Promise.resolve(null) 232 | } 233 | 234 | const currentDocIndex = doc.documents.indexOf(currentDoc) 235 | currentDoc.currentDocIndex = currentDocIndex 236 | 237 | return aslLanguageService.doHover(document, position, currentDoc) 238 | } 239 | 240 | languageService.format = function (document: TextDocument, range: Range, options: FormattingOptions): TextEdit[] { 241 | try { 242 | return formatter.format(document, options) 243 | } catch (error) { 244 | return [] 245 | } 246 | } 247 | 248 | languageService.findDocumentSymbols = function (document: TextDocument): SymbolInformation[] { 249 | const doc = parseYAML(document.getText()) 250 | if (!doc || doc.documents.length === 0) { 251 | return [] 252 | } 253 | 254 | let results: any[] = [] 255 | for (const yamlDoc of doc.documents) { 256 | if (yamlDoc.root) { 257 | results = results.concat(aslLanguageService.findDocumentSymbols(document, yamlDoc)) 258 | } 259 | } 260 | 261 | return results 262 | } 263 | 264 | languageService.findDocumentSymbols2 = function (document: TextDocument): DocumentSymbol[] { 265 | const doc = parseYAML(document.getText()) 266 | if (!doc || doc.documents.length === 0) { 267 | return [] 268 | } 269 | 270 | let results: any[] = [] 271 | for (const yamlDoc of doc.documents) { 272 | if (yamlDoc.root) { 273 | results = results.concat(aslLanguageService.findDocumentSymbols2(document, yamlDoc)) 274 | } 275 | } 276 | 277 | return results 278 | } 279 | 280 | return languageService 281 | } 282 | -------------------------------------------------------------------------------- /src/yamlLanguageService.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { LanguageService, JSONSchema } from 'vscode-json-languageservice' 7 | import { getLanguageService as getAslYamlLanguageService } from './yaml/aslYamlLanguageService' 8 | import { ASLLanguageServiceParams, getLanguageService } from './jsonLanguageService' 9 | import aslSchema from './json-schema/bundled.json' 10 | 11 | const ASL_SCHEMA = aslSchema as JSONSchema 12 | 13 | export const getYamlLanguageService = function (params: ASLLanguageServiceParams): LanguageService { 14 | const aslLanguageService: LanguageService = getLanguageService({ 15 | ...params, 16 | aslOptions: { 17 | ignoreColonOffset: true, 18 | }, 19 | }) 20 | return getAslYamlLanguageService(params, ASL_SCHEMA, aslLanguageService) 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "rootDir": "src", 5 | "sourceMap": true, 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "strictNullChecks": true, 10 | "skipLibCheck": true, 11 | "module": "CommonJS" 12 | }, 13 | "include": ["src"], 14 | "exclude": ["node_modules"] 15 | } 16 | --------------------------------------------------------------------------------