hubbado-trailblazer 1.0.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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +21 -0
- data/README.md +215 -0
- data/hubbado-trailblazer.gemspec +38 -0
- data/lib/hubbado/trailblazer/errors.rb +15 -0
- data/lib/hubbado/trailblazer/macro/decorate_model.rb +19 -0
- data/lib/hubbado/trailblazer/macro/policy.rb +44 -0
- data/lib/hubbado/trailblazer/macro/prepopulate_contract.rb +27 -0
- data/lib/hubbado/trailblazer/run_operation.rb +224 -0
- data/lib/hubbado/trailblazer/trace_operation.rb +9 -0
- data/lib/hubbado/trailblazer/transaction.rb +16 -0
- data/lib/hubbado/trailblazer.rb +5 -0
- data/lib/hubbado-trailblazer.rb +15 -0
- metadata +223 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 60b8c12b372f5ab077aa00bdf7f9638739d76485423eefebfd1aab14d4c17088
|
4
|
+
data.tar.gz: 3de9bda895d56d17681e98b1c23a9fbce15f2dbb0442ee5500eae57e8aefee6d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 852d22bce0c966280fad36f04c0eb0ec2df6318b58ccaf3d69a3c21ee15b108f1c91d2c985c715a27e5820c7aec742f5483819354d4fef57f2eaac3f7668cdf1
|
7
|
+
data.tar.gz: e6a2141bac839c57928ef4891222bdfb1584fd94e2a6eb66d319899084e893a496f9c9cd068b9f66e5592b397f9e2f18209c2cc6ed155e5ff31c27e784ef3240
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [1.0.0] - 2025-05-29
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- Initial release of the hubbado-trailblazer gem
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Hubbado
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
# Hubbado Trailblazer
|
2
|
+
|
3
|
+
Enhanced Trailblazer operation utilities for Ruby applications with improved error handling, operation execution patterns, and ActiveRecord integration.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'hubbado-trailblazer'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ POSTURE=dev ./install-gems.sh
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install hubbado-trailblazer
|
23
|
+
```
|
24
|
+
|
25
|
+
## Overview
|
26
|
+
|
27
|
+
This gem extends Trailblazer operations with enhanced execution patterns, automatic transaction handling, and structured error handling. It provides:
|
28
|
+
|
29
|
+
- **RunOperation**: A mixin for executing operations with structured result handling
|
30
|
+
- **Transaction**: Automatic ActiveRecord transaction wrapping for operations
|
31
|
+
- **Macros**: Custom operation macros for common patterns (policies, decorators, contract prepopulation)
|
32
|
+
- **Enhanced Error Handling**: Structured error responses with proper exception types
|
33
|
+
- **Operation Tracing**: Debug capabilities for operation execution flow
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
### Basic Operation Execution
|
38
|
+
|
39
|
+
Create a concern to include the RunOperation functionality in your controllers:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
module Concern
|
43
|
+
module Trailblazer
|
44
|
+
extend ActiveSupport::Concern
|
45
|
+
|
46
|
+
included do
|
47
|
+
include Hubbado::Trailblazer::RunOperation
|
48
|
+
|
49
|
+
def _run_options(options)
|
50
|
+
options.merge(
|
51
|
+
current_user: current_user || Users::Models::User.guest,
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Trailblazer takes all the parameters, no permit for us!
|
56
|
+
def _run_params(params)
|
57
|
+
params = params.to_unsafe_hash if params.respond_to?(:to_unsafe_hash)
|
58
|
+
params
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
Then include it in your controllers:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class UsersController < ApplicationController
|
69
|
+
include Concern::Trailblazer
|
70
|
+
|
71
|
+
def create
|
72
|
+
run_operation Users::Create do |result|
|
73
|
+
result.success do |ctx|
|
74
|
+
redirect_to user_path(ctx[:user])
|
75
|
+
end
|
76
|
+
|
77
|
+
result.validation_failed do |ctx|
|
78
|
+
render :new, locals: { contract: ctx['contract.default'] }
|
79
|
+
end
|
80
|
+
|
81
|
+
result.policy_failed do |ctx|
|
82
|
+
redirect_to root_path, alert: 'Not authorized'
|
83
|
+
end
|
84
|
+
|
85
|
+
result.otherwise do |ctx|
|
86
|
+
redirect_to users_path, alert: 'Something went wrong'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
### Transaction Handling
|
94
|
+
|
95
|
+
Wrap operations in ActiveRecord transactions that automatically rollback on failure:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
class Users::Create < Trailblazer::Operation
|
99
|
+
step Wrap(Hubbado::Trailblazer::Transaction) {
|
100
|
+
step Model(User, :new)
|
101
|
+
step Contract::Build(constant: Users::CreateContract)
|
102
|
+
step Contract::Validate()
|
103
|
+
step Contract::Persist()
|
104
|
+
}
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
### Custom Macros
|
109
|
+
|
110
|
+
#### Policy Macro
|
111
|
+
|
112
|
+
Enhanced policy handling for use with Hubbado::Policy:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class Users::Update < Trailblazer::Operation
|
116
|
+
step Hubbado::Trailblazer::Macro::Policy(Users::UpdatePolicy)
|
117
|
+
# ... other steps
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
#### Decorate Model Macro
|
122
|
+
|
123
|
+
Automatically decorate models using `SomeDecorator.build(model, current_user)`:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class Users::Show < Trailblazer::Operation
|
127
|
+
step Model(User, :find)
|
128
|
+
step Hubbado::Trailblazer::Macro::DecorateModel(UserDecorator)
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
#### Prepopulate Contract Macro
|
133
|
+
|
134
|
+
Prepopulate Reform contracts with values from params without validation:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class Users::Edit < Trailblazer::Operation
|
138
|
+
step Model(User, :find)
|
139
|
+
step Contract::Build(constant: Users::UpdateContract)
|
140
|
+
step Hubbado::Trailblazer::Macro::PrepopulateContract()
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
## Operation Tracing
|
145
|
+
|
146
|
+
Debug operation execution by setting the `TRACE_OPERATION` environment variable:
|
147
|
+
|
148
|
+
```bash
|
149
|
+
# Trace a specific operation
|
150
|
+
TRACE_OPERATION=Users::Create rails console
|
151
|
+
|
152
|
+
# Trace all operations
|
153
|
+
TRACE_OPERATION=_all rails console
|
154
|
+
```
|
155
|
+
|
156
|
+
This will output detailed execution traces showing which steps pass or fail.
|
157
|
+
|
158
|
+
## Advanced Features
|
159
|
+
|
160
|
+
### Custom Runtime Options
|
161
|
+
|
162
|
+
The concern template methods can be customized for your application's needs:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
def _run_options(options)
|
166
|
+
options.merge(
|
167
|
+
current_user: current_user || Users::Models::User.guest,
|
168
|
+
request: request,
|
169
|
+
session: session
|
170
|
+
)
|
171
|
+
end
|
172
|
+
|
173
|
+
def _run_params(params)
|
174
|
+
params = params.to_unsafe_hash if params.respond_to?(:to_unsafe_hash)
|
175
|
+
params
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
### Error Handling
|
180
|
+
|
181
|
+
The gem provides structured error handling with specific exception types:
|
182
|
+
|
183
|
+
- `Hubbado::Trailblazer::Errors::Unauthorized` - Raised when policies fail
|
184
|
+
- `Hubbado::Trailblazer::OperationFailed` - Raised when operations fail unexpectedly
|
185
|
+
|
186
|
+
## Testing
|
187
|
+
|
188
|
+
Run the test suite:
|
189
|
+
|
190
|
+
```bash
|
191
|
+
$ ./test.sh
|
192
|
+
```
|
193
|
+
|
194
|
+
The gem uses TestBench for testing and includes comprehensive test coverage for all operation patterns and macros.
|
195
|
+
|
196
|
+
## Requirements
|
197
|
+
|
198
|
+
- Ruby >= 3.2
|
199
|
+
- ActiveRecord
|
200
|
+
- Trailblazer Operation
|
201
|
+
- Reform (for contract testing)
|
202
|
+
|
203
|
+
## Contributing
|
204
|
+
|
205
|
+
1. Fork the repository
|
206
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
207
|
+
3. Make your changes and add tests
|
208
|
+
4. Ensure all tests pass (`./test.sh`)
|
209
|
+
5. Commit your changes (`git commit -am 'Add some feature'`)
|
210
|
+
6. Push to the branch (`git push origin my-new-feature`)
|
211
|
+
7. Create a new Pull Request
|
212
|
+
|
213
|
+
## License
|
214
|
+
|
215
|
+
This gem is available under the MIT License. See the LICENSE file for more details.
|
@@ -0,0 +1,38 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "hubbado-trailblazer"
|
3
|
+
s.version = "1.0.0"
|
4
|
+
s.summary = "Enhanced Trailblazer operation utilities for Ruby applications with improved error handling, operation execution patterns, and ActiveRecord integration."
|
5
|
+
|
6
|
+
s.authors = ["Hubbado Devs"]
|
7
|
+
s.email = ["devs@hubbado.com"]
|
8
|
+
s.homepage = 'https://github.com/hubbado/hubbado-trailblazer'
|
9
|
+
|
10
|
+
s.metadata["github_repo"] = s.homepage
|
11
|
+
s.metadata["homepage_uri"] = s.homepage
|
12
|
+
s.metadata["changelog_uri"] = "#{s.homepage}/blob/master/CHANGELOG.md"
|
13
|
+
|
14
|
+
s.require_paths = ["lib"]
|
15
|
+
s.files = Dir.glob(%w[
|
16
|
+
lib/**/*.rb
|
17
|
+
*.gemspec
|
18
|
+
LICENSE*
|
19
|
+
README*
|
20
|
+
CHANGELOG*
|
21
|
+
])
|
22
|
+
s.platform = Gem::Platform::RUBY
|
23
|
+
s.required_ruby_version = ">= 3.2"
|
24
|
+
|
25
|
+
s.add_dependency "activerecord"
|
26
|
+
s.add_dependency "evt-template_method"
|
27
|
+
s.add_dependency "hubbado-log"
|
28
|
+
s.add_dependency "trailblazer-operation"
|
29
|
+
|
30
|
+
s.add_development_dependency "debug"
|
31
|
+
s.add_development_dependency "dry-validation"
|
32
|
+
s.add_development_dependency "sqlite3"
|
33
|
+
s.add_development_dependency "hubbado-style"
|
34
|
+
s.add_development_dependency "reform"
|
35
|
+
s.add_development_dependency "test_bench"
|
36
|
+
s.add_development_dependency "trailblazer-macro"
|
37
|
+
s.add_development_dependency "trailblazer-macro-contract"
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Hubbado
|
2
|
+
module Trailblazer
|
3
|
+
module Macro
|
4
|
+
def self.DecorateModel(decorator, model: :model)
|
5
|
+
task = ->((ctx, flow_options), _) do
|
6
|
+
unless ctx[model] && ctx[:current_user]
|
7
|
+
return ::Trailblazer::Activity::Left, [ctx, flow_options]
|
8
|
+
end
|
9
|
+
|
10
|
+
ctx[model] = decorator.build(ctx[model], ctx[:current_user])
|
11
|
+
|
12
|
+
[::Trailblazer::Activity::Right, [ctx, flow_options]]
|
13
|
+
end
|
14
|
+
|
15
|
+
{ task: task, id: "Decorate" }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Hubbado
|
2
|
+
module Trailblazer
|
3
|
+
module Macro
|
4
|
+
def self.Policy(policy_class, action, name: :default, model: :model)
|
5
|
+
::Trailblazer::Macro::Policy.step(
|
6
|
+
Policy.build(policy_class, action, model), name: name
|
7
|
+
)
|
8
|
+
end
|
9
|
+
|
10
|
+
module Policy
|
11
|
+
def self.build(policy_class, action, model)
|
12
|
+
Condition.new(policy_class, action, model)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Pundit::Condition is invoked at runtime when iterating the pipe.
|
16
|
+
class Condition
|
17
|
+
def initialize(policy_class, action, model)
|
18
|
+
@policy_class = policy_class
|
19
|
+
@action = action
|
20
|
+
@model = model
|
21
|
+
end
|
22
|
+
|
23
|
+
# Instantiate the actual policy object, and call it.
|
24
|
+
def call((options), *)
|
25
|
+
policy = build_policy(options)
|
26
|
+
result!(policy.send(@action), policy)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_policy(options)
|
32
|
+
@policy_class.build(options[:current_user], options[@model])
|
33
|
+
end
|
34
|
+
|
35
|
+
def result!(policy_result, policy)
|
36
|
+
data = { policy: policy, policy_result: policy_result }
|
37
|
+
|
38
|
+
::Trailblazer::Operation::Result.new(policy_result.permitted?, data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Hubbado
|
2
|
+
module Trailblazer
|
3
|
+
module Macro
|
4
|
+
# This is used to pre-populate a contract before validating it, so that
|
5
|
+
# form options can be built based on the contract values
|
6
|
+
#
|
7
|
+
# For example, a contract, for a not yet saved assignment, might have
|
8
|
+
# timesheet approvers that depend on the client company ID in the
|
9
|
+
# contract
|
10
|
+
def self.PrepopulateContract(key:)
|
11
|
+
task = ->((ctx, flow_options), _) do
|
12
|
+
ctx[:prepopulated_contract] = key
|
13
|
+
|
14
|
+
params = ctx[:params][key]
|
15
|
+
|
16
|
+
return ::Trailblazer::Activity::Right, [ctx, flow_options] unless params
|
17
|
+
|
18
|
+
ctx['contract.default'].deserialize(params)
|
19
|
+
|
20
|
+
[::Trailblazer::Activity::Right, [ctx, flow_options]]
|
21
|
+
end
|
22
|
+
|
23
|
+
{ task: task, id: "PrepopulateContract" }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module Hubbado
|
2
|
+
module Trailblazer
|
3
|
+
# This allows you to run an operation and then respond differently based on
|
4
|
+
# the result of the operation.
|
5
|
+
#
|
6
|
+
# run_operation MyOperation do |result|
|
7
|
+
# result.success do |ctx|
|
8
|
+
# ...
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# result.policy_failed do |ctx|
|
12
|
+
# # This is optional if not used the operation will raise an exception
|
13
|
+
# ...
|
14
|
+
# # Also optionally you can raise the default exception, it will not be raised
|
15
|
+
# # if the block is executed otherwise when the block is finished
|
16
|
+
# result.raise_policy_failed
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# result.validation_failed do |ctx|
|
20
|
+
# ...
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# result.otherwise do |ctx|
|
24
|
+
# # This is optional if not used the operation will raise an exception
|
25
|
+
# ...
|
26
|
+
# # Also optionally you can raise the default exception, it will not be raised
|
27
|
+
# # if the block is executed otherwise when the block is finished
|
28
|
+
# result.raise_operation_failed
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# ctx is the context built by the operation.
|
33
|
+
#
|
34
|
+
# If there is a policy failure and you have not implemented
|
35
|
+
# `result.policy_failed` then an exception will be raised.
|
36
|
+
#
|
37
|
+
# If the operation fails (due to non-policy error) and you have not
|
38
|
+
# implemented `result.otherwise` then an exception will be raised.
|
39
|
+
#
|
40
|
+
# Note, it is on purpose that implementing `result.otherwise` is not enough
|
41
|
+
# to stop an exception being raised if the policy fails. This to prevent a
|
42
|
+
# `result.otherwise` block rendering a form when the policy failed.
|
43
|
+
#
|
44
|
+
# We also considered TrailblazerEndpoint in place of this, but it seemed to
|
45
|
+
# be complicated for what it quite a simple usecase
|
46
|
+
#
|
47
|
+
# Tracing operations
|
48
|
+
#
|
49
|
+
# Setting the ENV variable TRACE_OPERATION to the class name of an
|
50
|
+
# operation (or to "_all") will cause the operation to be run with "wtf?"
|
51
|
+
# rather than `call`, outputting the trace of the operation, and showing
|
52
|
+
# which step failed, to stdout (not logging)
|
53
|
+
#
|
54
|
+
# This is useful when debugging traces
|
55
|
+
module RunOperation
|
56
|
+
include TemplateMethod
|
57
|
+
|
58
|
+
# Implement this if params need pre-processing before being passed to the operation
|
59
|
+
# For example, a controller needs to turn params into an unsafe hash
|
60
|
+
template_method :_run_params do |params|
|
61
|
+
params
|
62
|
+
end
|
63
|
+
|
64
|
+
# Implement this to inject additional context into the operation
|
65
|
+
# For example, a a controller can pass in current_user
|
66
|
+
template_method :_run_options do |ctx|
|
67
|
+
ctx
|
68
|
+
end
|
69
|
+
|
70
|
+
def _run_runtime_options(ctx = {}, *dependencies)
|
71
|
+
[_run_options(ctx), *dependencies]
|
72
|
+
end
|
73
|
+
|
74
|
+
def run_operation(operation, params = self.params, *options)
|
75
|
+
trace_operation = TraceOperation.(operation)
|
76
|
+
operation_method = trace_operation ? 'wtf?' : 'call'
|
77
|
+
|
78
|
+
operation_arguments = { params: _run_params(params) }
|
79
|
+
operation_arguments[:request] = request if respond_to?(:request)
|
80
|
+
operation_arguments.merge!(*_run_runtime_options(*options))
|
81
|
+
|
82
|
+
ctx = operation.send(operation_method, operation_arguments)
|
83
|
+
|
84
|
+
result = Result.new(operation, ctx, trace_operation)
|
85
|
+
|
86
|
+
yield(result) if block_given?
|
87
|
+
|
88
|
+
if ctx['result.policy.default']&.failure?
|
89
|
+
result.raise_policy_failed unless result.policy_failed_executed?
|
90
|
+
elsif ctx.failure? && !result.validation_failed_executed? && !result.otherwise_executed?
|
91
|
+
result.raise_operation_failed
|
92
|
+
end
|
93
|
+
|
94
|
+
result.returned
|
95
|
+
end
|
96
|
+
|
97
|
+
class Result
|
98
|
+
include Hubbado::Log::Dependency
|
99
|
+
|
100
|
+
attr_reader :returned
|
101
|
+
attr_reader :trace_operation
|
102
|
+
|
103
|
+
def initialize(operation, ctx, trace_operation)
|
104
|
+
@operation = operation
|
105
|
+
@ctx = ctx
|
106
|
+
@trace_operation = trace_operation
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_level
|
110
|
+
trace_operation ? :debug : :info
|
111
|
+
end
|
112
|
+
|
113
|
+
def success
|
114
|
+
return unless ctx.success?
|
115
|
+
|
116
|
+
@success_executed = true
|
117
|
+
@returned = yield(ctx)
|
118
|
+
|
119
|
+
logger.send(log_level, "Success block executed for operation #{operation}")
|
120
|
+
@returned
|
121
|
+
end
|
122
|
+
|
123
|
+
def policy_failed
|
124
|
+
return unless ctx['result.policy.default']&.failure?
|
125
|
+
|
126
|
+
@policy_failed_executed = true
|
127
|
+
@returned = yield(ctx)
|
128
|
+
|
129
|
+
logger.send(log_level, "Policy failed block executed for operation #{operation}")
|
130
|
+
@returned
|
131
|
+
end
|
132
|
+
|
133
|
+
def validation_failed
|
134
|
+
return if success_executed?
|
135
|
+
|
136
|
+
contract = ctx['contract.default']
|
137
|
+
# TODO: We cannot call `contract.valid?` here, since the following
|
138
|
+
# steps will have converted form errors from strings to symbols:
|
139
|
+
#
|
140
|
+
# contract = Companies::Controls::Contracts::InviteMember.example
|
141
|
+
# contract.validate({name: 'Some name', email: 'email@example.com', role: :staff)
|
142
|
+
# contract.errors.add :email, :taken
|
143
|
+
# contract.valid?
|
144
|
+
#
|
145
|
+
# This only happens if errors are added outside of the validation
|
146
|
+
return if contract.nil? || contract.errors.full_messages.empty?
|
147
|
+
|
148
|
+
@validation_failed_executed = true
|
149
|
+
@returned = yield(ctx)
|
150
|
+
|
151
|
+
if trace_operation
|
152
|
+
logger.send(
|
153
|
+
log_level,
|
154
|
+
"Validation failed: #{ctx['contract.default'].errors.full_messages.join(', ')}"
|
155
|
+
)
|
156
|
+
else
|
157
|
+
logger.send(log_level, "Validation failed")
|
158
|
+
end
|
159
|
+
@returned
|
160
|
+
end
|
161
|
+
|
162
|
+
def otherwise
|
163
|
+
return if executed?
|
164
|
+
|
165
|
+
@otherwise_executed = true
|
166
|
+
@returned = yield(ctx)
|
167
|
+
|
168
|
+
logger.send(log_level, "Otherwise block executed for operation #{operation}")
|
169
|
+
@returned
|
170
|
+
end
|
171
|
+
|
172
|
+
def raise_operation_failed
|
173
|
+
msg = "Operation #{operation.name} failed"
|
174
|
+
|
175
|
+
error_messages = ctx['contract.default']&.errors&.full_messages
|
176
|
+
|
177
|
+
if error_messages&.any?
|
178
|
+
msg += " with errors:\n\n#{error_messages.map { |e| " - #{e}" }.join("\n")}"
|
179
|
+
end
|
180
|
+
|
181
|
+
raise StandardError, msg
|
182
|
+
end
|
183
|
+
|
184
|
+
def raise_policy_failed
|
185
|
+
current_user = ctx[:current_user]
|
186
|
+
true_user = ctx[:true_user]
|
187
|
+
|
188
|
+
msg = "User #{current_user&.id}/#{true_user&.id} (#{current_user&.roles&.join ', '}) " \
|
189
|
+
"not allowed to run #{operation.name}"
|
190
|
+
|
191
|
+
raise Hubbado::Trailblazer::Errors::Unauthorized.new(msg, ctx['result.policy.default'][:policy_result])
|
192
|
+
end
|
193
|
+
|
194
|
+
def success_executed?
|
195
|
+
!!@success_executed
|
196
|
+
end
|
197
|
+
|
198
|
+
def policy_failed_executed?
|
199
|
+
!!@policy_failed_executed
|
200
|
+
end
|
201
|
+
|
202
|
+
def validation_failed_executed?
|
203
|
+
!!@validation_failed_executed
|
204
|
+
end
|
205
|
+
|
206
|
+
def otherwise_executed?
|
207
|
+
!!@otherwise_executed
|
208
|
+
end
|
209
|
+
|
210
|
+
def executed?
|
211
|
+
success_executed? ||
|
212
|
+
policy_failed_executed? ||
|
213
|
+
validation_failed_executed? ||
|
214
|
+
otherwise_executed?
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
attr_reader :operation
|
220
|
+
attr_reader :ctx
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Hubbado
|
2
|
+
module Trailblazer
|
3
|
+
class Transaction
|
4
|
+
def self.call((_ctx, _flow_options), *)
|
5
|
+
res = nil
|
6
|
+
|
7
|
+
ActiveRecord::Base.transaction do
|
8
|
+
res = yield
|
9
|
+
raise ActiveRecord::Rollback if res.first.to_h[:semantic] == :failure
|
10
|
+
end
|
11
|
+
|
12
|
+
res
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'hubbado/log'
|
3
|
+
require 'template_method'
|
4
|
+
require 'trailblazer/operation'
|
5
|
+
|
6
|
+
require 'hubbado/trailblazer'
|
7
|
+
|
8
|
+
require 'hubbado/trailblazer/transaction'
|
9
|
+
require 'hubbado/trailblazer/errors'
|
10
|
+
require 'hubbado/trailblazer/run_operation'
|
11
|
+
require 'hubbado/trailblazer/trace_operation'
|
12
|
+
|
13
|
+
require 'hubbado/trailblazer/macro/decorate_model'
|
14
|
+
require 'hubbado/trailblazer/macro/policy'
|
15
|
+
require 'hubbado/trailblazer/macro/prepopulate_contract'
|
metadata
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hubbado-trailblazer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hubbado Devs
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activerecord
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: evt-template_method
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: hubbado-log
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: trailblazer-operation
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: debug
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: dry-validation
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: sqlite3
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: hubbado-style
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: reform
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: test_bench
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: trailblazer-macro
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
type: :development
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
- !ruby/object:Gem::Dependency
|
167
|
+
name: trailblazer-macro-contract
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
type: :development
|
174
|
+
prerelease: false
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
email:
|
181
|
+
- devs@hubbado.com
|
182
|
+
executables: []
|
183
|
+
extensions: []
|
184
|
+
extra_rdoc_files: []
|
185
|
+
files:
|
186
|
+
- CHANGELOG.md
|
187
|
+
- LICENSE
|
188
|
+
- README.md
|
189
|
+
- hubbado-trailblazer.gemspec
|
190
|
+
- lib/hubbado-trailblazer.rb
|
191
|
+
- lib/hubbado/trailblazer.rb
|
192
|
+
- lib/hubbado/trailblazer/errors.rb
|
193
|
+
- lib/hubbado/trailblazer/macro/decorate_model.rb
|
194
|
+
- lib/hubbado/trailblazer/macro/policy.rb
|
195
|
+
- lib/hubbado/trailblazer/macro/prepopulate_contract.rb
|
196
|
+
- lib/hubbado/trailblazer/run_operation.rb
|
197
|
+
- lib/hubbado/trailblazer/trace_operation.rb
|
198
|
+
- lib/hubbado/trailblazer/transaction.rb
|
199
|
+
homepage: https://github.com/hubbado/hubbado-trailblazer
|
200
|
+
licenses: []
|
201
|
+
metadata:
|
202
|
+
github_repo: https://github.com/hubbado/hubbado-trailblazer
|
203
|
+
homepage_uri: https://github.com/hubbado/hubbado-trailblazer
|
204
|
+
changelog_uri: https://github.com/hubbado/hubbado-trailblazer/blob/master/CHANGELOG.md
|
205
|
+
rdoc_options: []
|
206
|
+
require_paths:
|
207
|
+
- lib
|
208
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: '3.2'
|
213
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
214
|
+
requirements:
|
215
|
+
- - ">="
|
216
|
+
- !ruby/object:Gem::Version
|
217
|
+
version: '0'
|
218
|
+
requirements: []
|
219
|
+
rubygems_version: 3.6.7
|
220
|
+
specification_version: 4
|
221
|
+
summary: Enhanced Trailblazer operation utilities for Ruby applications with improved
|
222
|
+
error handling, operation execution patterns, and ActiveRecord integration.
|
223
|
+
test_files: []
|