better_service 1.0.1 → 1.1.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.
- checksums.yaml +4 -4
- data/LICENSE +13 -0
- data/README.md +3 -2
- data/Rakefile +201 -1
- data/lib/better_service/version.rb +1 -1
- data/lib/better_service/workflows/base.rb +1 -0
- data/lib/better_service/workflows/branch.rb +133 -0
- data/lib/better_service/workflows/branch_dsl.rb +151 -0
- data/lib/better_service/workflows/branch_group.rb +139 -0
- data/lib/better_service/workflows/dsl.rb +46 -0
- data/lib/better_service/workflows/execution.rb +35 -9
- data/lib/better_service/workflows/result_builder.rb +26 -17
- data/lib/better_service.rb +3 -0
- data/lib/generators/workflowable/templates/workflow.rb.tt +22 -0
- metadata +7 -4
- data/MIT-LICENSE +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 622ac1a705ab117672d0e9c4896e65b66585c747023d1e00b34824e9c35606a0
|
|
4
|
+
data.tar.gz: f4c6f5b6ae7baaa3f61eeb123d4a2a493a7c8acc2ff835c6891396ed543efa1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4f06ae88607c9d4b6eec865811abbba0f80403914fbcac556ad3949a395ff0b43e16e6663c652959cc950fcb89d64be427eb6e8c20b19ed0aa6b1771012e7b0
|
|
7
|
+
data.tar.gz: d1a69946471e0e2f5bd7067889c4f675d0ea94a198d69f9e877283fa5166664f49283a45fd52f282288b690670a6d095f562935f3503c86dce81633d03858e74
|
data/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
2
|
+
Version 2, December 2004
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2025 alessiobussolari <alessio@cosmic.tech>
|
|
5
|
+
|
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
|
7
|
+
copies of this license document, and changing it is allowed as long
|
|
8
|
+
as the name is changed.
|
|
9
|
+
|
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
12
|
+
|
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
### Clean, powerful Service Objects for Rails
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/rb/better_service)
|
|
8
|
-
[](http://www.wtfpl.net/about/)
|
|
9
9
|
|
|
10
10
|
[Features](#-features) • [Installation](#-installation) • [Quick Start](#-quick-start) • [Documentation](#-documentation) • [Usage](#-usage) • [Error Handling](#%EF%B8%8F-error-handling) • [Examples](#-examples)
|
|
11
11
|
|
|
@@ -28,6 +28,7 @@ BetterService is a comprehensive Service Objects framework for Rails that brings
|
|
|
28
28
|
- 🎨 **Presenter System**: Optional data transformation layer with `BetterService::Presenter` base class
|
|
29
29
|
- 📊 **Metadata Tracking**: Automatic action metadata in all service responses
|
|
30
30
|
- 🔗 **Workflow Composition**: Chain multiple services into pipelines with conditional steps, rollback support, and lifecycle hooks
|
|
31
|
+
- 🌲 **Conditional Branching** (v1.1.0+): Multi-path workflow execution with `branch`/`on`/`otherwise` DSL for clean conditional logic
|
|
31
32
|
- 🏗️ **Powerful Generators**: 10 generators for rapid scaffolding (scaffold, CRUD services, action, workflow, locale, presenter)
|
|
32
33
|
- 📦 **6 Service Types**: Specialized services for different use cases
|
|
33
34
|
- 🎨 **DSL-Based**: Clean, expressive DSL with `search_with`, `process_with`, `authorize_with`, etc.
|
|
@@ -1545,7 +1546,7 @@ See [Configuration Guide](docs/start/configuration.md) for more details.
|
|
|
1545
1546
|
|
|
1546
1547
|
## 📄 License
|
|
1547
1548
|
|
|
1548
|
-
The gem is available as open source under the terms of the [
|
|
1549
|
+
The gem is available as open source under the terms of the [WTFPL License](http://www.wtfpl.net/about/).
|
|
1549
1550
|
|
|
1550
1551
|
---
|
|
1551
1552
|
|
data/Rakefile
CHANGED
|
@@ -2,8 +2,198 @@ require "bundler/setup"
|
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rake/testtask"
|
|
5
|
+
require "fileutils"
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
# Track files created during test setup
|
|
8
|
+
CREATED_FILES_MARKER = ".test_created_files"
|
|
9
|
+
PRODUCT_SERVICES_DIR = "test/dummy/app/services/product"
|
|
10
|
+
|
|
11
|
+
# Service file templates
|
|
12
|
+
SERVICE_TEMPLATES = {
|
|
13
|
+
"create_service.rb" => <<~RUBY,
|
|
14
|
+
# frozen_string_literal: true
|
|
15
|
+
|
|
16
|
+
class Product::CreateService < BetterService::Services::CreateService
|
|
17
|
+
# Schema for validating params
|
|
18
|
+
schema do
|
|
19
|
+
required(:name).filled(:string)
|
|
20
|
+
required(:price).filled(:decimal, gt?: 0)
|
|
21
|
+
optional(:published).filled(:bool)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Phase 1: Search - Prepare dependencies (optional)
|
|
25
|
+
search_with do
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Phase 2: Process - Create the resource
|
|
30
|
+
process_with do |data|
|
|
31
|
+
product = user.products.create!(params)
|
|
32
|
+
{ resource: product }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Phase 4: Respond - Format response (optional override)
|
|
36
|
+
respond_with do |data|
|
|
37
|
+
success_result("Product created successfully", data)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
RUBY
|
|
41
|
+
"index_service.rb" => <<~RUBY,
|
|
42
|
+
# frozen_string_literal: true
|
|
43
|
+
|
|
44
|
+
class Product::IndexService < BetterService::Services::IndexService
|
|
45
|
+
# Schema for validating params
|
|
46
|
+
schema do
|
|
47
|
+
optional(:page).filled(:integer, gteq?: 1)
|
|
48
|
+
optional(:per_page).filled(:integer, gteq?: 1, lteq?: 100)
|
|
49
|
+
optional(:search).maybe(:string)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Phase 1: Search - Load raw data
|
|
53
|
+
search_with do
|
|
54
|
+
products = user.products
|
|
55
|
+
products = products.where("name LIKE ?", "%\#{params[:search]}%") if params[:search].present?
|
|
56
|
+
|
|
57
|
+
{ items: products.to_a }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Phase 2: Process - Transform and aggregate data
|
|
61
|
+
process_with do |data|
|
|
62
|
+
{
|
|
63
|
+
items: data[:items],
|
|
64
|
+
metadata: {
|
|
65
|
+
stats: {
|
|
66
|
+
total: data[:items].count
|
|
67
|
+
},
|
|
68
|
+
pagination: {
|
|
69
|
+
page: params[:page] || 1,
|
|
70
|
+
per_page: params[:per_page] || 25
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Phase 4: Respond - Format response (optional override)
|
|
77
|
+
respond_with do |data|
|
|
78
|
+
success_result("Products loaded successfully", data)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
RUBY
|
|
82
|
+
"show_service.rb" => <<~RUBY,
|
|
83
|
+
# frozen_string_literal: true
|
|
84
|
+
|
|
85
|
+
class Product::ShowService < BetterService::Services::ShowService
|
|
86
|
+
# Schema for validating params
|
|
87
|
+
schema do
|
|
88
|
+
required(:id).filled
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Phase 1: Search - Load the resource
|
|
92
|
+
search_with do
|
|
93
|
+
{ resource: user.products.find(params[:id]) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Phase 4: Respond - Format response (optional override)
|
|
97
|
+
respond_with do |data|
|
|
98
|
+
success_result("Product loaded successfully", data)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
RUBY
|
|
102
|
+
"update_service.rb" => <<~RUBY,
|
|
103
|
+
# frozen_string_literal: true
|
|
104
|
+
|
|
105
|
+
class Product::UpdateService < BetterService::Services::UpdateService
|
|
106
|
+
# Schema for validating params
|
|
107
|
+
schema do
|
|
108
|
+
required(:id).filled
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Phase 1: Search - Load the resource
|
|
112
|
+
search_with do
|
|
113
|
+
{ resource: user.products.find(params[:id]) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Phase 2: Process - Update the resource
|
|
117
|
+
process_with do |data|
|
|
118
|
+
product = data[:resource]
|
|
119
|
+
product.update!(params.except(:id))
|
|
120
|
+
{ resource: product }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Phase 4: Respond - Format response (optional override)
|
|
124
|
+
respond_with do |data|
|
|
125
|
+
success_result("Product updated successfully", data)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
RUBY
|
|
129
|
+
"destroy_service.rb" => <<~RUBY
|
|
130
|
+
# frozen_string_literal: true
|
|
131
|
+
|
|
132
|
+
class Product::DestroyService < BetterService::Services::DestroyService
|
|
133
|
+
# Schema for validating params
|
|
134
|
+
schema do
|
|
135
|
+
required(:id).filled
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Phase 1: Search - Load the resource
|
|
139
|
+
search_with do
|
|
140
|
+
{ resource: user.products.find(params[:id]) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Phase 2: Process - Delete the resource
|
|
144
|
+
process_with do |data|
|
|
145
|
+
product = data[:resource]
|
|
146
|
+
product.destroy!
|
|
147
|
+
{ resource: product }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Phase 4: Respond - Format response (optional override)
|
|
151
|
+
respond_with do |data|
|
|
152
|
+
success_result("Product deleted successfully", data)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
RUBY
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
namespace :test do
|
|
159
|
+
desc "Setup test environment - create missing Product service files"
|
|
160
|
+
task :setup do
|
|
161
|
+
created_files = []
|
|
162
|
+
|
|
163
|
+
SERVICE_TEMPLATES.each do |filename, content|
|
|
164
|
+
filepath = File.join(PRODUCT_SERVICES_DIR, filename)
|
|
165
|
+
|
|
166
|
+
unless File.exist?(filepath)
|
|
167
|
+
puts "Creating temporary test file: #{filepath}"
|
|
168
|
+
File.write(filepath, content)
|
|
169
|
+
created_files << filepath
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Save list of created files
|
|
174
|
+
File.write(CREATED_FILES_MARKER, created_files.join("\n")) if created_files.any?
|
|
175
|
+
puts "Test setup complete (#{created_files.size} files created)" if created_files.any?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
desc "Cleanup test environment - remove temporary Product service files"
|
|
179
|
+
task :cleanup do
|
|
180
|
+
if File.exist?(CREATED_FILES_MARKER)
|
|
181
|
+
created_files = File.read(CREATED_FILES_MARKER).split("\n")
|
|
182
|
+
|
|
183
|
+
created_files.each do |filepath|
|
|
184
|
+
if File.exist?(filepath)
|
|
185
|
+
puts "Removing temporary test file: #{filepath}"
|
|
186
|
+
File.delete(filepath)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
File.delete(CREATED_FILES_MARKER)
|
|
191
|
+
puts "Test cleanup complete (#{created_files.size} files removed)" if created_files.any?
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
Rake::TestTask.new(:test_only) do |t|
|
|
7
197
|
t.libs << "test"
|
|
8
198
|
t.test_files = FileList["test/**/*_test.rb"].exclude(
|
|
9
199
|
"test/dummy/**/*",
|
|
@@ -12,4 +202,14 @@ Rake::TestTask.new(:test) do |t|
|
|
|
12
202
|
t.verbose = false
|
|
13
203
|
end
|
|
14
204
|
|
|
205
|
+
# Main test task with automatic setup and cleanup
|
|
206
|
+
task :test do
|
|
207
|
+
begin
|
|
208
|
+
Rake::Task["test:setup"].invoke
|
|
209
|
+
Rake::Task["test_only"].invoke
|
|
210
|
+
ensure
|
|
211
|
+
Rake::Task["test:cleanup"].invoke
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
15
215
|
task default: :test
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Workflows
|
|
5
|
+
# Represents a single conditional branch within a workflow
|
|
6
|
+
#
|
|
7
|
+
# A Branch contains:
|
|
8
|
+
# - A condition (Proc) that determines if the branch should execute
|
|
9
|
+
# - An array of steps to execute if the condition is true
|
|
10
|
+
# - An optional name for identification
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# branch = Branch.new(
|
|
14
|
+
# condition: ->(ctx) { ctx.user.premium? },
|
|
15
|
+
# name: :premium_path
|
|
16
|
+
# )
|
|
17
|
+
# branch.add_step(step1)
|
|
18
|
+
# branch.add_step(step2)
|
|
19
|
+
#
|
|
20
|
+
# if branch.matches?(context)
|
|
21
|
+
# branch.execute(context, user, params)
|
|
22
|
+
# end
|
|
23
|
+
class Branch
|
|
24
|
+
attr_reader :condition, :steps, :name
|
|
25
|
+
|
|
26
|
+
# Creates a new Branch
|
|
27
|
+
#
|
|
28
|
+
# @param condition [Proc, nil] The condition to evaluate (nil for default/otherwise branch)
|
|
29
|
+
# @param name [Symbol, nil] Optional name for the branch
|
|
30
|
+
def initialize(condition: nil, name: nil)
|
|
31
|
+
@condition = condition
|
|
32
|
+
@steps = []
|
|
33
|
+
@name = name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Checks if this branch's condition matches the given context
|
|
37
|
+
#
|
|
38
|
+
# @param context [Workflowable::Context] The workflow context
|
|
39
|
+
# @return [Boolean] true if condition matches or is nil (default branch)
|
|
40
|
+
def matches?(context)
|
|
41
|
+
return true if condition.nil? # Default branch always matches
|
|
42
|
+
|
|
43
|
+
if condition.is_a?(Proc)
|
|
44
|
+
context.instance_exec(context, &condition)
|
|
45
|
+
else
|
|
46
|
+
condition.call(context)
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Rails.logger.error "Branch condition evaluation failed: #{e.message}"
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Adds a step to this branch
|
|
54
|
+
#
|
|
55
|
+
# @param step [Workflowable::Step] The step to add
|
|
56
|
+
# @return [Array] The updated steps array
|
|
57
|
+
def add_step(step)
|
|
58
|
+
@steps << step
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Executes all steps in this branch
|
|
62
|
+
#
|
|
63
|
+
# @param context [Workflowable::Context] The workflow context
|
|
64
|
+
# @param user [Object] The current user
|
|
65
|
+
# @param params [Hash] The workflow parameters
|
|
66
|
+
# @param branch_decisions [Array, nil] Array to track branch decisions for nested branches
|
|
67
|
+
# @return [Array<Workflowable::Step>] Array of successfully executed steps
|
|
68
|
+
# @raise [Errors::Workflowable::Runtime::StepExecutionError] If a required step fails
|
|
69
|
+
def execute(context, user, params, branch_decisions = nil)
|
|
70
|
+
executed_steps = []
|
|
71
|
+
|
|
72
|
+
@steps.each do |step_or_branch_group|
|
|
73
|
+
# Handle nested BranchGroup
|
|
74
|
+
if step_or_branch_group.is_a?(BranchGroup)
|
|
75
|
+
branch_result = step_or_branch_group.call(context, user, params)
|
|
76
|
+
|
|
77
|
+
# Add executed steps from nested branch
|
|
78
|
+
if branch_result[:executed_steps]
|
|
79
|
+
executed_steps.concat(branch_result[:executed_steps])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Track nested branch decisions
|
|
83
|
+
if branch_decisions && branch_result[:branch_decisions]
|
|
84
|
+
branch_decisions.concat(branch_result[:branch_decisions])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Handle regular Step
|
|
91
|
+
result = step_or_branch_group.call(context, user, params)
|
|
92
|
+
|
|
93
|
+
# Skip if step was skipped
|
|
94
|
+
next if result[:skipped]
|
|
95
|
+
|
|
96
|
+
# Handle step failure
|
|
97
|
+
if result[:success] == false && !result[:optional_failure]
|
|
98
|
+
raise Errors::Workflowable::Runtime::StepExecutionError.new(
|
|
99
|
+
"Step #{step_or_branch_group.name} failed in branch",
|
|
100
|
+
code: ErrorCodes::STEP_EXECUTION_FAILED,
|
|
101
|
+
context: {
|
|
102
|
+
step: step_or_branch_group.name,
|
|
103
|
+
branch: @name,
|
|
104
|
+
errors: result[:errors]
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Track successfully executed steps
|
|
110
|
+
executed_steps << step_or_branch_group unless result[:optional_failure]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
executed_steps
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns whether this is a default branch (no condition)
|
|
117
|
+
#
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def default?
|
|
120
|
+
condition.nil?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns a string representation of this branch
|
|
124
|
+
#
|
|
125
|
+
# @return [String]
|
|
126
|
+
def inspect
|
|
127
|
+
"#<BetterService::Workflows::Branch name=#{@name.inspect} " \
|
|
128
|
+
"condition=#{@condition.present? ? 'present' : 'nil'} " \
|
|
129
|
+
"steps=#{@steps.count}>"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Workflows
|
|
5
|
+
# DSL context for defining conditional branches
|
|
6
|
+
#
|
|
7
|
+
# This class provides the DSL methods available inside a `branch do...end` block:
|
|
8
|
+
# - `on(condition, &block)` - Defines a conditional branch
|
|
9
|
+
# - `otherwise(&block)` - Defines the default branch
|
|
10
|
+
# - `step(...)` - Adds a step to the current branch
|
|
11
|
+
# - `branch(&block)` - Allows nested branch groups
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# branch do
|
|
15
|
+
# on ->(ctx) { ctx.user.premium? } do
|
|
16
|
+
# step :premium_action, with: PremiumService
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# on ->(ctx) { ctx.user.basic? } do
|
|
20
|
+
# step :basic_action, with: BasicService
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# otherwise do
|
|
24
|
+
# step :default_action, with: DefaultService
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
class BranchDSL
|
|
28
|
+
attr_reader :branch_group
|
|
29
|
+
|
|
30
|
+
# Creates a new BranchDSL context
|
|
31
|
+
#
|
|
32
|
+
# @param branch_group [BranchGroup] The branch group being configured
|
|
33
|
+
def initialize(branch_group)
|
|
34
|
+
@branch_group = branch_group
|
|
35
|
+
@current_branch = nil
|
|
36
|
+
@branch_index = 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Defines a conditional branch
|
|
40
|
+
#
|
|
41
|
+
# @param condition [Proc] The condition to evaluate (receives context)
|
|
42
|
+
# @param block [Proc] The block defining steps for this branch
|
|
43
|
+
# @return [void]
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# on ->(ctx) { ctx.user.premium? } do
|
|
47
|
+
# step :send_premium_email, with: Email::PremiumService
|
|
48
|
+
# end
|
|
49
|
+
def on(condition, &block)
|
|
50
|
+
raise ArgumentError, "Condition must be a Proc" unless condition.is_a?(Proc)
|
|
51
|
+
raise ArgumentError, "Block required for 'on'" unless block_given?
|
|
52
|
+
|
|
53
|
+
@branch_index += 1
|
|
54
|
+
branch_name = :"on_#{@branch_index}"
|
|
55
|
+
|
|
56
|
+
@current_branch = @branch_group.add_branch(
|
|
57
|
+
condition: condition,
|
|
58
|
+
name: branch_name
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Evaluate the block in this DSL context
|
|
62
|
+
instance_eval(&block)
|
|
63
|
+
|
|
64
|
+
@current_branch = nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Defines the default branch (executed when no condition matches)
|
|
68
|
+
#
|
|
69
|
+
# @param block [Proc] The block defining steps for the default branch
|
|
70
|
+
# @return [void]
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# otherwise do
|
|
74
|
+
# step :default_action, with: DefaultService
|
|
75
|
+
# end
|
|
76
|
+
def otherwise(&block)
|
|
77
|
+
raise ArgumentError, "Block required for 'otherwise'" unless block_given?
|
|
78
|
+
raise ArgumentError, "Default branch already defined" if @branch_group.default_branch
|
|
79
|
+
|
|
80
|
+
@current_branch = @branch_group.set_default
|
|
81
|
+
|
|
82
|
+
# Evaluate the block in this DSL context
|
|
83
|
+
instance_eval(&block)
|
|
84
|
+
|
|
85
|
+
@current_branch = nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Adds a step to the current branch
|
|
89
|
+
#
|
|
90
|
+
# This method has the same signature as the workflow-level `step` method.
|
|
91
|
+
#
|
|
92
|
+
# @param name [Symbol] The step name
|
|
93
|
+
# @param with [Class] The service class to execute
|
|
94
|
+
# @param input [Proc, nil] Optional input mapper
|
|
95
|
+
# @param optional [Boolean] Whether the step is optional
|
|
96
|
+
# @param if [Proc, nil] Optional condition for step execution
|
|
97
|
+
# @param rollback [Proc, nil] Optional rollback handler
|
|
98
|
+
# @return [void]
|
|
99
|
+
def step(name, with:, input: nil, optional: false, **options)
|
|
100
|
+
raise "step must be called within an 'on' or 'otherwise' block" unless @current_branch
|
|
101
|
+
|
|
102
|
+
# Handle Ruby keyword 'if' gracefully
|
|
103
|
+
condition = options[:if] || options.dig(:binding)&.local_variable_get(:if) rescue nil
|
|
104
|
+
|
|
105
|
+
step_obj = Workflowable::Step.new(
|
|
106
|
+
name: name,
|
|
107
|
+
service_class: with,
|
|
108
|
+
input: input,
|
|
109
|
+
optional: optional,
|
|
110
|
+
condition: condition,
|
|
111
|
+
rollback: options[:rollback]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@current_branch.add_step(step_obj)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Allows nested branch groups within branches
|
|
118
|
+
#
|
|
119
|
+
# @param block [Proc] The block defining the nested branch group
|
|
120
|
+
# @return [void]
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# on ->(ctx) { ctx.type == 'contract' } do
|
|
124
|
+
# step :validate_contract, with: ValidateService
|
|
125
|
+
#
|
|
126
|
+
# branch do
|
|
127
|
+
# on ->(ctx) { ctx.value > 10000 } do
|
|
128
|
+
# step :ceo_approval, with: CEOApprovalService
|
|
129
|
+
# end
|
|
130
|
+
# otherwise do
|
|
131
|
+
# step :manager_approval, with: ManagerApprovalService
|
|
132
|
+
# end
|
|
133
|
+
# end
|
|
134
|
+
# end
|
|
135
|
+
def branch(&block)
|
|
136
|
+
raise "branch must be called within an 'on' or 'otherwise' block" unless @current_branch
|
|
137
|
+
raise ArgumentError, "Block required for 'branch'" unless block_given?
|
|
138
|
+
|
|
139
|
+
# Create nested branch group
|
|
140
|
+
nested_group = BranchGroup.new(name: :"nested_branch_#{@branch_index}")
|
|
141
|
+
nested_dsl = BranchDSL.new(nested_group)
|
|
142
|
+
|
|
143
|
+
# Evaluate the block in the nested DSL context
|
|
144
|
+
nested_dsl.instance_eval(&block)
|
|
145
|
+
|
|
146
|
+
# Add the branch group as a "step" (it responds to call like a step)
|
|
147
|
+
@current_branch.add_step(nested_group)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Workflows
|
|
5
|
+
# Represents a group of conditional branches in a workflow
|
|
6
|
+
#
|
|
7
|
+
# A BranchGroup is created by the `branch do...end` DSL block and contains:
|
|
8
|
+
# - Multiple conditional branches (from `on` blocks)
|
|
9
|
+
# - An optional default branch (from `otherwise` block)
|
|
10
|
+
#
|
|
11
|
+
# When executed, it evaluates conditions in order and executes the first matching branch.
|
|
12
|
+
# If no branch matches and there's no default, it raises an error.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# branch_group = BranchGroup.new(name: :payment_routing)
|
|
16
|
+
# branch_group.add_branch(condition: ->(ctx) { ctx.type == 'card' }) do
|
|
17
|
+
# # steps...
|
|
18
|
+
# end
|
|
19
|
+
# branch_group.set_default do
|
|
20
|
+
# # default steps...
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# result = branch_group.call(context, user, params)
|
|
24
|
+
class BranchGroup
|
|
25
|
+
attr_reader :branches, :default_branch, :name
|
|
26
|
+
|
|
27
|
+
# Creates a new BranchGroup
|
|
28
|
+
#
|
|
29
|
+
# @param name [Symbol, nil] Optional name for the branch group
|
|
30
|
+
def initialize(name: nil)
|
|
31
|
+
@branches = []
|
|
32
|
+
@default_branch = nil
|
|
33
|
+
@name = name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Adds a conditional branch to this group
|
|
37
|
+
#
|
|
38
|
+
# @param condition [Proc] The condition to evaluate
|
|
39
|
+
# @param name [Symbol, nil] Optional name for the branch
|
|
40
|
+
# @return [Branch] The created branch
|
|
41
|
+
def add_branch(condition:, name: nil)
|
|
42
|
+
branch = Branch.new(condition: condition, name: name)
|
|
43
|
+
@branches << branch
|
|
44
|
+
branch
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Sets the default branch (otherwise)
|
|
48
|
+
#
|
|
49
|
+
# @param name [Symbol, nil] Optional name for the default branch
|
|
50
|
+
# @return [Branch] The created default branch
|
|
51
|
+
def set_default(name: nil)
|
|
52
|
+
@default_branch = Branch.new(condition: nil, name: name || :otherwise)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Selects the first branch that matches the context
|
|
56
|
+
#
|
|
57
|
+
# @param context [Workflowable::Context] The workflow context
|
|
58
|
+
# @return [Branch, nil] The matching branch or nil if none match
|
|
59
|
+
def select_branch(context)
|
|
60
|
+
# Try conditional branches first (in order)
|
|
61
|
+
@branches.each do |branch|
|
|
62
|
+
return branch if branch.matches?(context)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Fall back to default branch if present
|
|
66
|
+
@default_branch
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Executes the appropriate branch based on context
|
|
70
|
+
#
|
|
71
|
+
# This is called during workflow execution. It:
|
|
72
|
+
# 1. Selects the matching branch
|
|
73
|
+
# 2. Executes all steps in that branch
|
|
74
|
+
# 3. Returns metadata about the execution
|
|
75
|
+
#
|
|
76
|
+
# @param context [Workflowable::Context] The workflow context
|
|
77
|
+
# @param user [Object] The current user
|
|
78
|
+
# @param params [Hash] The workflow parameters
|
|
79
|
+
# @param parent_decisions [Array, nil] Parent branch decisions for nested tracking
|
|
80
|
+
# @return [Hash] Execution result with :executed_steps, :branch_taken, :branch_decisions, :skipped
|
|
81
|
+
# @raise [Errors::Configuration::InvalidConfigurationError] If no branch matches
|
|
82
|
+
def call(context, user, params, parent_decisions = nil)
|
|
83
|
+
selected_branch = select_branch(context)
|
|
84
|
+
|
|
85
|
+
if selected_branch.nil?
|
|
86
|
+
raise Errors::Configuration::InvalidConfigurationError.new(
|
|
87
|
+
"No matching branch found and no default branch defined",
|
|
88
|
+
code: ErrorCodes::CONFIGURATION_ERROR,
|
|
89
|
+
context: {
|
|
90
|
+
branch_group: @name,
|
|
91
|
+
branches_count: @branches.count,
|
|
92
|
+
has_default: !@default_branch.nil?
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Track this branch decision
|
|
98
|
+
branch_decision = "#{@name}:#{selected_branch.name}"
|
|
99
|
+
branch_decisions = [branch_decision]
|
|
100
|
+
|
|
101
|
+
# Execute the selected branch
|
|
102
|
+
executed_steps = selected_branch.execute(context, user, params, branch_decisions)
|
|
103
|
+
|
|
104
|
+
# Return execution metadata
|
|
105
|
+
{
|
|
106
|
+
executed_steps: executed_steps,
|
|
107
|
+
branch_taken: selected_branch,
|
|
108
|
+
branch_decisions: branch_decisions,
|
|
109
|
+
skipped: false
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns the total number of branches (including default)
|
|
114
|
+
#
|
|
115
|
+
# @return [Integer]
|
|
116
|
+
def branch_count
|
|
117
|
+
count = @branches.count
|
|
118
|
+
count += 1 if @default_branch
|
|
119
|
+
count
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns whether this group has a default branch
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean]
|
|
125
|
+
def has_default?
|
|
126
|
+
!@default_branch.nil?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns a string representation of this branch group
|
|
130
|
+
#
|
|
131
|
+
# @return [String]
|
|
132
|
+
def inspect
|
|
133
|
+
"#<BetterService::Workflows::BranchGroup name=#{@name.inspect} " \
|
|
134
|
+
"branches=#{@branches.count} " \
|
|
135
|
+
"has_default=#{has_default?}>"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -42,6 +42,52 @@ module BetterService
|
|
|
42
42
|
self._steps += [ step ]
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# DSL method to define conditional branches in the workflow
|
|
46
|
+
#
|
|
47
|
+
# Creates a branch group that allows multiple conditional execution paths.
|
|
48
|
+
# Only one branch will be executed based on the first matching condition.
|
|
49
|
+
#
|
|
50
|
+
# @param block [Proc] Block containing branch definitions (on/otherwise)
|
|
51
|
+
#
|
|
52
|
+
# @example Define conditional branches
|
|
53
|
+
# branch do
|
|
54
|
+
# on ->(ctx) { ctx.user.premium? } do
|
|
55
|
+
# step :premium_feature, with: PremiumService
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# on ->(ctx) { ctx.user.basic? } do
|
|
59
|
+
# step :basic_feature, with: BasicService
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# otherwise do
|
|
63
|
+
# step :default_feature, with: DefaultService
|
|
64
|
+
# end
|
|
65
|
+
# end
|
|
66
|
+
def branch(&block)
|
|
67
|
+
raise ArgumentError, "Block required for 'branch'" unless block_given?
|
|
68
|
+
|
|
69
|
+
# Count existing branch groups to determine index
|
|
70
|
+
branch_count = _steps.count { |s| s.is_a?(BranchGroup) }
|
|
71
|
+
|
|
72
|
+
# Create branch group
|
|
73
|
+
branch_group = BranchGroup.new(name: :"branch_#{branch_count + 1}")
|
|
74
|
+
|
|
75
|
+
# Create DSL context and evaluate block
|
|
76
|
+
branch_dsl = BranchDSL.new(branch_group)
|
|
77
|
+
branch_dsl.instance_eval(&block)
|
|
78
|
+
|
|
79
|
+
# Validate: must have at least one branch (conditional or default)
|
|
80
|
+
if branch_group.branches.empty? && branch_group.default_branch.nil?
|
|
81
|
+
raise Errors::Configuration::InvalidConfigurationError.new(
|
|
82
|
+
"Branch group must contain at least one 'on' or 'otherwise' block",
|
|
83
|
+
code: ErrorCodes::CONFIGURATION_ERROR
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Add branch group to steps
|
|
88
|
+
self._steps += [ branch_group ]
|
|
89
|
+
end
|
|
90
|
+
|
|
45
91
|
# Enable or disable database transactions for the entire workflow
|
|
46
92
|
#
|
|
47
93
|
# @param enabled [Boolean] Whether to use transactions
|
|
@@ -22,16 +22,39 @@ module BetterService
|
|
|
22
22
|
steps_executed = []
|
|
23
23
|
steps_skipped = []
|
|
24
24
|
|
|
25
|
-
self.class._steps.each do |
|
|
26
|
-
#
|
|
25
|
+
self.class._steps.each do |step_or_branch|
|
|
26
|
+
# Handle BranchGroup (conditional branching)
|
|
27
|
+
if step_or_branch.is_a?(BranchGroup)
|
|
28
|
+
branch_result = nil
|
|
29
|
+
run_around_step_callbacks(step_or_branch, @context) do
|
|
30
|
+
branch_result = step_or_branch.call(@context, @user, @params)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Track branch decisions (including nested)
|
|
34
|
+
if branch_result[:branch_decisions]
|
|
35
|
+
@branch_decisions.concat(branch_result[:branch_decisions])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Track executed steps from the branch
|
|
39
|
+
if branch_result[:executed_steps]
|
|
40
|
+
branch_result[:executed_steps].each do |executed_step|
|
|
41
|
+
@executed_steps << executed_step
|
|
42
|
+
steps_executed << executed_step.name
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Handle regular Step
|
|
27
50
|
result = nil
|
|
28
|
-
run_around_step_callbacks(
|
|
29
|
-
result =
|
|
51
|
+
run_around_step_callbacks(step_or_branch, @context) do
|
|
52
|
+
result = step_or_branch.call(@context, @user, @params)
|
|
30
53
|
end
|
|
31
54
|
|
|
32
55
|
# Track skipped steps
|
|
33
56
|
if result[:skipped]
|
|
34
|
-
steps_skipped <<
|
|
57
|
+
steps_skipped << step_or_branch.name
|
|
35
58
|
next
|
|
36
59
|
end
|
|
37
60
|
|
|
@@ -41,11 +64,11 @@ module BetterService
|
|
|
41
64
|
rollback_steps
|
|
42
65
|
|
|
43
66
|
raise Errors::Workflowable::Runtime::StepExecutionError.new(
|
|
44
|
-
"Step #{
|
|
67
|
+
"Step #{step_or_branch.name} failed: #{result[:error] || result[:message]}",
|
|
45
68
|
code: ErrorCodes::STEP_FAILED,
|
|
46
69
|
context: {
|
|
47
70
|
workflow: self.class.name,
|
|
48
|
-
step:
|
|
71
|
+
step: step_or_branch.name,
|
|
49
72
|
steps_executed: steps_executed,
|
|
50
73
|
errors: result[:errors] || {}
|
|
51
74
|
}
|
|
@@ -53,8 +76,8 @@ module BetterService
|
|
|
53
76
|
end
|
|
54
77
|
|
|
55
78
|
# Track successful execution
|
|
56
|
-
@executed_steps <<
|
|
57
|
-
steps_executed <<
|
|
79
|
+
@executed_steps << step_or_branch
|
|
80
|
+
steps_executed << step_or_branch.name
|
|
58
81
|
end
|
|
59
82
|
|
|
60
83
|
# All steps succeeded
|
|
@@ -66,6 +89,9 @@ module BetterService
|
|
|
66
89
|
rescue Errors::Workflowable::Runtime::StepExecutionError
|
|
67
90
|
# Step error already raised, just re-raise
|
|
68
91
|
raise
|
|
92
|
+
rescue Errors::Configuration::InvalidConfigurationError
|
|
93
|
+
# Configuration error (e.g., no matching branch), just re-raise
|
|
94
|
+
raise
|
|
69
95
|
rescue StandardError => e
|
|
70
96
|
# Unexpected error during workflow execution
|
|
71
97
|
rollback_steps
|
|
@@ -15,16 +15,21 @@ module BetterService
|
|
|
15
15
|
# @param steps_skipped [Array<Symbol>] Names of steps that were skipped
|
|
16
16
|
# @return [Hash] Success result with context and metadata
|
|
17
17
|
def build_success_result(steps_executed: [], steps_skipped: [])
|
|
18
|
+
metadata = {
|
|
19
|
+
workflow: self.class.name,
|
|
20
|
+
steps_executed: steps_executed,
|
|
21
|
+
steps_skipped: steps_skipped,
|
|
22
|
+
duration_ms: duration_ms
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Include branch decisions if any branches were taken
|
|
26
|
+
metadata[:branches_taken] = @branch_decisions if @branch_decisions.any?
|
|
27
|
+
|
|
18
28
|
{
|
|
19
29
|
success: true,
|
|
20
30
|
message: "Workflow completed successfully",
|
|
21
31
|
context: @context,
|
|
22
|
-
metadata:
|
|
23
|
-
workflow: self.class.name,
|
|
24
|
-
steps_executed: steps_executed,
|
|
25
|
-
steps_skipped: steps_skipped,
|
|
26
|
-
duration_ms: duration_ms
|
|
27
|
-
}
|
|
32
|
+
metadata: metadata
|
|
28
33
|
}
|
|
29
34
|
end
|
|
30
35
|
|
|
@@ -37,22 +42,26 @@ module BetterService
|
|
|
37
42
|
# @param steps_skipped [Array<Symbol>] Names of steps that were skipped
|
|
38
43
|
# @return [Hash] Failure result with error details and metadata
|
|
39
44
|
def build_failure_result(message: nil, errors: {}, failed_step: nil, steps_executed: [], steps_skipped: [])
|
|
40
|
-
|
|
45
|
+
metadata = {
|
|
46
|
+
workflow: self.class.name,
|
|
47
|
+
failed_step: failed_step,
|
|
48
|
+
steps_executed: steps_executed,
|
|
49
|
+
steps_skipped: steps_skipped,
|
|
50
|
+
duration_ms: duration_ms
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Include branch decisions if any branches were taken before failure
|
|
54
|
+
metadata[:branches_taken] = @branch_decisions if @branch_decisions.any?
|
|
55
|
+
|
|
56
|
+
metadata.delete(:failed_step) if failed_step.nil?
|
|
57
|
+
|
|
58
|
+
{
|
|
41
59
|
success: false,
|
|
42
60
|
error: message || @context.errors[:message] || "Workflow failed",
|
|
43
61
|
errors: errors.any? ? errors : @context.errors,
|
|
44
62
|
context: @context,
|
|
45
|
-
metadata:
|
|
46
|
-
workflow: self.class.name,
|
|
47
|
-
failed_step: failed_step,
|
|
48
|
-
steps_executed: steps_executed,
|
|
49
|
-
steps_skipped: steps_skipped,
|
|
50
|
-
duration_ms: duration_ms
|
|
51
|
-
}
|
|
63
|
+
metadata: metadata
|
|
52
64
|
}
|
|
53
|
-
|
|
54
|
-
result[:metadata].delete(:failed_step) if failed_step.nil?
|
|
55
|
-
result
|
|
56
65
|
end
|
|
57
66
|
|
|
58
67
|
# Calculate duration in milliseconds
|
data/lib/better_service.rb
CHANGED
|
@@ -20,6 +20,9 @@ require "better_service/concerns/workflowable/callbacks"
|
|
|
20
20
|
require "better_service/workflows/result_builder"
|
|
21
21
|
require "better_service/workflows/rollback_support"
|
|
22
22
|
require "better_service/workflows/transaction_support"
|
|
23
|
+
require "better_service/workflows/branch"
|
|
24
|
+
require "better_service/workflows/branch_group"
|
|
25
|
+
require "better_service/workflows/branch_dsl"
|
|
23
26
|
require "better_service/workflows/dsl"
|
|
24
27
|
require "better_service/workflows/execution"
|
|
25
28
|
require "better_service/workflows/base"
|
|
@@ -48,6 +48,28 @@ class <%= workflow_class_name %> < BetterService::Workflow
|
|
|
48
48
|
# input: ->(ctx) { { resource_id: ctx.resource.id } },
|
|
49
49
|
# optional: true,
|
|
50
50
|
# if: ->(ctx) { ctx.user.notifications_enabled? }
|
|
51
|
+
#
|
|
52
|
+
# Conditional branching (NEW):
|
|
53
|
+
#
|
|
54
|
+
# branch do
|
|
55
|
+
# on ->(ctx) { ctx.user.premium? } do
|
|
56
|
+
# step :premium_feature,
|
|
57
|
+
# with: <%= class_name %>::PremiumService,
|
|
58
|
+
# input: ->(ctx) { { resource: ctx.resource } }
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# on ->(ctx) { ctx.user.basic? } do
|
|
62
|
+
# step :basic_feature,
|
|
63
|
+
# with: <%= class_name %>::BasicService,
|
|
64
|
+
# input: ->(ctx) { { resource: ctx.resource } }
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# otherwise do
|
|
68
|
+
# step :default_feature,
|
|
69
|
+
# with: <%= class_name %>::DefaultService,
|
|
70
|
+
# input: ->(ctx) { { resource: ctx.resource } }
|
|
71
|
+
# end
|
|
72
|
+
# end
|
|
51
73
|
|
|
52
74
|
<% end -%>
|
|
53
75
|
private
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- alessiobussolari
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -74,7 +74,7 @@ executables: []
|
|
|
74
74
|
extensions: []
|
|
75
75
|
extra_rdoc_files: []
|
|
76
76
|
files:
|
|
77
|
-
-
|
|
77
|
+
- LICENSE
|
|
78
78
|
- README.md
|
|
79
79
|
- Rakefile
|
|
80
80
|
- config/locales/better_service.en.yml
|
|
@@ -128,6 +128,9 @@ files:
|
|
|
128
128
|
- lib/better_service/subscribers/stats_subscriber.rb
|
|
129
129
|
- lib/better_service/version.rb
|
|
130
130
|
- lib/better_service/workflows/base.rb
|
|
131
|
+
- lib/better_service/workflows/branch.rb
|
|
132
|
+
- lib/better_service/workflows/branch_dsl.rb
|
|
133
|
+
- lib/better_service/workflows/branch_group.rb
|
|
131
134
|
- lib/better_service/workflows/dsl.rb
|
|
132
135
|
- lib/better_service/workflows/execution.rb
|
|
133
136
|
- lib/better_service/workflows/result_builder.rb
|
|
@@ -161,7 +164,7 @@ files:
|
|
|
161
164
|
- lib/tasks/better_service_tasks.rake
|
|
162
165
|
homepage: https://github.com/alessiobussolari/better_service
|
|
163
166
|
licenses:
|
|
164
|
-
-
|
|
167
|
+
- WTFPL
|
|
165
168
|
metadata:
|
|
166
169
|
homepage_uri: https://github.com/alessiobussolari/better_service
|
|
167
170
|
source_code_uri: https://github.com/alessiobussolari/better_service
|
data/MIT-LICENSE
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
Copyright alessiobussolari
|
|
2
|
-
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
-
a copy of this software and associated documentation files (the
|
|
5
|
-
"Software"), to deal in the Software without restriction, including
|
|
6
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
-
the following conditions:
|
|
10
|
-
|
|
11
|
-
The above copyright notice and this permission notice shall be
|
|
12
|
-
included in all copies or substantial portions of the Software.
|
|
13
|
-
|
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|