interceptors 1.0.2 → 1.0.3
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/README.md +61 -0
- data/lib/interceptors/use_case.rb +1 -111
- data/lib/interceptors/use_case_core.rb +125 -0
- data/lib/interceptors/use_case_mixin.rb +9 -0
- data/lib/interceptors/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f06c031c777b039546473d09618c1b7c76c1709879e4c9f516384811599a770
|
|
4
|
+
data.tar.gz: b7bc835285431b9784a5d4c492f5c1b3851e3638d0856be5e146412b68d6d367
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eab8fd67835c55d4ec907f30ca5a9fdb182542336e48d915b11204cbb6fbd14e065b4ab41d1b2cf25bf57a638f732c6b92810e0e79755c1a9904f54a5ef0502d
|
|
7
|
+
data.tar.gz: bbf17a658891f4e88f8295ac03b00de649cfc0b1ba483b2065286f1b4c85e710ff4f96cb8119c5c60c57e0a843b8752559b147bceadfdc7860c08175da988a97
|
data/README.md
CHANGED
|
@@ -116,6 +116,25 @@ else
|
|
|
116
116
|
end
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
### Using the mixin instead of inheritance
|
|
120
|
+
|
|
121
|
+
If you prefer not to inherit from `Interceptors::UseCase`, include the mixin to add the same DSL and runtime behaviour to any PORO:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
class RefundOrder
|
|
125
|
+
include Interceptors::UseCaseMixin
|
|
126
|
+
|
|
127
|
+
use Interceptors::LoggingInterceptor.new
|
|
128
|
+
|
|
129
|
+
def execute(ctx)
|
|
130
|
+
refund = RefundProcessor.call!(order_id: ctx[:order_id])
|
|
131
|
+
Interceptors::Result.ok(refund)
|
|
132
|
+
rescue RefundProcessor::Error => e
|
|
133
|
+
Interceptors::Result.err(Interceptors::AppError.new(e.message, code: "refund_failed"))
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
119
138
|
Instrument use cases with ActiveSupport:
|
|
120
139
|
|
|
121
140
|
```ruby
|
|
@@ -124,6 +143,48 @@ ActiveSupport::Notifications.subscribe("use_case.finish") do |_name, _start, _fi
|
|
|
124
143
|
end
|
|
125
144
|
```
|
|
126
145
|
|
|
146
|
+
### Writing custom interceptors
|
|
147
|
+
|
|
148
|
+
Interceptors respond to three optional hooks:
|
|
149
|
+
|
|
150
|
+
- `before(ctx)` runs before the next step and can mutate the context or raise to halt execution.
|
|
151
|
+
- `around(ctx) { |ctx| ... }` wraps the remainder of the pipeline; call `yield ctx` to continue or return a `Result` to short-circuit.
|
|
152
|
+
- `after(ctx, result)` executes after the inner handler returns; return value is ignored unless you return a new `Result`.
|
|
153
|
+
|
|
154
|
+
To build your own interceptor:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
class AuditInterceptor < Interceptors::Interceptor
|
|
158
|
+
def before(ctx)
|
|
159
|
+
AuditTrail.write(event: "start", use_case: ctx[:use_case])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def around(ctx)
|
|
163
|
+
super
|
|
164
|
+
rescue => e
|
|
165
|
+
AuditTrail.write(event: "error", use_case: ctx[:use_case], error: e.class.name)
|
|
166
|
+
raise
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def after(_ctx, result)
|
|
170
|
+
AuditTrail.write(event: "finish", ok: result.ok?)
|
|
171
|
+
result
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class ProcessPayment < Interceptors::UseCase
|
|
176
|
+
use AuditInterceptor.new
|
|
177
|
+
|
|
178
|
+
# ...
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Checklist for custom interceptors:
|
|
183
|
+
|
|
184
|
+
1. Subclass `Interceptors::Interceptor` (or include behavior manually) and implement whichever hooks you need.
|
|
185
|
+
2. Ensure `around` always yields or returns an `Interceptors::Result` to keep the pipeline consistent.
|
|
186
|
+
3. Register the interceptor with `use` on your use case, or reuse it across multiple use cases.
|
|
187
|
+
|
|
127
188
|
For Rails controllers, include the responder helper:
|
|
128
189
|
|
|
129
190
|
```ruby
|
|
@@ -2,116 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Interceptors
|
|
4
4
|
class UseCase
|
|
5
|
-
|
|
6
|
-
def inherited(subclass)
|
|
7
|
-
super
|
|
8
|
-
subclass.instance_variable_set(:@interceptors, interceptors.dup)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def interceptors
|
|
12
|
-
@interceptors ||= []
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def use(interceptor)
|
|
16
|
-
interceptors << interceptor
|
|
17
|
-
self
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def call(input = {}, **kwargs)
|
|
21
|
-
new.call(input, **kwargs)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def call(input = {}, **kwargs)
|
|
26
|
-
ctx = build_context(input, **kwargs)
|
|
27
|
-
|
|
28
|
-
instrument(event_name("start"), name: self.class.name, ctx: ctx)
|
|
29
|
-
|
|
30
|
-
result = pipeline.call(ctx) { |context| normalize_result(execute(context)) }
|
|
31
|
-
result = normalize_result(result)
|
|
32
|
-
|
|
33
|
-
instrument(event_name("finish"),
|
|
34
|
-
name: self.class.name,
|
|
35
|
-
ctx: ctx,
|
|
36
|
-
ok: result.ok?,
|
|
37
|
-
error: result.error&.message)
|
|
38
|
-
|
|
39
|
-
result
|
|
40
|
-
rescue AppError => e
|
|
41
|
-
instrument(event_name("error"),
|
|
42
|
-
name: self.class.name,
|
|
43
|
-
ctx: ctx,
|
|
44
|
-
code: e.code,
|
|
45
|
-
message: e.message)
|
|
46
|
-
|
|
47
|
-
Result.err(e, meta: base_meta)
|
|
48
|
-
rescue StandardError => e
|
|
49
|
-
instrument(event_name("error"),
|
|
50
|
-
name: self.class.name,
|
|
51
|
-
ctx: ctx,
|
|
52
|
-
code: "unhandled_exception",
|
|
53
|
-
message: e.message,
|
|
54
|
-
error_class: e.class.name)
|
|
55
|
-
|
|
56
|
-
err = AppError.new("Unhandled exception",
|
|
57
|
-
code: "unhandled_exception",
|
|
58
|
-
http_status: 500,
|
|
59
|
-
details: { cause: e.class.name })
|
|
60
|
-
Result.err(err, meta: base_meta.merge(error_class: e.class.name))
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
def execute(_ctx)
|
|
66
|
-
raise NotImplementedError, "#{self.class} must implement #execute"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def normalize_result(result)
|
|
70
|
-
case result
|
|
71
|
-
when Result
|
|
72
|
-
result
|
|
73
|
-
when nil
|
|
74
|
-
Result.ok
|
|
75
|
-
else
|
|
76
|
-
Result.ok(result)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def base_meta
|
|
81
|
-
{ use_case: self.class.name }
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def build_context(input, **kwargs)
|
|
85
|
-
ctx = default_context.merge(normalize_input(input))
|
|
86
|
-
ctx.merge!(kwargs) unless kwargs.empty?
|
|
87
|
-
ctx.with_indifferent_access
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def normalize_input(input)
|
|
91
|
-
return input if input.is_a?(Hash)
|
|
92
|
-
return input.to_h if input.respond_to?(:to_h)
|
|
93
|
-
|
|
94
|
-
raise ArgumentError, "use case input must be a Hash or respond to #to_h (got #{input.class})"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def default_context
|
|
98
|
-
{}
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def pipeline
|
|
102
|
-
Pipeline.new(self.class.interceptors)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def notification_namespace
|
|
106
|
-
Interceptors.configuration.notification_namespace
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def instrument(event_name, payload, &block)
|
|
110
|
-
Interceptors.instrument(event_name, payload, &block)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def event_name(suffix)
|
|
114
|
-
"#{notification_namespace}.#{suffix}"
|
|
115
|
-
end
|
|
5
|
+
include UseCaseCore
|
|
116
6
|
end
|
|
117
7
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interceptors
|
|
4
|
+
module UseCaseCore
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
base.include(InstanceMethods)
|
|
8
|
+
base.instance_variable_set(:@interceptors, [])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module ClassMethods
|
|
12
|
+
def interceptors
|
|
13
|
+
@interceptors ||= []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def use(interceptor)
|
|
17
|
+
interceptors << interceptor
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(input = {}, **kwargs)
|
|
22
|
+
new.call(input, **kwargs)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inherited(subclass)
|
|
26
|
+
super
|
|
27
|
+
subclass.instance_variable_set(:@interceptors, interceptors.dup)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module InstanceMethods
|
|
32
|
+
def call(input = {}, **kwargs)
|
|
33
|
+
ctx = build_context(input, **kwargs)
|
|
34
|
+
|
|
35
|
+
instrument(event_name("start"), name: self.class.name, ctx: ctx)
|
|
36
|
+
|
|
37
|
+
result = pipeline.call(ctx) { |context| normalize_result(execute(context)) }
|
|
38
|
+
result = normalize_result(result)
|
|
39
|
+
|
|
40
|
+
instrument(event_name("finish"),
|
|
41
|
+
name: self.class.name,
|
|
42
|
+
ctx: ctx,
|
|
43
|
+
ok: result.ok?,
|
|
44
|
+
error: result.error&.message)
|
|
45
|
+
|
|
46
|
+
result
|
|
47
|
+
rescue AppError => e
|
|
48
|
+
instrument(event_name("error"),
|
|
49
|
+
name: self.class.name,
|
|
50
|
+
ctx: ctx,
|
|
51
|
+
code: e.code,
|
|
52
|
+
message: e.message)
|
|
53
|
+
|
|
54
|
+
Result.err(e, meta: base_meta)
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
instrument(event_name("error"),
|
|
57
|
+
name: self.class.name,
|
|
58
|
+
ctx: ctx,
|
|
59
|
+
code: "unhandled_exception",
|
|
60
|
+
message: e.message,
|
|
61
|
+
error_class: e.class.name)
|
|
62
|
+
|
|
63
|
+
err = AppError.new("Unhandled exception",
|
|
64
|
+
code: "unhandled_exception",
|
|
65
|
+
http_status: 500,
|
|
66
|
+
details: { cause: e.class.name })
|
|
67
|
+
Result.err(err, meta: base_meta.merge(error_class: e.class.name))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def execute(_ctx)
|
|
73
|
+
raise NotImplementedError, "#{self.class} must implement #execute"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def default_context
|
|
77
|
+
{}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalize_result(result)
|
|
81
|
+
case result
|
|
82
|
+
when Result
|
|
83
|
+
result
|
|
84
|
+
when nil
|
|
85
|
+
Result.ok
|
|
86
|
+
else
|
|
87
|
+
Result.ok(result)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def base_meta
|
|
92
|
+
{ use_case: self.class.name }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_context(input, **kwargs)
|
|
96
|
+
ctx = default_context.merge(normalize_input(input))
|
|
97
|
+
ctx.merge!(kwargs) unless kwargs.empty?
|
|
98
|
+
ctx.with_indifferent_access
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def normalize_input(input)
|
|
102
|
+
return input if input.is_a?(Hash)
|
|
103
|
+
return input.to_h if input.respond_to?(:to_h)
|
|
104
|
+
|
|
105
|
+
raise ArgumentError, "use case input must be a Hash or respond to #to_h (got #{input.class})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def pipeline
|
|
109
|
+
Pipeline.new(self.class.interceptors)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def notification_namespace
|
|
113
|
+
Interceptors.configuration.notification_namespace
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def instrument(event_name, payload, &block)
|
|
117
|
+
Interceptors.instrument(event_name, payload, &block)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def event_name(suffix)
|
|
121
|
+
"#{notification_namespace}.#{suffix}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/interceptors/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: interceptors
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Laerti papa
|
|
@@ -78,6 +78,8 @@ files:
|
|
|
78
78
|
- lib/interceptors/timeout_interceptor.rb
|
|
79
79
|
- lib/interceptors/transaction_interceptor.rb
|
|
80
80
|
- lib/interceptors/use_case.rb
|
|
81
|
+
- lib/interceptors/use_case_core.rb
|
|
82
|
+
- lib/interceptors/use_case_mixin.rb
|
|
81
83
|
- lib/interceptors/validation_error.rb
|
|
82
84
|
- lib/interceptors/validation_interceptor.rb
|
|
83
85
|
- lib/interceptors/version.rb
|