ruby-sfn-local 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1c56a390adcd8cefff1fa95e14db7b966e40058f81fe7d775edf31256d7eaf76
4
+ data.tar.gz: 7da9325fb01e571bf5769e6d890547c91f684742cd872d43decbd0b6bd798f1c
5
+ SHA512:
6
+ metadata.gz: e88364b74f3ed18ba23d4aab7b1e7df243a56893693af3c30531ef04c9cbc0e6f91657eb59f291eeef6e131ed3b6e55a3720cf7ce9dedb68096cce2e26c1ec1b
7
+ data.tar.gz: 3091356de9b879a01978babcd45ca8939f4d3b8f4e28a84e9e1c5f7dc1df4d2c8662867cdebaf677553adbfeee8401824d5ee0ddb61ba00b99455aabbd0dbade
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Sfn
6
+ class AwsCli
7
+ class ExecutionError < StandardError; end
8
+
9
+ def self.run(mod, command, params, key = nil, debug_info = '')
10
+ cmd = "#{Sfn.configuration.aws_command || 'aws'} #{mod} #{command} \
11
+ --endpoint #{Sfn.configuration.aws_endpoint} \
12
+ #{params.map do |k, v|
13
+ "--#{k}=#{v}"
14
+ end.join(' ')} \
15
+ --no-cli-pager"
16
+
17
+ stdout, stderr, _status = Open3.capture3(cmd)
18
+ raise raise ExecutionError, "#{stderr.strip}\n#{debug_info}" unless stderr.strip.empty?
19
+
20
+ stdout = `#{cmd}`
21
+ unless key.nil?
22
+ data = JSON.parse(stdout)
23
+ return data[key].strip if data.key?(key)
24
+ end
25
+
26
+ stdout
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Sfn
6
+ class Collection
7
+ include Singleton
8
+
9
+ attr_reader :all
10
+
11
+ def initialize
12
+ response = AwsCli.run('stepfunctions', 'list-state-machines', {})
13
+ parsed_response = JSON.parse(response)
14
+ @all = parsed_response['stateMachines'] || []
15
+ end
16
+
17
+ def add(state_machine)
18
+ @all.push(state_machine.slice('stateMachineArn', 'name'))
19
+ end
20
+
21
+ def delete_by_arn(state_machine_arn)
22
+ @all.delete_if { |sf| sf['stateMachineArn'] == state_machine_arn }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+ module Sfn
5
+ def self.configuration
6
+ @configuration ||= OpenStruct.new
7
+ end
8
+
9
+ def self.configure
10
+ yield(configuration)
11
+ end
12
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sfn
4
+ class Execution
5
+ attr_accessor :uuid, :state_machine, :test_case, :arn, :output, :profile
6
+
7
+ def self.call(state_machine, test_case, mock_data, input)
8
+ new(state_machine, test_case).exec(mock_data, input)
9
+ end
10
+
11
+ def initialize(state_machine, test_case)
12
+ self.uuid = SecureRandom.uuid
13
+ self.state_machine = state_machine
14
+ self.test_case = test_case.camelize
15
+ end
16
+
17
+ def exec(mock_data, input)
18
+ MockData.write_context(state_machine.name, test_case, mock_data)
19
+ self.arn = AwsCli.run('stepfunctions', 'start-execution',
20
+ { name: uuid,
21
+ 'state-machine': "#{state_machine.arn}##{test_case}",
22
+ input: "'#{input.to_json}'" },
23
+ 'executionArn')
24
+ self.output, self.profile = ExecutionLog.parse(arn)
25
+ self
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sfn
4
+ class ExecutionError < RuntimeError
5
+ attr_accessor :code, :events
6
+
7
+ def initialize(msg, code, events)
8
+ self.code = code
9
+ self.events = JSON.parse(events)
10
+ super(msg)
11
+ end
12
+ end
13
+
14
+ class ExecutionLog
15
+ attr_accessor :event
16
+
17
+ EVENTS = %w[stateEnteredEventDetails stateExitedEventDetails executionSucceededEventDetails
18
+ executionFailedEventDetails].freeze
19
+ def self.parse(execution_arn)
20
+ profile = {}
21
+ output = nil
22
+ error = nil
23
+ events_json = AwsCli.run('stepfunctions', 'get-execution-history',
24
+ { 'execution-arn': execution_arn.to_s, query: "'events[?#{EVENTS.join(' || ')}]'" })
25
+
26
+ JSON.parse(events_json).each do |event|
27
+ parsed_event = new(event)
28
+
29
+ output ||= parsed_event.output
30
+ error ||= parsed_event.error(events_json)
31
+ state_name = parsed_event.state_name
32
+
33
+ next if state_name.nil?
34
+
35
+ profile[state_name] ||= { input: [], output: [] }
36
+ profile[state_name][:input] << parsed_event.profile[:input] unless parsed_event.profile[:input].nil?
37
+ profile[state_name][:output] << parsed_event.profile[:output] unless parsed_event.profile[:output].nil?
38
+ end
39
+ [output, profile]
40
+ end
41
+
42
+ def initialize(event)
43
+ self.event = event
44
+ end
45
+
46
+ def state_name
47
+ event.dig('stateEnteredEventDetails', 'name') || event.dig('stateExitedEventDetails', 'name')
48
+ end
49
+
50
+ def output
51
+ try_parse(event.dig('executionSucceededEventDetails', 'output'))
52
+ end
53
+
54
+ def error(events_json = '{}')
55
+ return if event['executionFailedEventDetails'].nil?
56
+
57
+ raise ExecutionError.new(event['executionFailedEventDetails']['cause'],
58
+ event['executionFailedEventDetails']['error'],
59
+ events_json)
60
+ end
61
+
62
+ def profile
63
+ {
64
+ input: try_parse(event.dig('stateEnteredEventDetails', 'input')),
65
+ output: try_parse(event.dig('stateExitedEventDetails', 'output'))
66
+ }.compact
67
+ end
68
+
69
+ private
70
+
71
+ def try_parse(json_string)
72
+ JSON.parse(json_string)
73
+ rescue StandardError
74
+ nil
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+ require 'openssl'
5
+
6
+ module Sfn
7
+ module MockData
8
+ def self.write_context(state_machine_name, context, mock_data = {})
9
+ data = {
10
+ 'StateMachines' => {
11
+ state_machine_name.to_s => {
12
+ 'TestCases' => {
13
+ context.to_s => { 'foo' => 'bar' }
14
+ }
15
+ }
16
+ },
17
+ 'MockedResponses' => { 'bar' => {} }
18
+ }
19
+
20
+ mock_data.each do |step, response|
21
+ uuid = OpenSSL::Digest::SHA512.digest({ step: step }.merge(response).to_json).camelize
22
+ data['StateMachines'][state_machine_name.to_s]['TestCases'][context.to_s][step.to_s] = uuid
23
+ data['MockedResponses'][uuid] = response
24
+ end
25
+ File.write(Sfn.configuration.mock_file_path, JSON.pretty_generate(data))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sfn
4
+ module MockMacros
5
+ module ApiGateway
6
+ def self.response(data)
7
+ data = [data] if data.is_a?(Hash)
8
+ data.map! do |val|
9
+ if val.key?(:error)
10
+ { Throw: { Error: val[:error], Cause: val[:cause] } }
11
+ else
12
+ val[:response] ||= val[:payload]
13
+ val[:response] ||= val[:output]
14
+ { Return: { Headers: val[:headers], ResponseBody: val[:response], StatusCode: val[:status],
15
+ StatusText: 'OK' } }
16
+ end
17
+ end
18
+ out = {}
19
+ data.each_with_index do |val, idx|
20
+ out[idx.to_s] = val
21
+ end
22
+ out
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sfn
4
+ module MockMacros
5
+ module Lambda
6
+ def self.response(data)
7
+ data = [data] if data.is_a?(Hash)
8
+ data.map! do |val|
9
+ if val.key?(:error)
10
+ { Throw: { Error: val[:error], Cause: val[:cause] } }
11
+ else
12
+ val[:payload] ||= val[:response]
13
+ val[:payload] ||= val[:output]
14
+ { Return: { Payload: val[:payload], StatusCode: 200 } }
15
+ end
16
+ end
17
+ out = {}
18
+ data.each_with_index do |val, idx|
19
+ out[idx.to_s] = val
20
+ end
21
+ out
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sfn
4
+ module MockMacros
5
+ module Sns
6
+ def self.response(data)
7
+ data = [data] if data.is_a?(Hash)
8
+ data.map! do |val|
9
+ if val.key?(:error)
10
+ { Throw: { Error: val[:error], Cause: val[:cause] } }
11
+ else
12
+ val[:uuid] ||= SecureRandom.uuid
13
+ val[:sequence] ||= 10_000_000_000_000_003_000
14
+ { Return: { MessageId: val[:uuid], SequenceNumber: val[:sequence] } }
15
+ end
16
+ end
17
+ out = {}
18
+ data.each_with_index do |val, idx|
19
+ out[idx.to_s] = val
20
+ end
21
+ out
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sfn
4
+ module MockMacros
5
+ module StepFunction
6
+ def self.response(data)
7
+ data = [data] if data.is_a?(Hash)
8
+ data.map! do |val|
9
+ if val.key?(:error)
10
+ { Throw: { Error: val[:error], Cause: val[:cause] } }
11
+ else
12
+ val[:output] ||= val[:payload]
13
+ val[:output] ||= val[:response]
14
+ { Return: { Output: val[:output].to_json, Status: 'SUCCEDED' } }
15
+ end
16
+ end
17
+ out = {}
18
+ data.each_with_index do |val, idx|
19
+ out[idx.to_s] = val
20
+ end
21
+ out
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sfn/mock_macros/lambda'
4
+ require 'sfn/mock_macros/step_function'
5
+ require 'sfn/mock_macros/api_gateway'
6
+ require 'sfn/mock_macros/sns'
7
+
8
+ module Sfn
9
+ module MockMacros
10
+ include Lambda
11
+ include StepFunction
12
+ include ApiGateway
13
+ include Sns
14
+
15
+ def self.gateway_response(data)
16
+ ApiGateway.response(data)
17
+ end
18
+
19
+ def self.lambda_response(data)
20
+ Lambda.response(data)
21
+ end
22
+
23
+ def self.sns_response(data)
24
+ Sns.response(data)
25
+ end
26
+
27
+ def self.step_function_response(data)
28
+ StepFunction.response(data)
29
+ end
30
+
31
+ def self.gateway_payload(data)
32
+ warn '[DEPRECATION] `gateway_payload` is deprecated. Please use `gateway_response` instead.'
33
+ ApiGateway.response(data)
34
+ end
35
+
36
+ def self.lambda_payload(data)
37
+ warn '[DEPRECATION] `lambda_payload` is deprecated. Please use `lambda_response` instead.'
38
+ Lambda.response(data)
39
+ end
40
+
41
+ def self.sns_payload(data)
42
+ warn '[DEPRECATION] `sns_payload` is deprecated. Please use `sns_response` instead.'
43
+ Sns.response(data)
44
+ end
45
+
46
+ def self.step_function_payload(data)
47
+ warn '[DEPRECATION] `step_function_payload` is deprecated. Please use `step_function_response` instead.'
48
+ StepFunction.response(data)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+ require 'openssl'
5
+
6
+ module Sfn
7
+ class DefinitionError < RuntimeError; end
8
+
9
+ class StateMachine
10
+ ROLE = 'arn:aws:iam::123456789012:role/DummyRole'
11
+
12
+ attr_accessor :name, :definition, :arn, :executions, :execution_arn
13
+
14
+ def self.all
15
+ Collection.instance.all.map { |sf| new(sf['name'], sf['stateMachineArn']) }
16
+ end
17
+
18
+ def self.destroy_all
19
+ all.each(&:destroy)
20
+ end
21
+
22
+ def self.find_by_name(name)
23
+ all.find { |sf| sf.name == name }
24
+ end
25
+
26
+ def self.find_by_arn(arn)
27
+ all.find { |sf| sf.arn == arn }
28
+ end
29
+
30
+ def initialize(name, arn = nil)
31
+ self.name = name
32
+ self.arn = arn || self.class.find_by_name(name)&.arn || create_state_machine
33
+ self.executions = {}
34
+ end
35
+
36
+ def destroy
37
+ AwsCli.run('stepfunctions', 'delete-state-machine',
38
+ { 'state-machine-arn': arn })
39
+ Collection.instance.delete_by_arn(arn)
40
+ end
41
+
42
+ def run(mock_data = {}, input = {}, test_name = nil)
43
+ test_name ||= OpenSSL::Digest::SHA512.digest(mock_data.merge(input).to_json)
44
+ executions[test_name] ||= Execution.call(self, test_name, mock_data, input)
45
+ executions[test_name]
46
+ end
47
+
48
+ def to_hash
49
+ { 'stateMachineArn' => arn, 'name' => name }
50
+ end
51
+
52
+ private
53
+
54
+ def create_state_machine
55
+ self.arn = AwsCli.run('stepfunctions', 'create-state-machine',
56
+ { definition: load_definition(name), name: name, 'role-arn': ROLE }, 'stateMachineArn')
57
+ raise Sf::DefinitionError if arn.empty?
58
+
59
+ Collection.instance.add(to_hash)
60
+ arn
61
+ end
62
+
63
+ def load_definition(_name)
64
+ local_definition_path = Tempfile.new(['name', '.json']).path
65
+ remote_definition_path = "#{Sfn.configuration.definition_path}/#{name}.json"
66
+
67
+ definition = File.read(remote_definition_path)
68
+ local_definition = definition.gsub(/"MaxConcurrency": [0-9]+/, '"MaxConcurrency": 1')
69
+
70
+ File.open(local_definition_path, 'w') { |file| file.puts local_definition }
71
+ "file://#{local_definition_path}"
72
+ end
73
+ end
74
+ end
data/lib/sfn.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'sfn/configuration'
6
+ require 'sfn/aws_cli'
7
+ require 'sfn/collection'
8
+ require 'sfn/state_machine'
9
+ require 'sfn/mock_data'
10
+ require 'sfn/mock_macros'
11
+ require 'sfn/execution'
12
+ require 'sfn/execution_log'
13
+ require 'string'
data/lib/string.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ def camelize
5
+ split(/[^a-z0-9]/i).collect(&:capitalize).join
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-sfn-local
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Gianni Mazza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-05-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake-release
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.28'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.28'
55
+ description: A convenient gem to test step function locally
56
+ email: gianni.mazza81rg@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/sfn.rb
62
+ - lib/sfn/aws_cli.rb
63
+ - lib/sfn/collection.rb
64
+ - lib/sfn/configuration.rb
65
+ - lib/sfn/execution.rb
66
+ - lib/sfn/execution_log.rb
67
+ - lib/sfn/mock_data.rb
68
+ - lib/sfn/mock_macros.rb
69
+ - lib/sfn/mock_macros/api_gateway.rb
70
+ - lib/sfn/mock_macros/lambda.rb
71
+ - lib/sfn/mock_macros/sns.rb
72
+ - lib/sfn/mock_macros/step_function.rb
73
+ - lib/sfn/state_machine.rb
74
+ - lib/string.rb
75
+ homepage: https://github.com/redvex/ruby-sfn-local
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ rubygems_mfa_required: 'true'
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.7.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.1.2
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Step Functions local ruby wrapper!
99
+ test_files: []