workflower 0.2.5 → 0.2.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18c999bb59c66918bd57ac930be03d55d79a5fc955e1a2972621aa83b2c7b654
4
- data.tar.gz: 839e96f58e78d27bf91b7ee287545ba87ed370d5969e207a31b3027f7087af79
3
+ metadata.gz: 9ec3a528c2e3ec78cd0bb386d8e6b0cccbb3e6b4420dedbcdfdb2269b99d9488
4
+ data.tar.gz: 42c9b00f69d3b5ee64e615a81451ec19284949b6eabad7db3b41341b2080431f
5
5
  SHA512:
6
- metadata.gz: aeb7a5d9bef2fd3f2bcfc4dfc597f71fdab5d71f2feb76be3d6dd0ece9297aaef584a24cb196f52bc65ef224a3a60bbf84ce7c237e3ffe8c51fdbbd3350fbca0
7
- data.tar.gz: 9cb5be4608a30639683a45a94dfaead6ce2218551bc5399b31cee01daeb860df80a4cbb8428040c5364c971a72c7c714a7f3faecb54b24d4785074ae8f838629
6
+ metadata.gz: f61b9ec6d00b77ef2c8fa7658e3e9683aa874801a911e4e3c653604e158ac97e6eee75b773f785a0ff43dfd43827213de0ee5084d2206687cecede81b0794793
7
+ data.tar.gz: a8a3a9f924057dd27cac7a2b73460bc22a9112cc8284b9cd0ca9d309aea8543c628fb89a937bb70e7fe52c3653f3329f9665857998419381be5169351586ed30
data/Gemfile.lock CHANGED
@@ -1,24 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- workflower (0.2.5)
4
+ workflower (0.2.2)
5
5
  activesupport (>= 6.0.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activesupport (7.0.4.3)
10
+ activesupport (7.0.1)
11
11
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
12
  i18n (>= 1.6, < 2)
13
13
  minitest (>= 5.1)
14
14
  tzinfo (~> 2.0)
15
15
  ast (2.4.2)
16
16
  byebug (11.1.3)
17
- concurrent-ruby (1.2.2)
17
+ concurrent-ruby (1.1.9)
18
18
  diff-lcs (1.4.4)
19
- i18n (1.12.0)
19
+ i18n (1.8.11)
20
20
  concurrent-ruby (~> 1.0)
21
- minitest (5.18.0)
21
+ minitest (5.14.2)
22
22
  parallel (1.20.1)
23
23
  parser (3.0.0.0)
24
24
  ast (~> 2.4.1)
@@ -51,7 +51,7 @@ GEM
51
51
  rubocop-ast (1.4.1)
52
52
  parser (>= 2.7.1.5)
53
53
  ruby-progressbar (1.11.0)
54
- tzinfo (2.0.6)
54
+ tzinfo (2.0.4)
55
55
  concurrent-ruby (~> 1.0)
56
56
  unicode-display_width (1.7.0)
57
57
 
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Workflower
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/workflower`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ The Workflower gem is a Ruby implementation of a workflow state-based pattern tailored for Rails applications. It is a lightweight, flexible, and extensible workflow engine that can be used to implement a wide variety of workflows. The Workflower gem provides the a simple and intuitive way for defining workflows, workflow states, transitions, events, conditions, actions, and much more.
6
4
 
7
5
  ## Installation
8
6
 
@@ -14,15 +12,730 @@ gem 'workflower'
14
12
 
15
13
  And then execute:
16
14
 
17
- $ bundle install
15
+ bundle install
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ gem install workflower
20
+
21
+ ## How it Works
22
+
23
+ The workflower gem consists of three main components:
24
+
25
+ - [Flow Class](lib/workflower/flow.rb): The Flow class is responsible for defining the workflow state machine.
26
+ - [Manager Class](lib/workflower/manager.rb): The Manager class is responsible for processing the workflow state machine.
27
+ - [Acts As Workflower Module](lib/workflower/acts_as_workflower.rb): The Acts As Workflower module is an ActiveSupport::Concern that is responsible for adding the workflow functionality to the model. This is done by defining some class and some instance methods. Under the hood both of the above classes are utilized to do all the heavy lifting.
28
+
29
+ ### Flow Class (State Machine Definition)
30
+
31
+ The Flow class is responsible for defining the workflow state machine. It is mainly responsible for handling the workflow definition files. A workflow definition file is a ruby file that defines the workflow state machine check the **[Annex: 1.0: Workflow Definition File Example](#annex-10-workflow-definition-file-example)** to see how to define a workflow definition file.
32
+
33
+ The Flow class requires each state machine to be defined as a hash, and each state machine must have the following keys:
34
+
35
+ - `state`: The state name.
36
+ - `transition_into`: The state name that this state can transition into.
37
+ - `event`: The event name that triggers the transition.
38
+ - `sequence`: The sequence number of the transition. Can be used to define the order of the FSM transitions (good for re-usability but adds complexity).
39
+ - `downgrade_sequence`: The downgrade sequence number of the transition.
40
+ - `metadata`: The metadata hash that contains the following keys:
41
+ - `roles`: An array of roles that are allowed to trigger the transition.
42
+ - `type`: The type of the transition, it can be either `update` or `create`.
43
+ - `required_parameters`: An array of required parameters that must be passed to the transition.
44
+ - Optionally, you can add any other keys to the metadata hash. for example a `send_notifications`, key that can be used to indicate whether to send notifications or not. You can also define a `permitted_parameters` key that can be used to define the permitted parameters in [JSON Schema](https://json-schema.org/) format the **[Annex 1.1: JSON Schema Example](#annex-11-json-schema-example)** to see how to define a JSON Schema and validate them.
45
+
46
+ Each state can also optionally, have the following keys can be added to the hash:
47
+
48
+ - `condition`: The name of the method that will be called to check if the transition can be triggered. The method must return true or false.
49
+ - `before_transition`: The name of the method that will be called before the transition is triggered. Please check the [Process Flow](#process-flow) section for more details on the sequence in which the methods are called.
50
+ - `after_transition`: The name of the method that will be called after the transition is triggered. Please check the [Process Flow](#process-flow) section for more details on the sequence in which the methods are called.
51
+ - `condition_type`: The type of the condition, it can be `expression`, if not specified, it will be `method`. If the condition type is `expression`, the condition will be evaluated as an expression, otherwise it will be evaluated as a method.
52
+
53
+ Typically, the workflow definition files are placed in a directory called `workflow_definitions` in the `lib` directory. However, you can place the workflow definition files anywhere you want, as long as you specify the path to the workflow definition files in the `source` option when initializing the workflower gem. We recommend defining the workflow definition files like the given example in the **[Annex 1.0: Workflow Definition File Example](#annex-10-workflow-definition-file-example)**.
54
+
55
+ ### Manager Class (State Machine Processor)
56
+
57
+ The Manager class is responsible for processing the workflow state machine. When initialized, it requires the following parameters:
58
+
59
+ - `calling_model`: The model that is calling the workflow state machine.
60
+ - `source`: The source of the workflow state machine. The source must be an object that responds to the `get_workflows` method and returns a hash of workflow state machines.
61
+
62
+ See the **[Annex 1.2: WorkflowSource Class Definition](#annex-12-workflow-source-example)** to see how to define a workflow source class.
63
+
64
+ The Manager class is responsible for the following:
65
+
66
+ - Initializing the workflow state machine.
67
+ - Processing the workflow state machine.
68
+ - Validating the workflow state machine.
69
+ - Providing the allowed events.
70
+ - Providing the allowed transitions.
71
+ - Providing the validation errors.
72
+
73
+ It also provides the following methods and accessors:
74
+
75
+ - `uninitialize`: Uninitializes the workflow state machine.
76
+ - `set_initial_state`: Sets the initial state of the workflow state machine (defaults to `saved`, but can be overridden).
77
+ - `process_transition!`: Processes the transition, please check the [Process Flow](#process-flow) section for more details on the sequence in which the methods are called.
78
+ - `allowed_events`: Returns the allowed events for the current state machine.
79
+ - `allowed_transitions`: Returns the allowed transitions from the current state machine.
80
+ - `validation_errors`: Returns the validation errors.
81
+ - `transition_possible?`: Checks if the transition is possible on the current state machine.
82
+
83
+ Please check the **[Annex 1.3: Workflowable](#annex-13-workflowable)** section for more details on how to use these methods and accessors.
84
+
85
+ <br>
86
+
87
+ #### Process Flow
88
+
89
+ The workflower's `process_transition!` method is responsible for processing the state transition. It uses the following steps to accomplish a transition:
90
+
91
+ 1. It first checks if the `condition`. If the condition is met, it proceeds with the transition. If the condition is not met, it adds an error on the field `workflow_state` with key `transition_faild` to the model.
92
+ 2. The first step in the transition process is calling the `before_transition` method. This method is provided either by explicitly defining it in the workflow definition file, or by defining it in the model. If the model, responds to a method with the name `before_<event_name>`.
93
+
94
+ ```ruby
95
+ class <ModelName> < ApplicationRecord
96
+ # ...
97
+ def before_event_name
98
+ # ...
99
+ end
100
+ # ...
101
+ end
102
+ ```
103
+
104
+ ```ruby
105
+ # ./lib/workflow_definitions/<model_name>/<role_name>.rb
106
+ # ...
107
+ {
108
+ state: '...',
109
+ transition_into: '...',
110
+ event: '...',
111
+ #...
112
+ before_transition: 'before_event_name_or_custom_method_name'
113
+ }
114
+ ```
115
+
116
+ 3. After invoking the before transition callback, the workflow fields (columns) are updated, as well as the required parameters defined in the metadata are assigned to the model. The record has not been saved yet.
117
+ 4. The next step is to call the `after_transition` method. This method is provided either by explicitly defining it in the workflow definition file, or by defining it in the model. If the model, responds to a method with the name `after_<event_name>`.
118
+
119
+ ```ruby
120
+ class <ModelName> < ApplicationRecord
121
+ # ...
122
+ def after_event_name
123
+ # ...
124
+ end
125
+ # ...
126
+ end
127
+ ```
128
+
129
+ ```ruby
130
+ # ./lib/workflow_definitions/<model_name>/<role_name>.rb
131
+ # ...
132
+ {
133
+ state: '...',
134
+ transition_into: '...',
135
+ event: '...',
136
+ #...
137
+ after_transition: 'after_event_name_or_custom_method_name'
138
+ }
139
+ ```
140
+
141
+ <br>
142
+
143
+ **IMPORTANT NOTE:**
144
+
145
+ - The `before_transition` and `after_transition` in the state definition have precedence over the `before_<event_name>` and `after_<event_name>` methods defined in the model.
146
+ - These callbacks are not transactional, so if you want to rollback the transaction, you have to wrap your controller action in a transaction block and make sure to raise an exception in the callback method if you want to rollback the transaction. Alternatively, you can take a look at [Annex 1.5: Before Save Callback Example](#annex-15-before-save-callback-example) to see how to use the `before_save` callback for a more transactional approach.
147
+
148
+ <br>
149
+
150
+ ### Acts As Workflower Module (The concern that adds the workflow functionality to the model)
151
+
152
+ The Acts As Workflower module is an ActiveSupport::Concern that is responsible for adding the workflow functionality to the model. It consists of two main parts:
153
+
154
+ - **Instance Methods**: The instance methods are responsible for initializing the workflow state machine, processing the workflow state machine, and uninitializing the workflow state machine.
155
+ - **Class Methods**: The class methods are responsible for defining the workflow state machine, and defining the workflow abilities.
156
+
157
+ #### Instance Methods
158
+
159
+ This module allows the model to initialize, process, and uninitialize the workflow state machine. It also allows the model to access the allowed events, allowed transitions, and validation errors. Under the hood, it uses the [Manager Class](#manager-class-state-machine-processor) class to do all the heavy lifting.
160
+
161
+ Here is the list of instance methods and attributes provided by this module:
162
+
163
+ - `possible_events`: Returns the possible events for the current state machine.
164
+ - `allowed_events`: Returns the allowed events for the current state machine.
165
+ - `allowed_transitions`: Returns the allowed transitions from the current state machine.
166
+ - `workflow_transition_event_name`: Returns the name of the event that triggered the transition.
167
+ - `workflow_transition_flow`: Returns the flow object that contains the transition information.
168
+ - `set_initial_state`: Sets the initial state of the workflow state machine (defaults to `saved`, but can be overridden).
169
+ - `workflower_initial_state`: Returns the initial state of the workflow state machine (defaults to `saved`, but can be overridden).
170
+ - `workflower_base`: Returns the workflow manager object.
171
+ - `source_workflow`: Returns the workflow source object.
172
+ - `workflower_initializer`: Initializes the workflow state machine.
173
+ - `workflower_uninitializer`: Uninitializes the workflow state machine.
174
+
175
+ #### Class Methods
176
+
177
+ This module allows the model to define the workflow state machine, and define the workflow abilities. Under the hood, it uses the [Flow Class](#flow-class-state-machine-definition) class to do all the heavy lifting.
178
+
179
+ Here is the list of class methods provided by this module:
180
+
181
+ - `workflower`: Defines the workflow state machine. This is a must to be invoked in order for the workflow state machine to be initialized. See the **[Annex 1.4: Workflower Initialization](#annex14-workflower-class-definition)**.
182
+ - `workflower_abilities`: Defines the workflow abilities based on the `roles` defined in the workflow definition files.
183
+
184
+ ## Annex
185
+
186
+ This section contains the annexes that are referenced in the above sections, please use them wisely to fully understand how the workflower gem works, you don't necessarily need to follow them all, but they are there to help you understand how to use the workflower gem.
187
+
188
+ ### Annex 1.0: Workflow Definition File Example
189
+
190
+ ```ruby
191
+ # ./lib/workflow_definitions/<model_name>/<role_name>.rb
192
+
193
+ module WorkflowDefinitions
194
+ module <ModelName><RoleName>
195
+ module V1
196
+ def self.own_actions(seq = 1)
197
+ [
198
+ {
199
+ state: "...",
200
+ transition_into: "...",
201
+ event: "...",
202
+ sequence: seq,
203
+ downgrade_sequence: -1,
204
+ metadata: {
205
+ roles: %w[...],
206
+ type: 'update',
207
+ required_parameters: %i[]
208
+ }
209
+ }
210
+
211
+ #...
212
+ ]
213
+ end
214
+
215
+ def self.formulate(seq = 1)
216
+ [
217
+ *own_actions(seq)
218
+ ]
219
+ end
220
+ end
221
+ end
222
+ end
223
+ ```
224
+
225
+ ### Annex 1.1: JSON Schema Example
226
+
227
+ ```ruby
228
+ # ./lib/workflow_definitions/<model_name>/<role_name>.rb
229
+ # ...
230
+ metadata: {
231
+ # ...
232
+ permitted_parameters: {
233
+ '$schema': 'http://json-schema.org/draft-07/schema#',
234
+ '$id': 'http://json-schema.org/draft-07/schema#',
235
+ type: 'object',
236
+ properties: {
237
+ workflow_comment: { type: 'string' }
238
+ },
239
+ required: %i[workflow_comment]
240
+ }
241
+ # ...
242
+ }
243
+ # ...
244
+ ```
245
+
246
+ ```ruby
247
+ # ./app/controllers/<model_name>_controller.rb
248
+ # ...
249
+ def check_required_params_in_workflow(required_parameters = {})
250
+ metadata = @resource.workflow_transition_flow&.metadata&.dig(:permitted_parameters)
251
+
252
+ return if metadata.blank?
253
+
254
+ metadata.except!(:$id, :$schema)
255
+
256
+ errors = JSON::Validator.fully_validate(metadata, required_parameters)
257
+
258
+ nil if errors.blank?
259
+ end
260
+ # ...
261
+ ```
262
+
263
+ ### Annex 1.2: Workflow Source Example
264
+
265
+ ```ruby
266
+ # ./app/models/concerns/workflows/<model_name>/workflow_source.rb
267
+
268
+ module Workflows
269
+ class WorkflowSource
270
+ Dir["#{Rails.application.root}/lib/workflow_definitions/<model_name>/*.rb"].each { |file| require file }
271
+
272
+ def initialize(_model = nil)
273
+ @workflows = {
274
+ '1': [
275
+ *WorkflowDefinitions::<ModelName><RoleName>::V1.formulate,
276
+ *WorkflowDefinitions::<ModelName><RoleName>::V1.formulate
277
+ ].flatten
278
+ }
279
+ end
280
+
281
+ def get_workflows
282
+ @workflows
283
+ end
284
+
285
+ def get_workflows_for_workflow_id(workflow_id)
286
+ get_workflows[workflow_id.to_s.to_sym]
287
+ end
288
+ end
289
+ end
290
+ ```
291
+
292
+ ### Annex 1.3: Workflowable
293
+
294
+ ```ruby
295
+ # ./app/models/concerns/workflows/<model_name>/workflowable.rb
296
+
297
+ module Workflowable
298
+ def workflow_is_accessible_roles(given_workflow = workflow_state)
299
+ source_workflow.select do |item|
300
+ item[:state] == given_workflow && item.dig(:metadata, :roles).present?
301
+ end.flat_map do |flow|
302
+ condition_flow = flow[:condition]
303
+ if condition_flow.blank?
304
+ flow.dig(:metadata, :roles)
305
+ else
306
+ condition_type = flow[:condition_type] || ''
307
+ if condition_type.present? && condition_type == 'expression'
308
+ flow.dig(:metadata, :roles) if eval(condition_flow)
309
+ elsif send(condition_flow)
310
+ flow.dig(:metadata, :roles)
311
+ end
312
+ end
313
+ end.compact.uniq
314
+ end
315
+
316
+ def reached_flow_stage_for_role?(role)
317
+ workflow_is_accessible_roles.map(&:to_sym).include?(role.to_sym)
318
+ end
319
+
320
+ def apply_transition(event, &proc)
321
+ workflower_initializer if allowed_transitions.nil?
322
+ return false unless allowed_transitions.map(&:event).include?(event)
323
+
324
+ proc.call
325
+ return false unless send("can_#{event}?")
326
+
327
+ send("#{event}!")
328
+ end
329
+
330
+ def selected_flow(event)
331
+ allowed_transitions&.select { |flow| flow.event == event }.try(:first)
332
+ end
333
+
334
+ # rubocop:disable Style/OpenStructUse
335
+ def structified_flow_metadata(event)
336
+ selected = selected_flow(event)
337
+ return [] if selected.blank? || selected.try(:metadata).blank?
338
+
339
+ OpenStruct.new(selected.metadata)
340
+ end
341
+
342
+ def applicable_transitions_as_response(&callback)
343
+ workflower_initializer
344
+
345
+ workflower_base.allowed_transitions.map do |flow|
346
+ { command: flow.event.to_sym, metadata: flow.metadata.try(:slice, :permitted_parameters) || {} }
347
+ end
348
+ .reject { |action| callback.call(action) }
349
+ end
350
+
351
+ # Utilities
352
+ def workflow_state_is?(state)
353
+ workflow_state.to_sym == state.to_sym
354
+ end
355
+
356
+ def workflow_state_is_any_of?(*states)
357
+ states.flatten.map { |item| item.to_sym if item.respond_to?(:to_sym) }.include?(workflow_state.to_sym)
358
+ end
359
+
360
+ def given_state_is_any_of?(*states, given_state:)
361
+ states.flatten.map { |item| item.to_sym if item.respond_to?(:to_sym) }.include?(given_state.to_sym)
362
+ end
363
+ end
364
+ ```
365
+
366
+ ### Annex 1.4: Workflower Class Definition
367
+
368
+ ```ruby
369
+ # ./app/models/<model_name>.rb
370
+ # ...
371
+ include Workflower::ActsAsWorkflower
372
+ # ...
373
+ workflower source: Workflows::<ModelName>::WorkflowSource,
374
+ workflower_state_column_name: 'workflow_state',
375
+ default_workflow_id: 1,
376
+ skip_setting_initial_state: true
377
+ # ...
378
+ ```
379
+
380
+ ### Annex 1.5: Before Save Callback Example
381
+
382
+ ```ruby
383
+ # ./app/models/<model_name>.rb
384
+ class <ModelName> < ApplicationRecord
385
+ before_save :send_notification_for_application_approval, if: proc { |obj| obj.workflow_state_changed? && obj.workflow_transition_flow.try(:event) == 'approve_on_application_by_auditor' }
386
+ before_save :set_applicant_status, if: proc { |obj| obj.workflow_transition_flow.try(:metadata).try(:[], :applicant_status).present? }
387
+
388
+
389
+ def send_notification_for_application_approval
390
+ # ...
391
+ end
392
+
393
+ def set_applicant_status
394
+ # ...
395
+ end
396
+ end
397
+ ```
398
+
399
+ <br>
400
+
401
+ ## Workflower Gem Usage with an Example
402
+
403
+ To fully understand how the workflower gem works, we will use a hypothetical example.
404
+
405
+ ---
406
+ **Scenario**
407
+
408
+ To apply for a competition, an applicant creates an application. The applicant then submits the application for review by an auditor. The auditor reviews the application and decides whether to accept, reject, or ask for changes to the application. If any changes are requested, the applicant makes the changes and resubmits the application for review by the auditor. The auditor reviews the application and decides whether to accept, ask for more changes or reject the application.
409
+
410
+ ---
411
+
412
+ ---
413
+ **Finite State Machine Diagram:**
414
+
415
+ <img src="https://github.com/muhammadnawzad/workflower/assets/58137134/f948fc88-7e2d-4e6f-a8ff-5be4af165010" alt="Workflow Definition Example" width="500"/>
416
+
417
+ ---
418
+
419
+ **Directory Structure Example:**
420
+
421
+ ```
422
+ lib
423
+ └── workflow_definitions
424
+ ├──── application
425
+ │ ├──── applicant.rb
426
+ │ ├──── auditor.rb
427
+ ```
428
+
429
+ #### Workflow Definition Filesadsa Example
430
+
431
+ ```ruby
432
+ # ./lib/workflow_definitions/applications/applicant.rb
433
+
434
+ module WorkflowDefinitions
435
+ module ApplicationApplicant
436
+ module V1
437
+ def self.own_actions(seq = 1)
438
+ [
439
+ {
440
+ state: 'saved',
441
+ transition_into: 'submitted_for_review_by_applicant_to_auditor',
442
+ event: 'submit_for_review_by_applicant_to_auditor',
443
+
444
+ # Can optionally add a condition (method name in the model that return true/false):
445
+ # condition: 'can_submit_for_review_by_applicant_to_auditor?',
446
+
447
+ # Can optionally add an after_transition method (method name in the model):
448
+ # after_transition: 'process_submission',
449
+
450
+ # Can optionally add a before_transition method (method name in the model):
451
+ # before_transition: 'process_submission',
452
+ sequence: seq,
453
+ downgrade_sequence: -1,
454
+ metadata: {
455
+ roles: %w[applicant],
456
+ type: 'update',
457
+ permitted_parameters: {
458
+ '$schema': 'http://json-schema.org/draft-07/schema#',
459
+ '$id': 'http://json-schema.org/draft-07/schema#',
460
+ type: 'object',
461
+ properties: {
462
+ workflow_comment: { type: 'string' }
463
+ },
464
+ required: %i[workflow_comment]
465
+ },
466
+ required_parameters: %i[workflow_comment]
467
+ }
468
+ },
469
+ {
470
+ state: 'sent_for_correction_by_auditor_to_applicant',
471
+ transition_into: 'submitted_after_correction_by_applicant_to_auditor',
472
+ event: 'submit_after_correction_by_applicant_to_auditor',
473
+ sequence: seq,
474
+ downgrade_sequence: -1,
475
+ metadata: {
476
+ roles: %w[guest],
477
+ type: 'update',
478
+ required_parameters: %i[]
479
+ }
480
+ }
481
+ ]
482
+ end
483
+
484
+ def self.formulate(seq = 1)
485
+ [
486
+ *own_actions(seq)
487
+ ]
488
+ end
489
+ end
490
+ end
491
+ end
492
+ ```
493
+
494
+ ```ruby
495
+ # ./lib/workflow_definitions/applications/auditor.rb
496
+
497
+ module WorkflowDefinitions
498
+ module ApplicationAuditor
499
+ module V1
500
+ def self.own_actions(seq = 1)
501
+ [
502
+ {
503
+ state: 'submitted_for_review_by_applicant_to_auditor',
504
+ transition_into: 'sent_for_correction_by_auditor_to_applicant',
505
+ event: 'send_for_correction_by_auditor_to_applicant',
506
+ sequence: seq,
507
+ downgrade_sequence: -1,
508
+ metadata: {
509
+ roles: %w[auditor],
510
+ type: 'update',
511
+ required_parameters: %i[]
512
+ }
513
+ },
514
+ {
515
+ state: 'submitted_for_review_by_applicant_to_auditor',
516
+ transition_into: 'rejected_by_auditor',
517
+ event: 'reject_application_by_auditor',
518
+ sequence: seq,
519
+ downgrade_sequence: -1,
520
+ metadata: {
521
+ roles: %w[auditor],
522
+ type: 'update',
523
+ required_parameters: %i[]
524
+ }
525
+ },
526
+ {
527
+ state: 'submitted_for_review_by_applicant_to_auditor',
528
+ transition_into: 'approved_by_auditor',
529
+ event: 'approve_on_application_by_auditor',
530
+ sequence: seq,
531
+ downgrade_sequence: -1,
532
+ metadata: {
533
+ roles: %w[auditor],
534
+ type: 'update',
535
+ required_parameters: %i[]
536
+ }
537
+ },
538
+ {
539
+ state: 'submitted_after_correction_by_applicant_to_auditor',
540
+ transition_into: 'rejected_by_auditor',
541
+ event: 'reject_application_by_auditor',
542
+ sequence: seq,
543
+ downgrade_sequence: -1,
544
+ metadata: {
545
+ roles: %w[auditor],
546
+ type: 'update',
547
+ required_parameters: %i[]
548
+ }
549
+ },
550
+ {
551
+ state: 'submitted_after_correction_by_applicant_to_auditor',
552
+ transition_into: 'approved_by_auditor',
553
+ event: 'approve_on_application_by_auditor',
554
+ sequence: seq,
555
+ downgrade_sequence: -1,
556
+ metadata: {
557
+ roles: %w[auditor],
558
+ type: 'update',
559
+ required_parameters: %i[]
560
+ }
561
+ },
562
+ {
563
+ state: 'submitted_after_correction_by_applicant_to_auditor',
564
+ transition_into: 'sent_for_correction_by_auditor_to_applicant',
565
+ event: 'send_for_correction_by_auditor_to_applicant',
566
+ sequence: seq,
567
+ downgrade_sequence: -1,
568
+ metadata: {
569
+ roles: %w[auditor],
570
+ type: 'update',
571
+ required_parameters: %i[]
572
+ }
573
+ }
574
+ ]
575
+ end
576
+
577
+ def self.formulate(seq = 1)
578
+ [
579
+ *own_actions(seq)
580
+ ]
581
+ end
582
+ end
583
+ end
584
+ end
585
+ ```
586
+
587
+ **Workflow Initialization:**
588
+
589
+ In order for the workflow to be initialized, the following steps must be taken:
590
+
591
+ 1. Add the following columns to your model's table (e.g. Application model):
592
+
593
+ ```ruby
594
+ # ...
595
+ t.string :workflow_state, null: false, default: 'saved', index: true
596
+ t.integer :sequence, null: false, default: 1
597
+ t.integer :workflow_id, null: false, default: 1
598
+ # ...
599
+ ```
600
+
601
+ 2. Add the following lines to your model:
602
+
603
+ ```ruby
604
+ class Application < ApplicationRecord
605
+ include Workflower::ActsAsWorkflower # Example is given below
606
+
607
+ # Workflower
608
+ workflower source: Workflows::Applications::WorkflowSource,
609
+ workflower_state_column_name: 'workflow_state',
610
+ default_workflow_id: 1,
611
+ skip_setting_initial_state: true
612
+ end
613
+ ```
614
+
615
+ 3. Let's define `Workflows::WorkflowSource` module.
616
+
617
+ ```ruby
618
+ # ./app/models/concerns/workflows/applications/workflow_source.rb
619
+
620
+ module Workflows
621
+ class WorkflowSource
622
+ Dir["#{Rails.application.root}/lib/workflow_definitions/application/*.rb"].each { |file| require file }
623
+
624
+ def initialize(_model = nil)
625
+ @workflows = {
626
+ '1': [
627
+ *WorkflowDefinitions::ApplicationApplicant::V1.formulate,
628
+ *WorkflowDefinitions::ApplicationAuditor::V1.formulate
629
+ ].flatten
630
+ }
631
+ end
632
+
633
+ def get_workflows
634
+ @workflows
635
+ end
636
+
637
+ def get_workflows_for_workflow_id(workflow_id)
638
+ get_workflows[workflow_id.to_s.to_sym]
639
+ end
640
+ end
641
+ end
642
+ ```
643
+
644
+ 4. Add the [Workflowable](#annex-13-workflowable) concern module to your model:
645
+
646
+ ```ruby
647
+ class Application < ApplicationRecord
648
+ # ...
649
+ include Workflowable # Example is given below
650
+
651
+ #...
652
+ end
653
+ ```
654
+
655
+ 5. Add [CanCanCan](https://github.com/CanCanCommunity/cancancan) abilities, or your choice of authorizations, in our case, add the following lines to your `Ability` class:
656
+
657
+ ```ruby
658
+ class Ability
659
+ include CanCan::Ability
660
+
661
+ def initialize(user)
662
+ # ...
663
+ if user.role_is_a?('applicant')
664
+ can %i[submit_for_review_by_applicant_to_auditor], Application
665
+ can %i[submit_after_correction_by_applicant_to_auditor], Application
666
+ elsif user.role_is_a?('auditor')
667
+ can %i[send_for_correction_by_auditor_to_applicant], Application
668
+ can %i[reject_application_by_auditor], Application
669
+ can %i[approve_on_application_by_auditor], Application
670
+ end
671
+
672
+ # Or you can use the following dynamic approach for a more advanced approach:
673
+ # (Applicant.workflower_abilities.try(:with_indifferent_access).try(:[], :guest) || []).each do |action|
674
+ # can action.to_sym, Applicant do |instance|
675
+ # instance.reached_flow_stage_for_role?(:guest) && instance.creator_id == user.id
676
+ # end
677
+ # end
678
+
679
+ # ...
680
+ end
681
+ end
682
+ ```
683
+
684
+ 6. Handle the workflow transition in your controller (the following example is for the sake of simplicity, you can use a service object or any other approach):
685
+
686
+ ```ruby
687
+ class ApplicationsController < ApplicationController
688
+ # ...
689
+ def transit
690
+ @resource = Application.where(id: params[:id])# .include(eager_loads)
691
+
692
+ raise ActiveRecord::RecordNotFound if @resource.blank?
693
+ @resource = @resource.first
694
+
695
+ event = params[:event]
696
+ @resource.workflower_initializer
697
+ selected_flow_metadata = @resource.structified_flow_metadata(event)
698
+ @resource.assign_attributes(transition_extra_params(selected_flow_metadata)) if %w[update amend].include?(selected_flow_metadata.try(:type))
699
+
700
+ transition_check = @resource.apply_transition(event) do
701
+ authorize! event.to_sym, @resource
702
+ end
703
+
704
+ if transition_check
705
+ # Now save
706
+ if @resource.save
707
+ @resource.workflower_uninitializer
708
+ render jsonapi: @resource and return
709
+ else
710
+ # render errors and return if @resource.errors.any?
711
+ end
712
+ end
713
+
714
+ # render errors if reached here
715
+ end
716
+
717
+ def transition_extra_params(given_flow)
718
+ return flow_extra_parameters(given_flow) if given_flow.present? && given_flow.try(:permitted_parameters).present?
719
+ end
720
+ # ...
721
+ end
722
+ ```
18
723
 
19
- Or install it yourself as:
724
+ 7. Add the following lines to your `routes.rb` file:
20
725
 
21
- $ gem install workflower
726
+ ```ruby
727
+ # ...
728
+ resources :applications do
729
+ member do
730
+ post '/transit/:event', to: "applications#transit"
731
+ end
732
+ end
733
+ # ...
734
+ ```
22
735
 
23
- ## Usage
736
+ <br>
24
737
 
25
- TODO: Write usage instructions here
738
+ The above steps are the minimum required steps to initialize the workflow state machine. However, you can add more steps to customize the workflow state machine to your needs. There is a lot more room for customization.
26
739
 
27
740
  ## Development
28
741
 
@@ -32,7 +745,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
745
 
33
746
  ## Contributing
34
747
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/workflower. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/workflower/blob/master/CODE_OF_CONDUCT.md).
748
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/[USERNAME]/workflower>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/workflower/blob/master/CODE_OF_CONDUCT.md).
36
749
 
37
750
  ## License
38
751
 
@@ -42,7 +42,7 @@ module Workflower
42
42
  end
43
43
 
44
44
  def condition_is_met?(calling_model)
45
- if @condition_type == "expression"
45
+ if @condition_type == "expression" && @condition.present?
46
46
 
47
47
  evaluation_phrase = @condition.split(" ").map do |item|
48
48
  if ["||", "&&", "(", ")", "=="].include?(item)
@@ -37,7 +37,6 @@ module Workflower
37
37
  end
38
38
 
39
39
  def possible_transitions
40
- # @transitions.where(state: @current_state).where("sequence = :seq OR sequence = :seq_plus", seq: @current_sequence, seq_plus: @current_sequence + 1).order("sequence ASC") || []
41
40
  @transitions.select do |item|
42
41
  item[:state] == @current_state && (item[:sequence] == @current_sequence || item[:sequence] == @current_sequence + 1)
43
42
  end
@@ -61,7 +60,11 @@ module Workflower
61
60
  @calling_model.assign_attributes flow.updateable_attributes(@calling_model)
62
61
  flow.call_after_transition(@calling_model)
63
62
  true
64
- rescue Exception
63
+ rescue Exception => e
64
+ # if the log level is set to debug, we want to log the error
65
+ logger = Workflower.configuration.logger
66
+ logger.debug("Error during transition: #{e.message}") if logger.present?
67
+
65
68
  @calling_model.errors.add(@calling_model.workflower_state_column_name, :transition_faild)
66
69
  false
67
70
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Workflower
4
- VERSION = "0.2.5"
4
+ VERSION = "0.2.7"
5
5
  end
data/lib/workflower.rb CHANGED
@@ -3,7 +3,25 @@
3
3
  require_relative "workflower/version"
4
4
  require "workflower/manager"
5
5
  require "workflower/acts_as_workflower"
6
+
6
7
  module Workflower
7
8
  class Error < StandardError; end
8
- # Your code goes here...
9
+
10
+ class << self
11
+ def configuration
12
+ @configuration ||= Configuration.new
13
+ end
14
+
15
+ def configure
16
+ yield(configuration)
17
+ end
18
+ end
19
+
20
+ class Configuration
21
+ attr_accessor :logger
22
+
23
+ def initialize
24
+ @logger = nil
25
+ end
26
+ end
9
27
  end
data/workflower.gemspec CHANGED
@@ -5,7 +5,7 @@ require_relative "lib/workflower/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "workflower"
7
7
  spec.version = Workflower::VERSION
8
- spec.authors = ["Brusk Awat"]
8
+ spec.authors = ["Brusk Hamarash"]
9
9
  spec.email = ["broosk.edogawa@gmail.com"]
10
10
 
11
11
  spec.summary = "A state-machine library that handles state management"
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
16
16
 
17
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
17
+ spec.metadata["allowed_push_host"] = 'https://rubygems.org'
18
18
 
19
19
  spec.metadata["homepage_uri"] = spec.homepage
20
20
  spec.metadata["source_code_uri"] = "https://github.com/broosk1993/workflower"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workflower
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
- - Brusk Awat
8
- autorequire:
7
+ - Brusk Hamarash
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-14 00:00:00.000000000 Z
11
+ date: 2025-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -73,7 +73,7 @@ metadata:
73
73
  homepage_uri: https://github.com/ditkrg/workflower
74
74
  source_code_uri: https://github.com/broosk1993/workflower
75
75
  changelog_uri: https://github.com/broosk1993/workflower/blob/main/CHANGELOG.md
76
- post_install_message:
76
+ post_install_message:
77
77
  rdoc_options: []
78
78
  require_paths:
79
79
  - lib
@@ -88,8 +88,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  requirements: []
91
- rubygems_version: 3.3.7
92
- signing_key:
91
+ rubygems_version: 3.5.22
92
+ signing_key:
93
93
  specification_version: 4
94
94
  summary: A state-machine library that handles state management
95
95
  test_files: []