├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── Gemfile ├── LICENSE ├── NOTICE.txt ├── README.md ├── Rakefile ├── bin └── statelint ├── data └── StateMachine.j2119 ├── lib ├── statelint.rb └── statelint │ └── state_node.rb ├── spec ├── state_node_spec.rb └── statelint_spec.rb ├── statelint.gemspec └── test ├── backoff-with-backoffrate-between-0-and-1.json ├── backoff-with-backoffrate-decimal.json ├── backoff-with-backoffrate-integer.json ├── backoff-with-backoffrate-negative.json ├── backoff-with-invalid-jitter-on-retry.json ├── backoff-with-jitter-on-retry.json ├── choice-nested-comment.json ├── choice-rule-with-comment.json ├── choice-with-context-object.json ├── choice-with-parameters.json ├── choice-with-resultpath.json ├── choice-with-resultselector.json ├── empty-error-equals-on-catch.json ├── empty-error-equals-on-retry.json ├── fail-with-error-and-cause-path-using-intrinsic-functions.json ├── fail-with-error-and-cause-path.json ├── fail-with-parameters.json ├── fail-with-resultpath.json ├── fail-with-resultselector.json ├── fail-with-static-and-dynamic-cause.json ├── fail-with-static-and-dynamic-error.json ├── has-dupes.json ├── invalid-backoff-with-jitter-on-retry.json ├── invalid-function-invocation.json ├── linked-parallel.json ├── map-with-itemprocessor-and-iterator.json ├── map-with-itemprocessor.json ├── map-with-itemspath-context-object.json ├── map-with-null-itemspath.json ├── map-with-parameters-and-itemselector.json ├── map-with-parameters.json ├── map-with-resultselector.json ├── minimal-fail-state.json ├── no-terminal.json ├── parallel-with-parameters.json ├── parallel-with-resultpath.json ├── parallel-with-resultselector.json ├── parameter-path-problems.json ├── pass-with-intrinsic-function-inputpath.json ├── pass-with-io-path-context-object.json ├── pass-with-null-inputpath.json ├── pass-with-null-outputpath.json ├── pass-with-parameters.json ├── pass-with-resultpath.json ├── pass-with-resultselector.json ├── states-array-intrinsic-functions.json ├── states-array-invocation-leftpad.json ├── states-array-invocation.json ├── states-encoding-decoding-intrinsic-functions.json ├── states-format-invocation.json ├── states-hash-intrinsic-functions.json ├── states-json-intrinsic-functions.json ├── states-jsontostring-invocation.json ├── states-math-intrinsic-functions.json ├── states-string-intrinsic-functions.json ├── states-stringtojson-invocation.json ├── states-uuid-intrinsic-functions.json ├── succeed-with-comment-in-catcher.json ├── succeed-with-parameters.json ├── succeed-with-resultpath.json ├── succeed-with-resultselector.json ├── task-with-credentials.json ├── task-with-dynamic-timeouts.json ├── task-with-invalid-credentials.json ├── task-with-parameters.json ├── task-with-resultpath.json ├── task-with-resultselector.json ├── task-with-static-and-dynamic-heartbeat.json ├── task-with-static-and-dynamic-timeout.json ├── test_retval.sh ├── wait-with-parameters.json ├── wait-with-resultpath.json └── wait-with-resultselector.json /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | ruby-version: ['3.2', '2.7'] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Ruby ${{ matrix.ruby-version }} 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | - name: Install dependencies 25 | run: bundle install 26 | - name: Run tests 27 | run: bundle exec rspec 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.gem 3 | .bundle 4 | vendor 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'j2119', '>=0.4.0' 4 | gem 'rake', '>=12.3.2' 5 | gem 'rspec', '>=3.8.0' 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | statelint 2 | 3 | Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # statelint 2 | A Ruby gem that provides a command-line validator for Amazon States Language JSON files. 3 | 4 | ## Installation 5 | 6 | Add this line to your application's Gemfile: 7 | 8 | ```ruby 9 | gem 'statelint' 10 | ``` 11 | 12 | And then execute: 13 | 14 | $ bundle 15 | 16 | Or install it yourself as: 17 | 18 | $ gem install statelint 19 | 20 | ## Usage 21 | 22 | statelint state-machine-spec, state-machinespec... 23 | 24 | There are no options. If you see no output, your state machine is fine. 25 | 26 | ## To do 27 | 28 | Currently covers most of the grammatical constraints for state-machine 29 | definitions. The checking of JsonPath syntax is hand-built and probably 30 | imperfect. 31 | 32 | The Ruby JSON parser unfortunately does not detect duplicate keys 33 | in objects, so neither does statelint. 34 | 35 | ## Contributing 36 | 37 | Bug reports and pull requests are welcome on GitHub 38 | 39 | ### Testing 40 | 41 | All changes should be covered by unit tests in the `spec` directory. To run tests, execute the following from the package root: 42 | 43 | ``` 44 | bundle exec rspec 45 | ``` 46 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | lib_dir = File.expand_path(File.join(File.dirname(__FILE__), "lib")) 2 | $:.unshift(lib_dir) 3 | $:.uniq! 4 | 5 | require 'rspec/core/rake_task' 6 | 7 | task :default => :spec 8 | 9 | RSpec::Core::RakeTask.new 10 | 11 | -------------------------------------------------------------------------------- /bin/statelint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 5 | # not use this file except in compliance with the License. A copy of the 6 | # License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the LICENSE.txt file accompanying this file. This file is distributed 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | # express or implied. See the License for the specific language governing 13 | # permissions and limitations under the License. 14 | 15 | require 'statelint' 16 | 17 | linter = StateMachineLint::Linter.new 18 | 19 | # arguments are JSON filenames 20 | ARGV.each do |file| 21 | problems = linter.validate(ARGV[0]) 22 | 23 | if !problems.empty? 24 | header = (problems.size == 1) ? 'One error:' : "#{problems.size} errors:" 25 | puts header 26 | problems.each do |problem| 27 | puts " #{problem}" 28 | end 29 | 30 | exit 1 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /data/StateMachine.j2119: -------------------------------------------------------------------------------- 1 | This document specifies a JSON object called a "State Machine". 2 | A State Machine MUST have an object field named "States"; each field is a "State". 3 | A State Machine MUST have a string field named "StartAt". 4 | A State Machine MAY have a string field named "Comment". 5 | A State Machine MAY have a string field named "Version". 6 | A State Machine MAY have a positive-integer field named "TimeoutSeconds" whose value MUST be less than 99999999. 7 | A State MUST have a string field named "Type" whose value MUST be one of "Pass", "Succeed", "Fail", "Task", "Choice", "Wait", "Parallel", or "Map". 8 | A State whose "Type" field's value is "Pass" is a "Pass State". 9 | A State whose "Type" field's value is "Succeed" is a "Succeed State". 10 | A State whose "Type" field's value is "Fail" is a "Fail State". 11 | A State whose "Type" field's value is "Task" is a "Task State". 12 | A State whose "Type" field's value is "Choice" is a "Choice State". 13 | A State whose "Type" field's value is "Wait" is a "Wait State". 14 | A State whose "Type" field's value is "Parallel" is a "Parallel State". 15 | A State whose "Type" field's value is "Map" is a "Map State". 16 | A State MAY have a string field named "Comment". 17 | Each of a Pass State, a Task State, a Wait State, a Parallel State, and a Map State MAY have a boolean field named "End". 18 | Each of a Pass State, a Task State, a Parallel State, and a Map State MAY have a field named "Parameters". 19 | A State whose "End" field's value is true is a "Terminal State". 20 | Each of a Succeed State and a Fail State is a "Terminal State". 21 | A State which is not a Terminal State or a Choice State MUST have a string field named "Next". 22 | A Terminal State MUST NOT have a field named "Next". 23 | A State MAY have a field named "InputPath". 24 | Each of a Pass State, a Task State, a Parallel State, and a Map State MAY have a nullable-referencePath field named "ResultPath". 25 | Each of a Task State, a Parallel State, and a Map State MAY have a field named "ResultSelector". 26 | A State MAY have a field named "OutputPath". 27 | A Pass State MAY have a field named "Result". 28 | A Fail State MUST NOT have a field named "InputPath". 29 | A Fail State MUST NOT have a field named "OutputPath". 30 | A Fail State MAY have a string field named "Cause". 31 | A Fail State MAY have a field named "CausePath". 32 | A Fail State MAY have only one of "Cause" and "CausePath". 33 | A Fail State MAY have a string field named "Error". 34 | A Fail State MAY have a field named "ErrorPath". 35 | A Fail State MAY have only one of "Error" and "ErrorPath". 36 | Each of a Task State, a Parallel State, and a Map State MAY have an object-array field named "Retry"; each element is a "Retrier". 37 | A Task State MUST have a URI field named "Resource". 38 | A Task State MAY have a positive-integer field named "TimeoutSeconds" whose value MUST be less than 99999999. 39 | A Task State MAY have a positive-integer field named "HeartbeatSeconds" whose value MUST be less than 99999999. 40 | A Task State MAY have a referencePath field named "TimeoutSecondsPath". 41 | A Task State MAY have a referencePath field named "HeartbeatSecondsPath". 42 | A Task State MAY have only one of "TimeoutSeconds" and "TimeoutSecondsPath". 43 | A Task State MAY have only one of "HeartbeatSeconds" and "HeartbeatSecondsPath". 44 | A Task State MAY have an object field named "Credentials". 45 | A Retrier MUST have a nonempty-string-array field named "ErrorEquals". 46 | A Retrier MAY have an positive-integer field named "IntervalSeconds". 47 | A Retrier MAY have a nonnegative-integer field named "MaxAttempts" whose value MUST be less than 99999999. 48 | A Retrier MAY have a numeric field named "BackoffRate" whose value MUST be greater than or equal to 1. 49 | A Retrier MAY have a positive-integer field named "MaxDelaySeconds". 50 | A Retrier MAY have a string field named "JitterStrategy" whose value MUST be one of "FULL" or "NONE". 51 | Each of a Task State, a Parallel State, and a Map State MAY have an object-array field named "Catch"; each element is a "Catcher". 52 | A Catcher MUST have an nonempty-string-array field named "ErrorEquals". 53 | A Catcher MUST have a string field named "Next". 54 | A Catcher MAY have a nullable-referencePath field named "ResultPath". 55 | A Choice State MUST have a nonempty-object-array field named "Choices"; each element is a "Choice Rule". 56 | A Choice State MUST NOT have a field named "Next". 57 | A Choice State MUST NOT have a field named "End". 58 | A Choice State MAY have a string field named "Default". 59 | A Choice Rule MAY have a nonempty-object-array field named "And"; each element is a "Nested Rule". 60 | A Choice Rule MAY have a nonempty-object-array field named "Or"; each element is a "Nested Rule". 61 | A Choice Rule MAY have an object field named "Not"; its value is a "Nested Rule". 62 | A Choice Rule MUST have a string field named "Next". 63 | A Choice Rule with an "And" field is a "Boolean". 64 | A Choice Rule with an "Or" field is a "Boolean". 65 | A Choice Rule with a "Not" field is a "Boolean". 66 | A Choice Rule MAY have a field named "Variable". 67 | A Choice Rule with a "Variable" field is a "Comparison". 68 | A Choice Rule MAY have a string field named "Comment". 69 | A Comparison MUST have a field named one of "StringEquals", "StringLessThan", "StringGreaterThan", "StringLessThanEquals", "StringGreaterThanEquals", "NumericEquals", "NumericLessThan", "NumericGreaterThan", "NumericLessThanEquals", "NumericGreaterThanEquals", "BooleanEquals", "TimestampEquals", "TimestampLessThan", "TimestampGreaterThan", "TimestampLessThanEquals", "TimestampGreaterThanEquals", "StringEqualsPath", "StringLessThanPath", "StringGreaterThanPath", "StringLessThanEqualsPath", "StringGreaterThanEqualsPath", "NumericEqualsPath", "NumericLessThanPath", "NumericGreaterThanPath", "NumericLessThanEqualsPath", "NumericGreaterThanEqualsPath", "BooleanEqualsPath", "TimestampEqualsPath", "TimestampLessThanPath", "TimestampGreaterThanPath", "TimestampLessThanEqualsPath", "TimestampGreaterThanEqualsPath", "IsNull", "IsPresent", "IsNumeric", "IsString", "IsBoolean", "IsTimestamp", or "StringMatches". 70 | A Comparison MAY have a string field named "StringEquals". 71 | A Comparison MAY have a string field named "StringLessThan". 72 | A Comparison MAY have a string field named "StringGreaterThan". 73 | A Comparison MAY have a string field named "StringLessThanEquals". 74 | A Comparison MAY have a string field named "StringGreaterThanEquals". 75 | A Comparison MAY have a numeric field named "NumericEquals". 76 | A Comparison MAY have a numeric field named "NumericLessThan". 77 | A Comparison MAY have a numeric field named "NumericGreaterThan". 78 | A Comparison MAY have a numeric field named "NumericLessThanEquals". 79 | A Comparison MAY have a numeric field named "NumericGreaterThanEquals". 80 | A Comparison MAY have a boolean field named "BooleanEquals". 81 | A Comparison MAY have a timestamp field named "TimestampEquals". 82 | A Comparison MAY have a timestamp field named "TimestampLessThan". 83 | A Comparison MAY have a timestamp field named "TimestampGreaterThan". 84 | A Comparison MAY have a timestamp field named "TimestampLessThanEquals". 85 | A Comparison MAY have a timestamp field named "TimestampGreaterThanEquals". 86 | A Comparison MAY have a boolean field named "IsNull". 87 | A Comparison MAY have a boolean field named "IsPresent". 88 | A Comparison MAY have a boolean field named "IsNumeric". 89 | A Comparison MAY have a boolean field named "IsString". 90 | A Comparison MAY have a boolean field named "IsBoolean". 91 | A Comparison MAY have a boolean field named "IsTimestamp". 92 | A Comparison MAY have a string field named "StringMatches". 93 | A Comparison MAY have a referencePath field named "StringEqualsPath". 94 | A Comparison MAY have a referencePath field named "StringLessThanPath". 95 | A Comparison MAY have a referencePath field named "StringGreaterThanPath". 96 | A Comparison MAY have a referencePath field named "StringLessThanEqualsPath". 97 | A Comparison MAY have a referencePath field named "StringGreaterThanEqualsPath". 98 | A Comparison MAY have a referencePath field named "NumericEqualsPath". 99 | A Comparison MAY have a referencePath field named "NumericLessThanPath". 100 | A Comparison MAY have a referencePath field named "NumericGreaterThanPath". 101 | A Comparison MAY have a referencePath field named "NumericLessThanEqualsPath". 102 | A Comparison MAY have a referencePath field named "NumericGreaterThanEqualsPath". 103 | A Comparison MAY have a referencePath field named "BooleanEqualsPath". 104 | A Comparison MAY have a referencePath field named "TimestampEqualsPath". 105 | A Comparison MAY have a referencePath field named "TimestampLessThanPath". 106 | A Comparison MAY have a referencePath field named "TimestampGreaterThanPath". 107 | A Comparison MAY have a referencePath field named "TimestampLessThanEqualsPath". 108 | A Comparison MAY have a referencePath field named "TimestampGreaterThanEqualsPath". 109 | A Comparison MUST NOT have a field named "And". 110 | A Comparison MUST NOT have a field named "Or". 111 | A Comparison MUST NOT have a field named "Not". 112 | A Nested Rule MAY have a nonempty-object-array field named "And"; each element is a "Nested Rule". 113 | A Nested Rule MAY have a nonempty-object-array field named "Or"; each element is a "Nested Rule". 114 | A Nested Rule MAY have an object field named "Not"; its value is a "Nested Rule". 115 | A Nested Rule with an "And" field is a "Nested Boolean". 116 | A Nested Rule with an "Or" field is a "Nested Boolean". 117 | A Nested Rule with a "Not" field is a "Nested Boolean". 118 | A Nested Rule MUST NOT have a field named "Next". 119 | A Nested Rule MAY have a field named "Variable". 120 | A Nested Rule MAY have a string field named "Comment". 121 | A Nested Rule with a "Variable" field is a "Nested Comparison". 122 | A Nested Comparison MAY have a string field named "StringEquals". 123 | A Nested Comparison MAY have a string field named "StringLessThan". 124 | A Nested Comparison MAY have a string field named "StringGreaterThan". 125 | A Nested Comparison MAY have a string field named "StringLessThanEquals". 126 | A Nested Comparison MAY have a string field named "StringGreaterThanEquals". 127 | A Nested Comparison MAY have a numeric field named "NumericEquals". 128 | A Nested Comparison MAY have a numeric field named "NumericLessThan". 129 | A Nested Comparison MAY have a numeric field named "NumericGreaterThan". 130 | A Nested Comparison MAY have a numeric field named "NumericLessThanEquals". 131 | A Nested Comparison MAY have a numeric field named "NumericGreaterThanEquals". 132 | A Nested Comparison MAY have a boolean field named "BooleanEquals". 133 | A Nested Comparison MAY have a timestamp field named "TimestampEquals". 134 | A Nested Comparison MAY have a timestamp field named "TimestampLessThan". 135 | A Nested Comparison MAY have a timestamp field named "TimestampGreaterThan". 136 | A Nested Comparison MAY have a timestamp field named "TimestampLessThanEquals". 137 | A Nested Comparison MAY have a timestamp field named "TimestampGreaterThanEquals". 138 | A Nested Comparison MAY have a boolean field named "IsNull". 139 | A Nested Comparison MAY have a boolean field named "IsPresent". 140 | A Nested Comparison MAY have a boolean field named "IsNumeric". 141 | A Nested Comparison MAY have a boolean field named "IsString". 142 | A Nested Comparison MAY have a boolean field named "IsBoolean". 143 | A Nested Comparison MAY have a boolean field named "IsTimestamp". 144 | A Nested Comparison MAY have a string field named "StringMatches". 145 | A Nested Comparison MAY have a referencePath field named "StringEqualsPath". 146 | A Nested Comparison MAY have a referencePath field named "StringLessThanPath". 147 | A Nested Comparison MAY have a referencePath field named "StringGreaterThanPath". 148 | A Nested Comparison MAY have a referencePath field named "StringLessThanEqualsPath". 149 | A Nested Comparison MAY have a referencePath field named "StringGreaterThanEqualsPath". 150 | A Nested Comparison MAY have a referencePath field named "NumericEqualsPath". 151 | A Nested Comparison MAY have a referencePath field named "NumericLessThanPath". 152 | A Nested Comparison MAY have a referencePath field named "NumericGreaterThanPath". 153 | A Nested Comparison MAY have a referencePath field named "NumericLessThanEqualsPath". 154 | A Nested Comparison MAY have a referencePath field named "NumericGreaterThanEqualsPath". 155 | A Nested Comparison MAY have a referencePath field named "BooleanEqualsPath". 156 | A Nested Comparison MAY have a referencePath field named "TimestampEqualsPath". 157 | A Nested Comparison MAY have a referencePath field named "TimestampLessThanPath". 158 | A Nested Comparison MAY have a referencePath field named "TimestampGreaterThanPath". 159 | A Nested Comparison MAY have a referencePath field named "TimestampLessThanEqualsPath". 160 | A Nested Comparison MAY have a referencePath field named "TimestampGreaterThanEqualsPath". 161 | A Nested Comparison MUST NOT have a field named "And". 162 | A Nested Comparison MUST NOT have a field named "Or". 163 | A Nested Comparison MUST NOT have a field named "Not". 164 | A Wait State MAY have a positive-integer field named "Seconds". 165 | A Wait State MAY have a referencePath field named "SecondsPath". 166 | A Wait State MAY have a timestamp field named "Timestamp". 167 | A Wait State MAY have a referencePath field named "TimestampPath". 168 | A Wait State MUST have only one of "Seconds", "SecondsPath", "Timestamp", and "TimestampPath". 169 | A Wait State MUST have a field named one of "Seconds", "SecondsPath", "Timestamp", or "TimestampPath". 170 | A Parallel State MUST have an object-array field named "Branches"; each element is a "Branch". 171 | A Map State MAY have an object field named "Iterator"; its value is a "ItemProcessor". 172 | A Map State MAY have an object field named "ItemProcessor"; its value is a "ItemProcessor". 173 | A Map State MUST have only one of "Iterator" and "ItemProcessor". 174 | A Map State MAY have a field named "ItemsPath". 175 | A Map State MAY have an object field named "ItemSelector". 176 | A Map State MAY have only one of "ItemSelector" and "Parameters". 177 | A Map State MAY have a numeric field named "MaxConcurrency". 178 | A Branch MUST have an object field named "States"; each field is a "State". 179 | A Branch MUST have a string field named "StartAt". 180 | A Branch MAY have a string field named "Comment". 181 | A Catcher MAY have a string field named "Comment". 182 | A ItemProcessor MAY have an object field named "ProcessorConfig". 183 | A ItemProcessor MAY have a string field named "Comment". 184 | A ItemProcessor MUST have a string field named "StartAt". 185 | A ItemProcessor MUST have an object field named "States"; each field is a "State". -------------------------------------------------------------------------------- /lib/statelint.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the LICENSE.txt file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | #!/usr/bin/env ruby 14 | 15 | require 'j2119' 16 | require 'statelint/state_node' 17 | 18 | # This module calls the J2119 validator on the supplised input, and 19 | # then performs some States-Language specific semantic checks that 20 | # aren't expressible in a J2119 schema 21 | 22 | module StateMachineLint 23 | 24 | # TODO: Semantic validations: 25 | # - Path and Reference Path validations 26 | class Linter 27 | 28 | def initialize 29 | schema = File.dirname(__FILE__) + '/../data/StateMachine.j2119' 30 | @validator = J2119::Validator.new schema 31 | end 32 | 33 | def validate json 34 | problems = @validator.validate json 35 | checker = StateNode.new 36 | checker.check(@validator.parsed, @validator.root, problems) 37 | problems 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/statelint/state_node.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the LICENSE.txt file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | #!/usr/bin/env ruby 14 | 15 | require 'j2119' 16 | 17 | module StateMachineLint 18 | 19 | # Semantic validation that can't be expressed in a J2119 schema 20 | # 21 | class StateNode 22 | 23 | def initialize 24 | # We push States nodes on here when we traverse them. 25 | # Then, whenever we find a "Next" or "Default" or "StartAt" node, 26 | # we validate that the target is there, and record that that 27 | # target has an incoming pointer 28 | # 29 | @current_states_node = [] 30 | @current_states_incoming = [] 31 | 32 | # We keep track of all the state names and complain about 33 | # dupes 34 | @all_state_names = {} 35 | @payload_builder_fields = ["Parameters", "ResultSelector"] 36 | @context_object_access_fields = [{"field"=> "InputPath", "nullable"=> true}, {"field"=> "OutputPath", "nullable"=> true}, {"field"=> "ItemsPath", "nullable"=> false}] 37 | @choice_state_nested_operators = ["And", "Or", "Not"] 38 | @intrinsic_invocation_regex = /^States\.(Format|Array|ArrayPartition|ArrayContains|ArrayRange|ArrayGetItem|ArrayLength|ArrayUnique|Base64Encode|Base64Decode|Hash|JsonMerge|JsonToString|StringToJson|MathRandom|MathAdd|StringSplit)\(.+\)$/ 39 | @intrinsic_uuid_invocation_regex = /^States\.UUID\(\)$/ 40 | end 41 | 42 | def check(node, path, problems) 43 | if !node.is_a? Hash 44 | return 45 | end 46 | 47 | is_machine_top = node.key?("States") && node['States'].is_a?(Hash) 48 | if is_machine_top 49 | @current_states_node << node['States'] 50 | start_at = node['StartAt'] 51 | if start_at && start_at.is_a?(String) 52 | @current_states_incoming << [ start_at ] 53 | if !node['States'].key?(start_at) 54 | problems << 55 | "StartAt value #{start_at} not found in " + 56 | "States field at #{path}" 57 | end 58 | else 59 | @current_states_incoming << [] 60 | end 61 | 62 | states = node['States'] 63 | states.keys.each do |name| 64 | child = states[name] 65 | if child.is_a?(Hash) 66 | child_path = path + '.' + name 67 | probe_context_object_access(child, child_path, problems) 68 | @payload_builder_fields.each do |field_name| 69 | if child.key?(field_name) 70 | probe_payload_builder(child[field_name], child_path, problems, field_name) 71 | end 72 | end 73 | if child.key?("Type") && child["Type"] == "Choice" && child.key?("Choices") 74 | probe_choice_state(child["Choices"], child_path + '.Choices' , problems) 75 | end 76 | end 77 | 78 | if @all_state_names[name] 79 | problems << 80 | "State \"#{name}\", defined at #{path}.States, " + 81 | "is also defined at #{@all_state_names[name]}" 82 | else 83 | @all_state_names[name] = "#{path}.States" 84 | end 85 | end 86 | end 87 | 88 | check_for_terminal(node, path, problems) 89 | 90 | check_next(node, path, problems) 91 | 92 | check_States_ALL(node['Retry'], path + '.Retry', problems) 93 | check_States_ALL(node['Catch'], path + '.Catch', problems) 94 | 95 | node.each do |name, val| 96 | if val.is_a?(Array) 97 | i = 0 98 | val.each do |element| 99 | check(element, "#{path}.#{name}[#{i}]", problems) 100 | i += 1 101 | end 102 | else 103 | check(val, "#{path}.#{name}", problems) 104 | end 105 | end 106 | 107 | if is_machine_top 108 | states = @current_states_node.pop 109 | incoming = @current_states_incoming.pop 110 | missing = states.keys - incoming 111 | missing.each do |state| 112 | problems << "No transition found to state #{path}.#{state}" 113 | end 114 | end 115 | end 116 | 117 | def check_next(node, path, problems) 118 | add_next(node, path, 'Next', problems) 119 | add_next(node, path, 'Default', problems) 120 | end 121 | 122 | def add_next(node, path, field, problems) 123 | if node[field] && node[field].is_a?(String) 124 | transition_to = node[field] 125 | 126 | if !@current_states_node.empty? 127 | if @current_states_node[-1].key?(transition_to) 128 | @current_states_incoming[-1] << transition_to 129 | else 130 | problems << 131 | "No state found named \"#{transition_to}\", referenced at " + 132 | "#{path}.#{field}" 133 | end 134 | end 135 | end 136 | end 137 | 138 | def probe_context_object_access(node, path, problems) 139 | @context_object_access_fields.each do |field| 140 | field_name = field["field"] 141 | nullable = field["nullable"] 142 | if node.key?(field_name) 143 | if !nullable && node[field_name].nil? 144 | problems << "Field \"#{field_name}\" defined at \"#{path}\" should be non-null" 145 | return 146 | end 147 | if !node[field_name].nil? and !is_valid_parameters_path?(node[field_name]) 148 | problems << "Field \"#{field_name}\" defined at \"#{path}\" is not a JSONPath" 149 | end 150 | end 151 | end 152 | end 153 | 154 | def probe_choice_state(node, path, problems) 155 | if node.is_a?(Hash) 156 | if node.key?("Variable") && !is_valid_parameters_path?(node["Variable"]) 157 | problems << "Field \"Variable\" of Choice state at \"#{path}\" is not a JSONPath" 158 | end 159 | @choice_state_nested_operators.each do |operator| 160 | if node.key?(operator) 161 | probe_choice_state(node[operator], path + '.' + operator, problems) 162 | end 163 | end 164 | elsif node.is_a?(Array) 165 | node.size.times {|i| probe_choice_state(node[i], "#{path}[#{i}]", problems) } 166 | end 167 | end 168 | 169 | # Search through Parameters for object nodes and check field semantics 170 | def probe_payload_builder(node, path, problems, field_name) 171 | if node.is_a?(Hash) 172 | node.each do |name, val| 173 | if name.end_with? '.$' 174 | if !is_intrinsic_invocation?(val) && !is_valid_parameters_path?(val) 175 | problems << "Field \"#{name}\" of #{field_name} at \"#{path}\" is not a JSONPath or intrinsic function expression" 176 | end 177 | else 178 | probe_payload_builder(val, "#{path}.#{name}", problems, field_name) 179 | end 180 | end 181 | elsif node.is_a?(Array) 182 | node.size.times {|i| probe_payload_builder(node[i], "#{path}[#{i}]", problems, field_name) } 183 | end 184 | end 185 | 186 | def is_intrinsic_invocation?(val) 187 | if val.is_a?(String) && (val.match?(@intrinsic_invocation_regex) || val.match?(@intrinsic_uuid_invocation_regex)) 188 | return true 189 | end 190 | return false 191 | end 192 | 193 | # Check if a string that ends with ".$" is a valid path 194 | def is_valid_parameters_path?(val) 195 | if !val.is_a?(String) 196 | return false 197 | end 198 | # If the value begins with “$$”, the first dollar character is stripped off and the remainder MUST be a Path. 199 | if val.start_with?("$$") 200 | path_to_check = val.gsub(/^\$/, "") 201 | J2119::JSONPathChecker.is_path?(path_to_check) 202 | else 203 | J2119::JSONPathChecker.is_path?(val) 204 | end 205 | end 206 | 207 | def check_for_terminal(node, path, problems) 208 | if node['States'] && node['States'].is_a?(Hash) 209 | terminal_found = false 210 | node['States'].each_value do |state_node| 211 | if state_node.is_a?(Hash) 212 | if [ 'Succeed', 'Fail' ].include?(state_node['Type']) 213 | terminal_found = true 214 | elsif state_node['End'] == true 215 | terminal_found = true 216 | end 217 | end 218 | end 219 | 220 | if !terminal_found 221 | problems << "No terminal state found in machine at #{path}.States" 222 | end 223 | end 224 | end 225 | 226 | def check_States_ALL(node, path, problems) 227 | if !node.is_a?(Array) 228 | return 229 | end 230 | 231 | i = 0 232 | node.each do |element| 233 | if element.is_a?(Hash) 234 | if element['ErrorEquals'].is_a?(Array) 235 | ee = element['ErrorEquals'] 236 | if ee.include? 'States.ALL' 237 | if i != (node.size - 1) || ee.size != 1 238 | problems << 239 | "#{path}[#{i}]: States.ALL can only appear in the last " + 240 | "element, and by itself." 241 | end 242 | end 243 | end 244 | end 245 | i += 1 246 | end 247 | end 248 | end 249 | end 250 | 251 | -------------------------------------------------------------------------------- /spec/state_node_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the LICENSE.txt file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | #!/usr/bin/env ruby 14 | 15 | require 'json' 16 | require 'statelint/state_node' 17 | 18 | describe StateMachineLint::StateNode do 19 | 20 | it 'should find missing StartAt targets' do 21 | json = '{ "StartAt": "x", ' + 22 | ' "States": {' + 23 | ' "y": {"Type":"Succeed"} ' + 24 | ' }' + 25 | '}' 26 | 27 | json = JSON.parse json 28 | problems = [] 29 | checker = StateMachineLint::StateNode.new 30 | checker.check(json, 'a.b', problems) 31 | # problems.each {|p| puts "P #{p}"} 32 | expect(problems.size).to eq(2) 33 | end 34 | 35 | it 'should catch nested problems' do 36 | json = '{ "StartAt": "x", ' + 37 | ' "States": { ' + 38 | ' "x": {' + 39 | ' "StartAt": "z",' + 40 | ' "States": { ' + 41 | ' "w": 1' + 42 | ' }' + 43 | ' }' + 44 | ' }' + 45 | '}' 46 | json = JSON.parse json 47 | problems = [] 48 | checker = StateMachineLint::StateNode.new 49 | checker.check(json, 'a.b', problems) 50 | expect(problems.size).to eq(4) 51 | end 52 | 53 | it 'should find States.ALL not in end position' do 54 | json = '{ "Retry": [' + 55 | ' { "ErrorEquals": [ "States.ALL", "other" ] },' + 56 | ' { "ErrorEquals": [ "YET ANOTHER" ] } ' + 57 | ' ] ' + 58 | '}' 59 | json = JSON.parse json 60 | problems = [] 61 | checker = StateMachineLint::StateNode.new 62 | checker.check(json, 'a.b', problems) 63 | expect(problems.size).to eq(1) 64 | end 65 | 66 | it 'should find States.ALL not by itself' do 67 | json = '{ "Retry": [' + 68 | ' { "ErrorEquals": [ "YET ANOTHER" ] }, ' + 69 | ' { "ErrorEquals": [ "States.ALL", "other" ] }' + 70 | ' ] ' + 71 | '}' 72 | json = JSON.parse json 73 | problems = [] 74 | checker = StateMachineLint::StateNode.new 75 | checker.check(json, 'a.b', problems) 76 | expect(problems.size).to eq(1) 77 | end 78 | 79 | it 'should use Default field correctly' do 80 | text = { 81 | "StartAt"=> "A", 82 | "States"=> { 83 | "A" => { 84 | "Type" => "Choice", 85 | "Choices" => [ 86 | { 87 | "Variable" => "$.a", 88 | "Next" => "B" 89 | } 90 | ], 91 | "Default" => "C" 92 | }, 93 | "B" => { 94 | "Type" => "Succeed" 95 | }, 96 | "C" => { 97 | "Type" => "Succeed" 98 | } 99 | } 100 | } 101 | json = JSON.parse(JSON.pretty_generate(text)) 102 | problems = [] 103 | checker = StateMachineLint::StateNode.new 104 | checker.check(json, 'a.b', problems) 105 | expect(problems.size).to eq(0) 106 | end 107 | 108 | it "should find Next fields with targets that don't match state names" do 109 | text = { 110 | "StartAt"=> "A", 111 | "States"=> { 112 | "A" => { 113 | "Type" => "Pass", 114 | "Next" => "B" 115 | } 116 | } 117 | } 118 | json = JSON.parse(JSON.pretty_generate(text)) 119 | problems = [] 120 | checker = StateMachineLint::StateNode.new 121 | checker.check(json, 'a.b', problems) 122 | expect(problems.size).to eq(2) 123 | end 124 | 125 | it "should find un-pointed-to states" do 126 | text = { 127 | "StartAt"=> "A", 128 | "States"=> { 129 | "A" => { 130 | "Type" => "Succeed" 131 | }, 132 | "X" => { 133 | "Type" => "Succeed" 134 | } 135 | } 136 | } 137 | json = JSON.parse(JSON.pretty_generate(text)) 138 | problems = [] 139 | checker = StateMachineLint::StateNode.new 140 | checker.check(json, 'a.b', problems) 141 | expect(problems.size).to eq(1) 142 | end 143 | 144 | it "should find missing terminal state" do 145 | text = { 146 | "StartAt"=> "A", 147 | "States"=> { 148 | "A" => { 149 | "Type" => "Pass", 150 | "Next" => "B", 151 | }, 152 | "B" => { 153 | "Type" => "C", 154 | "Next" => "A" 155 | } 156 | } 157 | } 158 | json = JSON.parse(JSON.pretty_generate(text)) 159 | problems = [] 160 | checker = StateMachineLint::StateNode.new 161 | checker.check(json, 'a.b', problems) 162 | expect(problems.size).to eq(1) 163 | end 164 | 165 | it 'should handle complex missing terminal' do 166 | j = File.read "test/no-terminal.json" 167 | j = JSON.parse j 168 | problems = [] 169 | checker = StateMachineLint::StateNode.new 170 | checker.check(j, 'a.b', problems) 171 | expect(problems.size).to eq(1) 172 | end 173 | 174 | it 'should catch linkage from one parallel branch to another' do 175 | j = File.read "test/linked-parallel.json" 176 | j = JSON.parse j 177 | problems = [] 178 | checker = StateMachineLint::StateNode.new 179 | checker.check(j, 'a.b', problems) 180 | expect(problems.size).to eq(4) 181 | end 182 | 183 | it 'should catch duplicate state names, even in parallels' do 184 | j = File.read "test/has-dupes.json" 185 | j = JSON.parse j 186 | problems = [] 187 | checker = StateMachineLint::StateNode.new 188 | checker.check(j, 'a.b', problems) 189 | expect(problems.size).to eq(1) 190 | # problems.each {|p| puts "P #{p}"} 191 | end 192 | 193 | end 194 | -------------------------------------------------------------------------------- /spec/statelint_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the LICENSE.txt file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | #!/usr/bin/env ruby 14 | 15 | require 'json' 16 | require 'statelint' 17 | 18 | describe StateMachineLint do 19 | 20 | it 'should allow Fail states to omit optional Cause/Error fields' do 21 | j = File.read "test/minimal-fail-state.json" 22 | j = JSON.parse j 23 | linter = StateMachineLint::Linter.new 24 | problems = linter.validate(j) 25 | expect(problems.size).to eq(0) 26 | end 27 | 28 | it 'should allow Fail states to use ErrorPath and CausePath fields' do 29 | j = File.read "test/fail-with-error-and-cause-path.json" 30 | j = JSON.parse j 31 | linter = StateMachineLint::Linter.new 32 | problems = linter.validate(j) 33 | expect(problems.size).to eq(0) 34 | end 35 | 36 | it 'should allow Fail states to use ErrorPath and CausePath fields with intrinsic functions' do 37 | j = File.read "test/fail-with-error-and-cause-path-using-intrinsic-functions.json" 38 | j = JSON.parse j 39 | linter = StateMachineLint::Linter.new 40 | problems = linter.validate(j) 41 | expect(problems.size).to eq(0) 42 | end 43 | 44 | it 'should reject Fail state with both static and dynamic error/cause' do 45 | j = File.read "test/fail-with-static-and-dynamic-error.json" 46 | linter = StateMachineLint::Linter.new 47 | problems = linter.validate(j) 48 | expect(problems.size).to eq(1) 49 | expect(problems[0]).to include('may have only one of ["Error", "ErrorPath"]') 50 | 51 | j = File.read "test/fail-with-static-and-dynamic-cause.json" 52 | linter = StateMachineLint::Linter.new 53 | problems = linter.validate(j) 54 | expect(problems.size).to eq(1) 55 | expect(problems[0]).to include('may have only one of ["Cause", "CausePath"]') 56 | end 57 | 58 | it 'should allow Retry to use MaxDelaySeconds and JitterStrategy' do 59 | j = File.read "test/backoff-with-jitter-on-retry.json" 60 | j = JSON.parse j 61 | linter = StateMachineLint::Linter.new 62 | problems = linter.validate(j) 63 | expect(problems.size).to eq(0) 64 | end 65 | 66 | it 'should reject invalid JitterStrategy' do 67 | j = File.read "test/backoff-with-invalid-jitter-on-retry.json" 68 | j = JSON.parse j 69 | linter = StateMachineLint::Linter.new 70 | problems = linter.validate(j) 71 | expect(problems.size).to eq(1) 72 | expect(problems[0]).to include('not one of the allowed values ["FULL", "NONE"]') 73 | end 74 | 75 | it 'should reject invalid MaxDelaySeconds' do 76 | j = File.read "test/invalid-backoff-with-jitter-on-retry.json" 77 | j = JSON.parse j 78 | linter = StateMachineLint::Linter.new 79 | problems = linter.validate(j) 80 | expect(problems.size).to eq(1) 81 | expect(problems[0]).to include('allowed floor is 0') 82 | end 83 | 84 | it 'should allow BackOffRate integer' do 85 | j = File.read "test/backoff-with-backoffrate-integer.json" 86 | j = JSON.parse j 87 | linter = StateMachineLint::Linter.new 88 | problems = linter.validate(j) 89 | expect(problems.size).to eq(0) 90 | end 91 | 92 | it 'should allow BackOffRate decimal' do 93 | j = File.read "test/backoff-with-backoffrate-decimal.json" 94 | j = JSON.parse j 95 | linter = StateMachineLint::Linter.new 96 | problems = linter.validate(j) 97 | expect(problems.size).to eq(0) 98 | end 99 | 100 | it 'should reject BackOffRate negative' do 101 | j = File.read "test/backoff-with-backoffrate-negative.json" 102 | j = JSON.parse j 103 | linter = StateMachineLint::Linter.new 104 | problems = linter.validate(j) 105 | expect(problems.size).to eq(1) 106 | expect(problems[0]).to include('allowed minimum is 1') 107 | end 108 | 109 | it 'should reject BackOffRate between 0 and 1' do 110 | j = File.read "test/backoff-with-backoffrate-between-0-and-1.json" 111 | j = JSON.parse j 112 | linter = StateMachineLint::Linter.new 113 | problems = linter.validate(j) 114 | expect(problems.size).to eq(1) 115 | expect(problems[0]).to include('allowed minimum is 1') 116 | end 117 | 118 | it 'should reject empty ErrorEquals clauses' do 119 | j = File.read "test/empty-error-equals-on-catch.json" 120 | linter = StateMachineLint::Linter.new 121 | linter = StateMachineLint::Linter.new 122 | problems = linter.validate(j) 123 | expect(problems.size).to eq(1) 124 | expect(problems[0]).to include('non-empty required') 125 | 126 | j = File.read "test/empty-error-equals-on-retry.json" 127 | linter = StateMachineLint::Linter.new 128 | linter = StateMachineLint::Linter.new 129 | problems = linter.validate(j) 130 | expect(problems.size).to eq(1) 131 | expect(problems[0]).to include('non-empty required') 132 | end 133 | 134 | it 'should reject Parameters except in Pass, Task, and Parallel' do 135 | j = File.read "test/pass-with-parameters.json" 136 | linter = StateMachineLint::Linter.new 137 | problems = linter.validate(j) 138 | problems.each { |p| puts "P: #{p}" } 139 | expect(problems.size).to eq(0) 140 | 141 | j = File.read "test/task-with-parameters.json" 142 | linter = StateMachineLint::Linter.new 143 | problems = linter.validate(j) 144 | problems.each { |p| puts "P: #{p}" } 145 | expect(problems.size).to eq(0) 146 | 147 | j = File.read "test/choice-with-parameters.json" 148 | linter = StateMachineLint::Linter.new 149 | problems = linter.validate(j) 150 | expect(problems.size).to eq(1) 151 | expect(problems[0]).to include('"Parameters"') 152 | 153 | j = File.read "test/wait-with-parameters.json" 154 | linter = StateMachineLint::Linter.new 155 | problems = linter.validate(j) 156 | expect(problems.size).to eq(1) 157 | expect(problems[0]).to include('"Parameters"') 158 | 159 | j = File.read "test/succeed-with-parameters.json" 160 | linter = StateMachineLint::Linter.new 161 | problems = linter.validate(j) 162 | expect(problems.size).to eq(1) 163 | expect(problems[0]).to include('"Parameters"') 164 | 165 | j = File.read "test/fail-with-parameters.json" 166 | linter = StateMachineLint::Linter.new 167 | problems = linter.validate(j) 168 | expect(problems.size).to eq(1) 169 | expect(problems[0]).to include('"Parameters"') 170 | 171 | j = File.read "test/parallel-with-parameters.json" 172 | linter = StateMachineLint::Linter.new 173 | problems = linter.validate(j) 174 | expect(problems.size).to eq(0) 175 | 176 | j = File.read "test/map-with-parameters.json" 177 | linter = StateMachineLint::Linter.new 178 | problems = linter.validate(j) 179 | puts problems 180 | expect(problems.size).to eq(0) 181 | end 182 | 183 | it 'should reject non-Path constructs in Parameter fields ending in ".$"' do 184 | j = File.read "test/parameter-path-problems.json" 185 | linter = StateMachineLint::Linter.new 186 | problems = linter.validate(j) 187 | expect(problems.size).to eq(5) 188 | expect(problems[0]).to include('bad1') 189 | expect(problems[1]).to include('bad2') 190 | expect(problems[2]).to include('bad3') 191 | expect(problems[3]).to include('bad4') 192 | expect(problems[4]).to include('bad5') 193 | end 194 | 195 | it 'should reject ResultPath except in Pass, Task, and Parallel' do 196 | j = File.read "test/pass-with-resultpath.json" 197 | linter = StateMachineLint::Linter.new 198 | problems = linter.validate(j) 199 | problems.each { |p| puts "P: #{p}" } 200 | expect(problems.size).to eq(0) 201 | 202 | j = File.read "test/task-with-resultpath.json" 203 | linter = StateMachineLint::Linter.new 204 | problems = linter.validate(j) 205 | problems.each { |p| puts "P: #{p}" } 206 | expect(problems.size).to eq(0) 207 | 208 | j = File.read "test/choice-with-resultpath.json" 209 | linter = StateMachineLint::Linter.new 210 | problems = linter.validate(j) 211 | expect(problems.size).to eq(1) 212 | expect(problems[0]).to include('"ResultPath"') 213 | 214 | j = File.read "test/wait-with-resultpath.json" 215 | linter = StateMachineLint::Linter.new 216 | problems = linter.validate(j) 217 | expect(problems.size).to eq(1) 218 | expect(problems[0]).to include('"ResultPath"') 219 | 220 | j = File.read "test/succeed-with-resultpath.json" 221 | linter = StateMachineLint::Linter.new 222 | problems = linter.validate(j) 223 | expect(problems.size).to eq(1) 224 | expect(problems[0]).to include('"ResultPath"') 225 | 226 | j = File.read "test/fail-with-resultpath.json" 227 | linter = StateMachineLint::Linter.new 228 | problems = linter.validate(j) 229 | expect(problems.size).to eq(1) 230 | expect(problems[0]).to include('"ResultPath"') 231 | 232 | j = File.read "test/parallel-with-resultpath.json" 233 | linter = StateMachineLint::Linter.new 234 | problems = linter.validate(j) 235 | expect(problems.size).to eq(0) 236 | end 237 | 238 | it 'should allow context object access in InputPath and OutputPath' do 239 | j = File.read "test/pass-with-io-path-context-object.json" 240 | linter = StateMachineLint::Linter.new 241 | problems = linter.validate(j) 242 | expect(problems.size).to eq(0) 243 | end 244 | 245 | it 'should allow context object access in Choice state Variable' do 246 | j = File.read "test/choice-with-context-object.json" 247 | linter = StateMachineLint::Linter.new 248 | problems = linter.validate(j) 249 | expect(problems.size).to eq(0) 250 | end 251 | 252 | it 'should allow context object access in Map state ItemsPath' do 253 | j = File.read "test/map-with-itemspath-context-object.json" 254 | linter = StateMachineLint::Linter.new 255 | problems = linter.validate(j) 256 | expect(problems.size).to eq(0) 257 | end 258 | 259 | it 'should allow dynamic timeout fields in Task state' do 260 | j = File.read "test/task-with-dynamic-timeouts.json" 261 | linter = StateMachineLint::Linter.new 262 | problems = linter.validate(j) 263 | expect(problems.size).to eq(0) 264 | end 265 | 266 | it 'should allow null values in InputPath and OutputPath' do 267 | j = File.read "test/pass-with-null-inputpath.json" 268 | linter = StateMachineLint::Linter.new 269 | problems = linter.validate(j) 270 | expect(problems.size).to eq(0) 271 | 272 | j = File.read "test/pass-with-null-outputpath.json" 273 | linter = StateMachineLint::Linter.new 274 | problems = linter.validate(j) 275 | expect(problems.size).to eq(0) 276 | end 277 | 278 | it 'should not allow null value in Map state ItemsPath' do 279 | j = File.read "test/map-with-null-itemspath.json" 280 | linter = StateMachineLint::Linter.new 281 | problems = linter.validate(j) 282 | expect(problems.size).to eq(1) 283 | expect(problems[0]).to include('"ItemsPath"') 284 | end 285 | 286 | it 'should reject ResultSelector except in Task, Parallel, and Map states' do 287 | j = File.read "test/task-with-resultselector.json" 288 | linter = StateMachineLint::Linter.new 289 | problems = linter.validate(j) 290 | expect(problems.size).to eq(0) 291 | 292 | j = File.read "test/parallel-with-resultselector.json" 293 | linter = StateMachineLint::Linter.new 294 | problems = linter.validate(j) 295 | expect(problems.size).to eq(0) 296 | 297 | j = File.read "test/map-with-resultselector.json" 298 | linter = StateMachineLint::Linter.new 299 | problems = linter.validate(j) 300 | expect(problems.size).to eq(0) 301 | 302 | j = File.read "test/pass-with-resultselector.json" 303 | linter = StateMachineLint::Linter.new 304 | problems = linter.validate(j) 305 | expect(problems.size).to eq(1) 306 | expect(problems[0]).to include('"ResultSelector"') 307 | 308 | j = File.read "test/wait-with-resultselector.json" 309 | linter = StateMachineLint::Linter.new 310 | problems = linter.validate(j) 311 | expect(problems.size).to eq(1) 312 | expect(problems[0]).to include('"ResultSelector"') 313 | 314 | j = File.read "test/fail-with-resultselector.json" 315 | linter = StateMachineLint::Linter.new 316 | problems = linter.validate(j) 317 | expect(problems.size).to eq(1) 318 | expect(problems[0]).to include('"ResultSelector"') 319 | 320 | j = File.read "test/succeed-with-resultselector.json" 321 | linter = StateMachineLint::Linter.new 322 | problems = linter.validate(j) 323 | expect(problems.size).to eq(1) 324 | expect(problems[0]).to include('"ResultSelector"') 325 | 326 | j = File.read "test/choice-with-resultselector.json" 327 | linter = StateMachineLint::Linter.new 328 | problems = linter.validate(j) 329 | expect(problems.size).to eq(1) 330 | expect(problems[0]).to include('"ResultSelector"') 331 | end 332 | 333 | it 'should allow only valid intrinsic function invocations in payload builder fields' do 334 | j = File.read "test/states-array-invocation.json" 335 | linter = StateMachineLint::Linter.new 336 | problems = linter.validate(j) 337 | expect(problems.size).to eq(0) 338 | 339 | j = File.read "test/states-format-invocation.json" 340 | linter = StateMachineLint::Linter.new 341 | problems = linter.validate(j) 342 | expect(problems.size).to eq(0) 343 | 344 | j = File.read "test/states-stringtojson-invocation.json" 345 | linter = StateMachineLint::Linter.new 346 | problems = linter.validate(j) 347 | expect(problems.size).to eq(0) 348 | 349 | j = File.read "test/states-jsontostring-invocation.json" 350 | linter = StateMachineLint::Linter.new 351 | problems = linter.validate(j) 352 | expect(problems.size).to eq(0) 353 | 354 | j = File.read "test/states-array-invocation-leftpad.json" 355 | linter = StateMachineLint::Linter.new 356 | problems = linter.validate(j) 357 | expect(problems.size).to eq(1) 358 | expect(problems[0]).to include('not a JSONPath or intrinsic function expression') 359 | 360 | j = File.read "test/invalid-function-invocation.json" 361 | linter = StateMachineLint::Linter.new 362 | problems = linter.validate(j) 363 | expect(problems.size).to eq(1) 364 | expect(problems[0]).to include('not a JSONPath or intrinsic function expression') 365 | 366 | j = File.read "test/pass-with-intrinsic-function-inputpath.json" 367 | linter = StateMachineLint::Linter.new 368 | problems = linter.validate(j) 369 | expect(problems.size).to eq(1) 370 | expect(problems[0]).to include('"InputPath"') 371 | end 372 | 373 | it 'should reject Task state with both static and dynamic timeouts' do 374 | j = File.read "test/task-with-static-and-dynamic-timeout.json" 375 | linter = StateMachineLint::Linter.new 376 | problems = linter.validate(j) 377 | expect(problems.size).to eq(1) 378 | expect(problems[0]).to include('may have only one of ["TimeoutSeconds", "TimeoutSecondsPath"]') 379 | 380 | j = File.read "test/task-with-static-and-dynamic-heartbeat.json" 381 | linter = StateMachineLint::Linter.new 382 | problems = linter.validate(j) 383 | expect(problems.size).to eq(1) 384 | expect(problems[0]).to include('may have only one of ["HeartbeatSeconds", "HeartbeatSecondsPath"]') 385 | end 386 | 387 | it 'should allow valid new intrinsic function invocations that were added in 2022' do 388 | j = File.read "test/states-array-intrinsic-functions.json" 389 | linter = StateMachineLint::Linter.new 390 | problems = linter.validate(j) 391 | expect(problems.size).to eq(0) 392 | 393 | j = File.read "test/states-encoding-decoding-intrinsic-functions.json" 394 | linter = StateMachineLint::Linter.new 395 | problems = linter.validate(j) 396 | expect(problems.size).to eq(0) 397 | 398 | j = File.read "test/states-hash-intrinsic-functions.json" 399 | linter = StateMachineLint::Linter.new 400 | problems = linter.validate(j) 401 | expect(problems.size).to eq(0) 402 | 403 | j = File.read "test/states-json-intrinsic-functions.json" 404 | linter = StateMachineLint::Linter.new 405 | problems = linter.validate(j) 406 | expect(problems.size).to eq(0) 407 | 408 | j = File.read "test/states-math-intrinsic-functions.json" 409 | linter = StateMachineLint::Linter.new 410 | problems = linter.validate(j) 411 | expect(problems.size).to eq(0) 412 | 413 | j = File.read "test/states-string-intrinsic-functions.json" 414 | linter = StateMachineLint::Linter.new 415 | problems = linter.validate(j) 416 | expect(problems.size).to eq(0) 417 | 418 | j = File.read "test/states-uuid-intrinsic-functions.json" 419 | linter = StateMachineLint::Linter.new 420 | problems = linter.validate(j) 421 | expect(problems.size).to eq(0) 422 | end 423 | 424 | it 'should allow Choice Rule to use Comment' do 425 | j = File.read "test/choice-rule-with-comment.json" 426 | j = JSON.parse j 427 | linter = StateMachineLint::Linter.new 428 | problems = linter.validate(j) 429 | expect(problems.size).to eq(0) 430 | end 431 | 432 | it 'should allow a Comment field in Catcher' do 433 | j = File.read "test/succeed-with-comment-in-catcher.json" 434 | linter = StateMachineLint::Linter.new 435 | problems = linter.validate(j) 436 | expect(problems.size).to eq(0) 437 | end 438 | 439 | it 'should allow ItemProcessor in Map' do 440 | j = File.read("test/map-with-itemprocessor.json") 441 | linter = StateMachineLint::Linter.new 442 | problems = linter.validate(j) 443 | expect(problems.size).to eq(0) 444 | end 445 | 446 | it 'should reject Map state with both Iterator and ItemProcessor' do 447 | j = File.read("test/map-with-itemprocessor-and-iterator.json") 448 | linter = StateMachineLint::Linter.new 449 | problems = linter.validate(j) 450 | expect(problems.size).to eq(1) 451 | end 452 | 453 | it 'should reject Map state with both Parameters and ItemSelector' do 454 | j = File.read("test/map-with-parameters-and-itemselector.json") 455 | linter = StateMachineLint::Linter.new 456 | problems = linter.validate(j) 457 | expect(problems.size).to eq(1) 458 | end 459 | 460 | it 'should allow Credentials in Task' do 461 | j = File.read "test/task-with-credentials.json" 462 | linter = StateMachineLint::Linter.new 463 | problems = linter.validate(j) 464 | expect(problems.size).to eq(0) 465 | end 466 | 467 | it 'should reject invalid Credentials in Task' do 468 | j = File.read "test/task-with-invalid-credentials.json" 469 | linter = StateMachineLint::Linter.new 470 | problems = linter.validate(j) 471 | expect(problems.size).to eq(1) 472 | end 473 | 474 | it 'should allow Comment in Nested Choice Rules' do 475 | j = File.read "test/choice-nested-comment.json" 476 | linter = StateMachineLint::Linter.new 477 | problems = linter.validate(j) 478 | expect(problems.size).to eq(0) 479 | end 480 | end 481 | -------------------------------------------------------------------------------- /statelint.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'statelint' 3 | s.version = '0.8.0' 4 | s.summary = "State Machine JSON validator" 5 | s.description = "Validates a JSON object representing a State Machine" 6 | s.authors = ["Tim Bray"] 7 | s.email = 'timbray@amazon.com' 8 | s.executables << 'statelint' 9 | s.files = `git ls-files`.split("\n").reject do |f| 10 | f.match(%r{^(spec|test)/}) 11 | end 12 | 13 | s.homepage = 'https://github.com/awslabs/statelint' 14 | s.license = 'Apache-2.0' 15 | 16 | s.required_ruby_version = '>= 1.9.2' 17 | 18 | s.add_runtime_dependency 'j2119', '~> 0.4', '>= 0.4.0' 19 | 20 | s.metadata = { 21 | 'source_code_uri' => 'https://github.com/awslabs/statelint', 22 | "bug_tracker_uri" => 'https://github.com/awslabs/statelint/issues' 23 | } 24 | end 25 | -------------------------------------------------------------------------------- /test/backoff-with-backoffrate-between-0-and-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Retry": [ 9 | { 10 | "ErrorEquals": ["States.Timeout"], 11 | "BackoffRate": 0.3 12 | } 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/backoff-with-backoffrate-decimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Retry": [ 9 | { 10 | "ErrorEquals": ["States.Timeout"], 11 | "BackoffRate": 2.42 12 | } 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/backoff-with-backoffrate-integer.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Retry": [ 9 | { 10 | "ErrorEquals": ["States.Timeout"], 11 | "BackoffRate": 3 12 | } 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/backoff-with-backoffrate-negative.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Retry": [ 9 | { 10 | "ErrorEquals": ["States.Timeout"], 11 | "BackoffRate": -2 12 | } 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/backoff-with-invalid-jitter-on-retry.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Retry": [ 9 | { 10 | "ErrorEquals": ["States.Timeout"], 11 | "MaxDelaySeconds": 5, 12 | "JitterStrategy": "PARTIAL" 13 | } 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/backoff-with-jitter-on-retry.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Retry": [ 9 | { 10 | "ErrorEquals": ["States.Timeout"], 11 | "MaxDelaySeconds": 5, 12 | "JitterStrategy": "FULL" 13 | } 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/choice-nested-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "A workflow definition with Comment fields at multiple levels in a Choice state", 3 | "StartAt": "Choice", 4 | "States": { 5 | "Choice": { 6 | "Type": "Choice", 7 | "Choices": [ 8 | { 9 | "Variable": "$.a", 10 | "IsPresent": true, 11 | "Next": "Choice-Nested", 12 | "Comment": "my comment" 13 | } 14 | ], 15 | "Default": "Decision3" 16 | }, 17 | "Choice-Nested": { 18 | "Type": "Choice", 19 | "Choices": [ 20 | { 21 | "And": [ 22 | { 23 | "Variable": "$.b", 24 | "IsPresent": true, 25 | "Comment": "nested comment" 26 | }, 27 | { 28 | "Variable": "$.c", 29 | "IsPresent": true 30 | }, 31 | { 32 | "Or": [ 33 | { 34 | "Variable": "$.d", 35 | "IsPresent": true, 36 | "Comment": "nested comment" 37 | }, 38 | { 39 | "Variable": "$.e", 40 | "IsPresent": true 41 | } 42 | ], 43 | "Comment": "nested comment in Or" 44 | } 45 | ], 46 | "Comment": "nested choice", 47 | "Next": "Decision1" 48 | } 49 | ], 50 | "Default": "Decision2" 51 | }, 52 | "Decision1": { 53 | "Type": "Pass", 54 | "End": true 55 | }, 56 | "Decision2": { 57 | "Type": "Pass", 58 | "End": true 59 | }, 60 | "Decision3": { 61 | "Type": "Pass", 62 | "End": true 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/choice-rule-with-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Choice", 6 | "Choices": [ 7 | { 8 | "Variable": "$.foo", 9 | "StringEquals": "x", 10 | "Next": "x", 11 | "Comment": "foo" 12 | } 13 | ] 14 | }, 15 | "x": { 16 | "Type": "Succeed" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/choice-with-context-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "ChoiceState", 3 | "States": { 4 | "ChoiceState": { 5 | "Type": "Choice", 6 | "Choices": [ 7 | { 8 | "Variable": "$$.value", 9 | "IsNull": false, 10 | "Next": "SucceedState" 11 | } 12 | ], 13 | "Default": "FailState" 14 | }, 15 | "SucceedState": { 16 | "Type": "Succeed" 17 | }, 18 | "FailState": { 19 | "Type": "Fail" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /test/choice-with-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Choice", 6 | "Choices": [ 7 | { 8 | "Variable": "$.foo", 9 | "StringEquals": "x", 10 | "Next": "x" 11 | } 12 | ], 13 | "Parameters": "I'm a parameter!" 14 | }, 15 | "x": { 16 | "Type": "Succeed" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/choice-with-resultpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Choice", 6 | "Choices": [ 7 | { 8 | "Variable": "$.foo", 9 | "StringEquals": "x", 10 | "Next": "x" 11 | } 12 | ], 13 | "ResultPath": "$.foo" 14 | }, 15 | "x": { 16 | "Type": "Succeed" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/choice-with-resultselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Choice", 6 | "Choices": [ 7 | { 8 | "Variable": "$.foo", 9 | "StringEquals": "x", 10 | "Next": "x" 11 | } 12 | ], 13 | "ResultSelector": { 14 | "a": "x", 15 | "b.$": "$.y", 16 | "c.$": "$$.z" 17 | } 18 | }, 19 | "x": { 20 | "Type": "Succeed" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/empty-error-equals-on-catch.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Catch": [ 9 | { 10 | "ErrorEquals": [], 11 | "Next": "x" 12 | } 13 | ] 14 | }, 15 | "x": { 16 | "Type": "Fail" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/empty-error-equals-on-retry.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Retry": [ 9 | { 10 | "ErrorEquals": [] 11 | } 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/fail-with-error-and-cause-path-using-intrinsic-functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "FailState", 3 | "States": { 4 | "FailState": { 5 | "Type": "Fail", 6 | "ErrorPath": "States.Format('this is the error: {}, and this is the cause: {}', $.Error, $.Cause)", 7 | "CausePath": "States.UUID()" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/fail-with-error-and-cause-path.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "FailState", 3 | "States": { 4 | "FailState": { 5 | "Type": "Fail", 6 | "ErrorPath": "$.Error", 7 | "CausePath": "$.Cause" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/fail-with-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Fail", 6 | "Parameters": "I'm a parameter!" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fail-with-resultpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Fail", 6 | "ResultPath": "$.foo" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fail-with-resultselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Fail", 6 | "ResultSelector": { 7 | "a": "x", 8 | "b.$": "$.y", 9 | "c.$": "$$.z" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fail-with-static-and-dynamic-cause.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "FailState", 3 | "States": { 4 | "FailState": { 5 | "Type": "Fail", 6 | "Cause": "Cause", 7 | "CausePath": "$.Cause" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/fail-with-static-and-dynamic-error.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "FailState", 3 | "States": { 4 | "FailState": { 5 | "Type": "Fail", 6 | "Error": "Error", 7 | "ErrorPath": "$.Error" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/has-dupes.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "A", 3 | "States": { 4 | "A": { 5 | "Type": "Parallel", 6 | "Branches": [ 7 | { 8 | "StartAt": "Sub1_1", 9 | "States": { 10 | "Sub1_1": { 11 | "Type": "Succeed" 12 | } 13 | } 14 | }, 15 | { 16 | "StartAt": "Sub2_1", 17 | "States": { 18 | "Sub2_1": { 19 | "Type": "Succeed" 20 | } 21 | } 22 | } 23 | ], 24 | "Next": "X" 25 | }, 26 | "X": { 27 | "Type": "Pass", 28 | "Next": "Sub2_1" 29 | }, 30 | "Sub2_1": { 31 | "Type": "Succeed" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/invalid-backoff-with-jitter-on-retry.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "End": true, 8 | "Retry": [ 9 | { 10 | "ErrorEquals": ["States.Timeout"], 11 | "MaxDelaySeconds": -1, 12 | "JitterStrategy": "FULL" 13 | } 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/invalid-function-invocation.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "abc.$": "States.Xyz($.result)" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/linked-parallel.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "A", 3 | "States": { 4 | "A": { 5 | "Type": "Parallel", 6 | "Branches": [ 7 | { 8 | "StartAt": "Sub1_1", 9 | "States": { 10 | "Sub1_1": { 11 | "Type": "Pass", 12 | "Next": "Sub2_1" 13 | } 14 | } 15 | }, 16 | { 17 | "StartAt": "Sub2_1", 18 | "States": { 19 | "Sub2_1": { 20 | "Type": "Succeed" 21 | } 22 | } 23 | } 24 | ], 25 | "Next": "X" 26 | }, 27 | "X": { 28 | "Type": "Choice", 29 | "Choices": [ 30 | {"Variable": "$.x", "NumericEquals": 1, "Next": "Sub1_1"}, 31 | {"Variable": "$.x", "NumericEquals": 1, "Next": "Z"} 32 | ], 33 | "Default": "Sub1_1" 34 | }, 35 | "Z": { 36 | "Type": "Succeed" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/map-with-itemprocessor-and-iterator.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "m", 3 | "States": { 4 | "m": { 5 | "Type": "Map", 6 | "ItemProcessor": { 7 | "ProcessorConfig": { 8 | "Mode": "INLINE" 9 | }, 10 | "StartAt": "y", 11 | "States": { 12 | "y": { 13 | "Type": "Pass", 14 | "End": true 15 | } 16 | } 17 | }, 18 | "Iterator": { 19 | "StartAt": "x", 20 | "States": { 21 | "x": { 22 | "Type": "Pass", 23 | "End": true 24 | } 25 | } 26 | }, 27 | "End": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/map-with-itemprocessor.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "m", 3 | "States": { 4 | "m": { 5 | "Type": "Map", 6 | "ItemProcessor": { 7 | "ProcessorConfig": { 8 | "Mode": "INLINE" 9 | }, 10 | "StartAt": "x", 11 | "States": { 12 | "x": { 13 | "Type": "Pass", 14 | "End": true 15 | } 16 | } 17 | }, 18 | "End": true 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/map-with-itemspath-context-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "m", 3 | "States": { 4 | "m": { 5 | "Type": "Map", 6 | "ItemsPath": "$$.mapItems", 7 | "Iterator": { 8 | "StartAt": "x", 9 | "States": { 10 | "x": { 11 | "Type": "Pass", 12 | "End": true 13 | } 14 | } 15 | }, 16 | "Parameters": "$.foo", 17 | "End": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/map-with-null-itemspath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "m", 3 | "States": { 4 | "m": { 5 | "Type": "Map", 6 | "ItemsPath": null, 7 | "Iterator": { 8 | "StartAt": "x", 9 | "States": { 10 | "x": { 11 | "Type": "Pass", 12 | "End": true 13 | } 14 | } 15 | }, 16 | "Parameters": "$.foo", 17 | "End": true 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/map-with-parameters-and-itemselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "m", 3 | "States": { 4 | "m": { 5 | "Type": "Map", 6 | "Parameters": { 7 | "foo.$": "$.bar" 8 | }, 9 | "ItemSelector": { 10 | "foo.$": "$.bar" 11 | }, 12 | "ItemProcessor": { 13 | "ProcessorConfig": { 14 | "Mode": "INLINE" 15 | }, 16 | "StartAt": "x", 17 | "States": { 18 | "x": { 19 | "Type": "Pass", 20 | "End": true 21 | } 22 | } 23 | }, 24 | "End": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/map-with-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "m", 3 | "States": { 4 | "m": { 5 | "Type": "Map", 6 | "Iterator": { 7 | "StartAt": "x", 8 | "States": { 9 | "x": { 10 | "Type": "Pass", 11 | "End": true 12 | } 13 | } 14 | }, 15 | "Parameters": "$.foo", 16 | "End": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/map-with-resultselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "m", 3 | "States": { 4 | "m": { 5 | "Type": "Map", 6 | "Iterator": { 7 | "StartAt": "x", 8 | "States": { 9 | "x": { 10 | "Type": "Pass", 11 | "End": true 12 | } 13 | } 14 | }, 15 | "Parameters": "$.foo", 16 | "ResultSelector": { 17 | "a": "x", 18 | "b.$": "$.y", 19 | "c.$": "$$.z" 20 | }, 21 | "End": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/minimal-fail-state.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "A Task", 3 | "States": { 4 | "A Task": { 5 | "Type": "Task", 6 | "Resource": "arn:aws:states:us-west-2:42:activity:a-task", 7 | "Next": "Final" 8 | }, 9 | "Final": { 10 | "Type": "Fail" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/no-terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "FirstState", 3 | "States": { 4 | "FirstState": { 5 | "Type": "Task", 6 | "Resource": "arn:aws:lambda:us-east-1:ACCOUNT_ID:function:FUNCTION_NAME", 7 | "Next": "ChoiceState" 8 | }, 9 | 10 | "ChoiceState": { 11 | "Type" : "Choice", 12 | "Choices": [ 13 | { 14 | "Variable": "$.foo", 15 | "NumericEquals": 3, 16 | "Next": "FirstMatchState" 17 | }, 18 | { 19 | "Variable": "$.foo", 20 | "NumericEquals": 4, 21 | "Next": "SecondMatchState" 22 | } 23 | ], 24 | "Default": "NextState" 25 | }, 26 | 27 | "FirstMatchState": { 28 | "Type" : "Task", 29 | "Resource": "arn:aws:lambda:us-east-1::function:OnFirstMatch", 30 | "Next": "NextState" 31 | }, 32 | 33 | "SecondMatchState": { 34 | "Type" : "Task", 35 | "Resource": "arn:aws:lambda:us-east-1::function:OnSecondMatch", 36 | "Next": "NextState" 37 | }, 38 | 39 | "NextState": { 40 | "Type": "Task", 41 | "Resource": "arn:aws:lambda:us-east-1:ACCOUNT_ID:function:FUNCTION_NAME", 42 | "Next": "SecondMatchState" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/parallel-with-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Parallel", 6 | "Branches": [ 7 | { 8 | "StartAt": "x", 9 | "States": { 10 | "x": { 11 | "Type": "Succeed" 12 | } 13 | } 14 | } 15 | ], 16 | "Parameters": "$.foo", 17 | "End": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/parallel-with-resultpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Parallel", 6 | "Branches": [ 7 | { 8 | "StartAt": "x", 9 | "States": { 10 | "x": { 11 | "Type": "Succeed" 12 | } 13 | } 14 | } 15 | ], 16 | "ResultPath": "$.foo", 17 | "End": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/parallel-with-resultselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Parallel", 6 | "Branches": [ 7 | { 8 | "StartAt": "x", 9 | "States": { 10 | "x": { 11 | "Type": "Succeed" 12 | } 13 | } 14 | } 15 | ], 16 | "Parameters": "$.foo", 17 | "ResultSelector": { 18 | "a": "x", 19 | "b.$": "$.y", 20 | "c.$": "$$.z" 21 | }, 22 | "End": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/parameter-path-problems.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "FNORD", 3 | "States": { 4 | "FNORD": { 5 | "Type": "Pass", 6 | "Result": { "foo": 1 }, 7 | "Parameters": { 8 | "f0": 1, 9 | "good1.$": "$.g1", 10 | "bad1.$": 23, 11 | "bad2.$": "23", 12 | "f3": { 13 | "bad3.$": [ 1, 2, 3 ], 14 | "good2.$": "$.b[23]", 15 | "f5": [ 16 | "f6", 17 | { 18 | "f7": { 19 | "bad4.$": { "f9": 1 }, 20 | "good3.$": "$.x.y.z[2]" 21 | } 22 | } 23 | ] 24 | }, 25 | "good4.$": "$$", 26 | "good5.$": "$$.g5", 27 | "bad5.$": "$$23" 28 | }, 29 | "End": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/pass-with-intrinsic-function-inputpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Pass", 6 | "Result": { "foo": 1 , "bar": 2}, 7 | "InputPath": "States.Format('{} xyz {}', 1, 'def')", 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/pass-with-io-path-context-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Pass", 6 | "Result": { "foo": 1 , "bar": 2}, 7 | "InputPath": "$$.path.inputToProcess", 8 | "OutputPath": "$$.outputToPass", 9 | "End": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/pass-with-null-inputpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Pass", 6 | "Result": { "foo": 1 , "bar": 2}, 7 | "InputPath": null, 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/pass-with-null-outputpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Pass", 6 | "Result": { "foo": 1 , "bar": 2}, 7 | "OutputPath": null, 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/pass-with-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Pass", 6 | "Result": { "foo": 1 }, 7 | "Parameters": "I'm a parameter!", 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/pass-with-resultpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Pass", 6 | "Result": { "foo": 1 }, 7 | "ResultPath": "$.foo", 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/pass-with-resultselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Pass", 6 | "Result": { "foo": 1 }, 7 | "ResultSelector": { 8 | "a": "$.foo", 9 | "b.$": "$.y", 10 | "c.$": "$$.z" 11 | }, 12 | "End": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/states-array-intrinsic-functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "array.$": "States.Array('Foo', $.xyz, 'Bar', 2.3)", 9 | "partition.$": "States.ArrayPartition($.inputArray, 4)", 10 | "contains.$": "States.ArrayContains($.inputArray, $.lookingFor)", 11 | "range.$": "States.ArrayRange(1, 9, 2)", 12 | "item.$": "States.ArrayGetItem($.inputArray, $.index)", 13 | "length.$": "States.ArrayLength($.inputArray)", 14 | "unique.$": "States.ArrayUnique($.inputArray)" 15 | }, 16 | "End": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/states-array-invocation-leftpad.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "abc.$": " States.Array('Foo', $.xyz, 'Bar', 2.3)" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/states-array-invocation.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "abc.$": "States.Array('Foo', $.xyz, 'Bar', 2.3)" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/states-encoding-decoding-intrinsic-functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "base64.$": "States.Base64Encode($.input)", 9 | "data.$": "States.Base64Decode($.base64)" 10 | }, 11 | "End": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/states-format-invocation.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "Parameters": { 8 | "abc.$": "States.Format('{} xyz {}', 1, 'def')" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/states-hash-intrinsic-functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "output.$": "States.Hash($.Data, $.Algorithm)" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/states-json-intrinsic-functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "output.$": "States.JsonMerge($.json1, $.json2, false)", 9 | "json.$": "States.StringToJson($.escapedJsonString)", 10 | "string.$": "States.JsonToString($.unescapedJson)" 11 | }, 12 | "End": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/states-jsontostring-invocation.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "Parameters": { 8 | "abc.$": "States.JsonToString($.xyz)" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/states-math-intrinsic-functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "random.$": "States.MathRandom($.start, $.end)", 9 | "addition.$": "States.MathAdd($.value1, $.step)" 10 | }, 11 | "End": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/states-string-intrinsic-functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "array.$": "States.StringSplit($.inputString, $.splitter)" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/states-stringtojson-invocation.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "abc.$": "States.StringToJson($.result)" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/states-uuid-intrinsic-functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "uuid.$": "States.UUID()" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/succeed-with-comment-in-catcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "test", 3 | "StartAt": "foo", 4 | "States": { 5 | "foo": { 6 | "Type":"Task", 7 | "Resource":"arn:aws:states:::lambda:invoke", 8 | "Next": "end", 9 | "Catch": [ 10 | { 11 | "ErrorEquals": [ 12 | "States.ALL" 13 | ], 14 | "Comment": "foo", 15 | "Next": "end" 16 | } 17 | ] 18 | }, 19 | "end": { 20 | "Type": "Fail" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/succeed-with-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Succeed", 6 | "Parameters": "I'm a parameter!" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/succeed-with-resultpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Succeed", 6 | "ResultPath": "$.foo" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/succeed-with-resultselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Succeed", 6 | "ResultSelector": { 7 | "a": "x", 8 | "b.$": "$.y", 9 | "c.$": "$$.z" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/task-with-credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "Credentials": { 8 | "roleArn": "fizz:buzz" 9 | }, 10 | "End": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/task-with-dynamic-timeouts.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "TimeoutSecondsPath": "$.x", 8 | "HeartbeatSecondsPath": "$.y", 9 | "End": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/task-with-invalid-credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "Credentials": "foo:bar", 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/task-with-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "Parameters": "I'm a parameter!", 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/task-with-resultpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultPath": "$.foo", 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/task-with-resultselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "ResultSelector": { 8 | "a": "x", 9 | "b.$": "$.y", 10 | "c.$": "$$.z" 11 | }, 12 | "End": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/task-with-static-and-dynamic-heartbeat.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "HeartbeatSeconds": 30, 8 | "HeartbeatSecondsPath": "$.abc", 9 | "End": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/task-with-static-and-dynamic-timeout.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Task", 6 | "Resource": "foo:bar", 7 | "TimeoutSeconds": 30, 8 | "TimeoutSecondsPath": "$.abc", 9 | "End": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/test_retval.sh: -------------------------------------------------------------------------------- 1 | # Should be run from the root of the repository 2 | 3 | ruby -I./lib bin/statelint test/has-dupes.json > /dev/null 2>&1 4 | 5 | if [ $? -eq 0 ]; then 6 | echo "TEST FAILED: 0 returned for invalid JSON file" 7 | exit 1 8 | fi 9 | 10 | ruby -I./lib bin/statelint test/minimal-fail-state.json > /dev/null 2>&1 11 | 12 | if [ $? -ne 0 ]; then 13 | echo "TEST FAILED: 0 not returned for valid JSON file" 14 | exit 1 15 | fi 16 | 17 | echo "Return value test successful" 18 | -------------------------------------------------------------------------------- /test/wait-with-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Wait", 6 | "Seconds": 1, 7 | "Parameters": "I'm a parameter!", 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/wait-with-resultpath.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Wait", 6 | "Seconds": 1, 7 | "ResultPath": "$.foo", 8 | "End": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/wait-with-resultselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "p", 3 | "States": { 4 | "p": { 5 | "Type": "Wait", 6 | "Seconds": 1, 7 | "ResultSelector": { 8 | "a": "x", 9 | "b.$": "$.y", 10 | "c.$": "$$.z" 11 | }, 12 | "End": true 13 | } 14 | } 15 | } 16 | --------------------------------------------------------------------------------