floe 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/floe/workflow.rb CHANGED
@@ -6,6 +6,7 @@ require "json"
6
6
  module Floe
7
7
  class Workflow
8
8
  include Logging
9
+ include ValidationMixin
9
10
 
10
11
  class << self
11
12
  def load(path_or_io, context = nil, credentials = {}, name = nil)
@@ -85,61 +86,73 @@ module Floe
85
86
  end
86
87
  end
87
88
 
88
- attr_reader :context, :credentials, :payload, :states, :states_by_name, :start_at, :name, :comment
89
+ attr_reader :context, :payload, :states, :states_by_name, :start_at, :name, :comment
89
90
 
90
- def initialize(payload, context = nil, credentials = {}, name = nil)
91
+ def initialize(payload, context = nil, credentials = nil, name = nil)
91
92
  payload = JSON.parse(payload) if payload.kind_of?(String)
92
93
  credentials = JSON.parse(credentials) if credentials.kind_of?(String)
93
94
  context = Context.new(context) unless context.kind_of?(Context)
94
95
 
95
- raise Floe::InvalidWorkflowError, "Missing field \"States\"" if payload["States"].nil?
96
- raise Floe::InvalidWorkflowError, "Missing field \"StartAt\"" if payload["StartAt"].nil?
97
- raise Floe::InvalidWorkflowError, "\"StartAt\" not in the \"States\" field" unless payload["States"].key?(payload["StartAt"])
96
+ # backwards compatibility
97
+ # caller should really put credentials into context and not pass that variable
98
+ context.credentials = credentials if credentials
98
99
 
99
- @name = name
100
+ # NOTE: this is a string, and states use an array
101
+ @name = name || "State Machine"
100
102
  @payload = payload
101
103
  @context = context
102
- @credentials = credentials || {}
103
104
  @comment = payload["Comment"]
104
105
  @start_at = payload["StartAt"]
105
106
 
106
- @states = payload["States"].to_a.map { |state_name, state| State.build!(self, state_name, state) }
107
- @states_by_name = @states.each_with_object({}) { |state, result| result[state.name] = state }
107
+ # NOTE: Everywhere else we include our name (i.e.: parent name) when building the child name.
108
+ # When creating the states, we are dropping our name (i.e.: the workflow name)
109
+ @states = payload["States"].to_a.map { |state_name, state| State.build!(self, ["States", state_name], state) }
108
110
 
109
- unless context.state.key?("Name")
110
- context.state["Name"] = start_at
111
- context.state["Input"] = context.execution["Input"].dup
112
- end
113
- rescue StandardError => err
111
+ validate_workflow
112
+
113
+ @states_by_name = @states.each_with_object({}) { |state, result| result[state.short_name] = state }
114
+ rescue Floe::Error
115
+ raise
116
+ rescue => err
114
117
  raise Floe::InvalidWorkflowError, err.message
115
118
  end
116
119
 
117
120
  def run_nonblock
121
+ start_workflow
118
122
  loop while step_nonblock == 0 && !end?
119
123
  self
120
124
  end
121
125
 
126
+ # NOTE: If running manually, make sure to call start_workflow at startup
122
127
  def step_nonblock
123
128
  return Errno::EPERM if end?
124
129
 
125
- step_next
126
- current_state.run_nonblock!
130
+ result = current_state.run_nonblock!(context)
131
+ return result if result != 0
132
+
133
+ # if it completed the step
134
+ context.state_history << context.state
135
+ context.next_state ? step! : end_workflow!
136
+
137
+ result
127
138
  end
128
139
 
140
+ # if this hasn't started (and we have no current_state yet), assume it is ready
129
141
  def step_nonblock_wait(timeout: nil)
130
- current_state.wait(:timeout => timeout)
142
+ context.started? ? current_state.wait(context, :timeout => timeout) : 0
131
143
  end
132
144
 
145
+ # if this hasn't started (and we have no current_state yet), assume it is ready
133
146
  def step_nonblock_ready?
134
- current_state.ready?
147
+ !context.started? || current_state.ready?(context)
135
148
  end
136
149
 
137
150
  def waiting?
138
- current_state.waiting?
151
+ current_state.waiting?(context)
139
152
  end
140
153
 
141
154
  def wait_until
142
- current_state.wait_until
155
+ current_state.wait_until(context)
143
156
  end
144
157
 
145
158
  def status
@@ -147,21 +160,60 @@ module Floe
147
160
  end
148
161
 
149
162
  def output
150
- context.output if end?
163
+ context.json_output if end?
151
164
  end
152
165
 
153
166
  def end?
154
167
  context.ended?
155
168
  end
156
169
 
170
+ # setup a workflow
171
+ def start_workflow
172
+ return if context.state_name
173
+
174
+ context.state["Name"] = start_at
175
+ context.state["Input"] = context.execution["Input"].dup
176
+ context.state["Guid"] = SecureRandom.uuid
177
+
178
+ context.execution["Id"] = SecureRandom.uuid
179
+ context.execution["StartTime"] = Time.now.utc.iso8601
180
+
181
+ self
182
+ end
183
+
184
+ # NOTE: Expecting the context to be initialized (via start_workflow) before this
157
185
  def current_state
158
186
  @states_by_name[context.state_name]
159
187
  end
160
188
 
189
+ # backwards compatibility. Caller should access directly from context
190
+ def credentials
191
+ @context.credentials
192
+ end
193
+
161
194
  private
162
195
 
163
- def step_next
164
- context.state = {"Name" => context.next_state, "Input" => context.output} if context.next_state
196
+ def validate_workflow
197
+ missing_field_error!("States") if @states.empty?
198
+ missing_field_error!("StartAt") if @start_at.nil?
199
+ invalid_field_error!("StartAt", @start_at, "is not found in \"States\"") unless workflow_state?(@start_at, self)
200
+ end
201
+
202
+ def step!
203
+ next_state = {"Name" => context.next_state, "Guid" => SecureRandom.uuid, "PreviousStateGuid" => context.state["Guid"]}
204
+
205
+ # if rerunning due to an error (and we are using Retry)
206
+ if context.state_name == context.next_state && context.failed? && context.state.key?("Retrier")
207
+ next_state.merge!(context.state.slice("RetryCount", "Input", "Retrier"))
208
+ else
209
+ next_state["Input"] = context.output
210
+ end
211
+
212
+ context.state = next_state
213
+ end
214
+
215
+ def end_workflow!
216
+ context.execution["EndTime"] = context.state["FinishedTime"]
165
217
  end
166
218
  end
167
219
  end
data/lib/floe.rb CHANGED
@@ -7,7 +7,9 @@ require_relative "floe/logging"
7
7
 
8
8
  require_relative "floe/runner"
9
9
 
10
+ require_relative "floe/validation_mixin"
10
11
  require_relative "floe/workflow"
12
+ require_relative "floe/workflow/error_matcher_mixin"
11
13
  require_relative "floe/workflow/catcher"
12
14
  require_relative "floe/workflow/choice_rule"
13
15
  require_relative "floe/workflow/choice_rule/not"
@@ -15,6 +17,9 @@ require_relative "floe/workflow/choice_rule/or"
15
17
  require_relative "floe/workflow/choice_rule/and"
16
18
  require_relative "floe/workflow/choice_rule/data"
17
19
  require_relative "floe/workflow/context"
20
+ require_relative "floe/workflow/intrinsic_function"
21
+ require_relative "floe/workflow/intrinsic_function/parser"
22
+ require_relative "floe/workflow/intrinsic_function/transformer"
18
23
  require_relative "floe/workflow/path"
19
24
  require_relative "floe/workflow/payload_template"
20
25
  require_relative "floe/workflow/reference_path"
@@ -37,6 +42,7 @@ require "time"
37
42
  module Floe
38
43
  class Error < StandardError; end
39
44
  class InvalidWorkflowError < Error; end
45
+ class InvalidExecutionInput < Error; end
40
46
 
41
47
  def self.logger
42
48
  @logger ||= NullLogger.new
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ManageIQ Developers
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-24 00:00:00.000000000 Z
11
+ date: 2024-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn
@@ -80,20 +80,34 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: parslet
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: manageiq-style
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - ">="
88
102
  - !ruby/object:Gem::Version
89
- version: '0'
103
+ version: 1.5.2
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
- version: '0'
110
+ version: 1.5.2
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rake
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -122,20 +136,6 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: rubocop
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: simplecov
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -164,13 +164,14 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
- description: Simple Workflow Runner.
167
+ description: Floe is a runner for Amazon States Language workflows.
168
168
  email:
169
169
  executables:
170
170
  - floe
171
171
  extensions: []
172
172
  extra_rdoc_files: []
173
173
  files:
174
+ - ".codeclimate.yml"
174
175
  - ".rspec"
175
176
  - ".rubocop.yml"
176
177
  - ".rubocop_cc.yml"
@@ -178,6 +179,7 @@ files:
178
179
  - ".yamllint"
179
180
  - CHANGELOG.md
180
181
  - Gemfile
182
+ - LICENSE.txt
181
183
  - README.md
182
184
  - Rakefile
183
185
  - examples/set-credential.asl
@@ -185,6 +187,7 @@ files:
185
187
  - exe/floe
186
188
  - floe.gemspec
187
189
  - lib/floe.rb
190
+ - lib/floe/cli.rb
188
191
  - lib/floe/container_runner.rb
189
192
  - lib/floe/container_runner/docker.rb
190
193
  - lib/floe/container_runner/docker_mixin.rb
@@ -193,6 +196,7 @@ files:
193
196
  - lib/floe/logging.rb
194
197
  - lib/floe/null_logger.rb
195
198
  - lib/floe/runner.rb
199
+ - lib/floe/validation_mixin.rb
196
200
  - lib/floe/version.rb
197
201
  - lib/floe/workflow.rb
198
202
  - lib/floe/workflow/catcher.rb
@@ -202,6 +206,10 @@ files:
202
206
  - lib/floe/workflow/choice_rule/not.rb
203
207
  - lib/floe/workflow/choice_rule/or.rb
204
208
  - lib/floe/workflow/context.rb
209
+ - lib/floe/workflow/error_matcher_mixin.rb
210
+ - lib/floe/workflow/intrinsic_function.rb
211
+ - lib/floe/workflow/intrinsic_function/parser.rb
212
+ - lib/floe/workflow/intrinsic_function/transformer.rb
205
213
  - lib/floe/workflow/path.rb
206
214
  - lib/floe/workflow/payload_template.rb
207
215
  - lib/floe/workflow/reference_path.rb
@@ -220,7 +228,8 @@ files:
220
228
  - renovate.json
221
229
  - sig/floe.rbs/floe.rbs
222
230
  homepage: https://github.com/ManageIQ/floe
223
- licenses: []
231
+ licenses:
232
+ - Apache-2.0
224
233
  metadata:
225
234
  allowed_push_host: https://rubygems.org
226
235
  rubygems_mfa_required: 'true'
@@ -242,8 +251,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
251
  - !ruby/object:Gem::Version
243
252
  version: '0'
244
253
  requirements: []
245
- rubygems_version: 3.5.10
254
+ rubygems_version: 3.4.20
246
255
  signing_key:
247
256
  specification_version: 4
248
- summary: Simple Workflow Runner.
257
+ summary: Floe is a runner for Amazon States Language workflows.
249
258
  test_files: []