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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4c31ab76d3b09a2a51c674ceb27aaaaf82cda31e6267b56be86b67ee77a9155
4
- data.tar.gz: f1a4039246aa384126f1391faa4e879d3f4a8d3b98900f95f88e5b5bbe7aa60e
3
+ metadata.gz: 6f06c031c777b039546473d09618c1b7c76c1709879e4c9f516384811599a770
4
+ data.tar.gz: b7bc835285431b9784a5d4c492f5c1b3851e3638d0856be5e146412b68d6d367
5
5
  SHA512:
6
- metadata.gz: 8a5edaa5997b949864f547e50d2226b75af99fd9bc214cbff7b8e4eec4f23e6546e104edc46978aed03d6ae0b860d1955f2125a92b5b7c2a1105d9ceef3ab9fb
7
- data.tar.gz: 942e90230e3bd6ecbff48ab19d2bbb81543cf09e805c3dec779731fbab1515537270d522172a97c08c03e7efb9c69031b9920245272beb1c89844e3458fc5842
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
- class << self
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interceptors
4
+ module UseCaseMixin
5
+ def self.included(base)
6
+ base.include(UseCaseCore)
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Interceptors
4
- VERSION = "1.0.2"
4
+ VERSION = "1.0.3"
5
5
  end
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.2
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