trailblazer-context 0.1.2 → 0.3.1

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.
@@ -1,5 +1,5 @@
1
1
  module Trailblazer
2
- class Context
3
- VERSION = "0.1.2"
2
+ module Context
3
+ VERSION = "0.3.1"
4
4
  end
5
5
  end
@@ -1,22 +1,5 @@
1
1
  module Trailblazer
2
- # @note This might go to trailblazer-args along with `Context` at some point.
3
- def self.Option(proc)
4
- Option.build(Option, proc)
5
- end
6
-
7
2
  class Option
8
- # Generic builder for a callable "option".
9
- # @param call_implementation [Class, Module] implements the process of calling the proc
10
- # while passing arguments/options to it in a specific style (e.g. kw args, step interface).
11
- # @return [Proc] when called, this proc will evaluate its option (at run-time).
12
- def self.build(call_implementation, proc)
13
- if proc.is_a? Symbol
14
- ->(*args, &block) { call_implementation.evaluate_method(proc, *args, &block) }
15
- else
16
- ->(*args, &block) { call_implementation.evaluate_callable(proc, *args, &block) }
17
- end
18
- end
19
-
20
3
  # A call implementation invoking `proc.(*args)` and plainly forwarding all arguments.
21
4
  # Override this for your own step strategy (see KW#call!).
22
5
  # @private
@@ -27,16 +10,28 @@ module Trailblazer
27
10
  # Note that both #evaluate_callable and #evaluate_method drop most of the args.
28
11
  # If you need those, override this class.
29
12
  # @private
30
- def self.evaluate_callable(proc, *args, **flow_options, &block)
13
+ def self.evaluate_callable(proc, *args, **, &block)
31
14
  call!(proc, *args, &block)
32
15
  end
33
16
 
34
17
  # Make the context's instance method a "lambda" and reuse #call!.
35
18
  # @private
36
- def self.evaluate_method(proc, *args, exec_context:raise("No :exec_context given."), **flow_options, &block)
19
+ def self.evaluate_method(proc, *args, exec_context: raise("No :exec_context given."), **, &block)
37
20
  call!(exec_context.method(proc), *args, &block)
38
21
  end
39
22
 
23
+ # Generic builder for a callable "option".
24
+ # @param call_implementation [Class, Module] implements the process of calling the proc
25
+ # while passing arguments/options to it in a specific style (e.g. kw args, step interface).
26
+ # @return [Proc] when called, this proc will evaluate its option (at run-time).
27
+ def self.build(call_implementation, proc)
28
+ if proc.is_a? Symbol
29
+ ->(*args, &block) { call_implementation.evaluate_method(proc, *args, &block) }
30
+ else
31
+ ->(*args, &block) { call_implementation.evaluate_callable(proc, *args, &block) }
32
+ end
33
+ end
34
+
40
35
  # Returns a {Proc} that, when called, invokes the `proc` argument with keyword arguments.
41
36
  # This is known as "step (call) interface".
42
37
  #
@@ -75,4 +70,8 @@ module Trailblazer
75
70
  end
76
71
  end
77
72
  end
73
+ # @note This might go to trailblazer-args along with `Context` at some point.
74
+ def self.Option(proc)
75
+ Option.build(Option, proc)
76
+ end
78
77
  end
@@ -0,0 +1,32 @@
1
+ require 'test_helper'
2
+ require 'benchmark/ips'
3
+
4
+ BenchmarkRepresenter = Struct.new(:benchmark, :times_slower)
5
+
6
+ Minitest::Spec.class_eval do
7
+ def benchmark_ips(records, time: 1, warmup: 1)
8
+ base = records[:base]
9
+ target = records[:target]
10
+
11
+ benchmark = Benchmark.ips do |x|
12
+ x.config(time: time, warmup: warmup)
13
+
14
+ x.report(base[:label], &base[:block])
15
+ x.report(target[:label], &target[:block])
16
+
17
+ x.compare!
18
+ end
19
+
20
+ times_slower = benchmark.data[0][:ips] / benchmark.data[1][:ips]
21
+ BenchmarkRepresenter.new(benchmark, times_slower)
22
+ end
23
+
24
+ def assert_times_slower(result, threshold)
25
+ base = result.benchmark.data[0]
26
+ target = result.benchmark.data[1]
27
+
28
+ msg = "Expected #{target[:name]} to be slower by at most #{threshold} times than #{base[:name]}, but got #{result.times_slower}"
29
+
30
+ assert result.times_slower < threshold, msg
31
+ end
32
+ end
@@ -0,0 +1,89 @@
1
+ require_relative "benchmark_helper"
2
+
3
+ describe "Context::IndifferentAccess Performance" do
4
+ wrapped_options = { model: Object, policy: Hash, representer: String }
5
+ mutable_options = { write: String, read: Integer, delete: Float, merge: Symbol }
6
+ context_options = {
7
+ container_class: Trailblazer::Context::Container,
8
+ replica_class: Trailblazer::Context::Store::IndifferentAccess,
9
+ }
10
+
11
+ default_hash = Hash(**wrapped_options, **mutable_options)
12
+ indifferent_hash = Trailblazer::Context.build(wrapped_options, mutable_options, context_options)
13
+
14
+ it "initialize" do
15
+ result = benchmark_ips(
16
+ base: { label: :initialize_default_hash, block: ->{
17
+ Hash(**wrapped_options, **mutable_options)
18
+ }},
19
+ target: { label: :initialize_indifferent_hash, block: ->{
20
+ Trailblazer::Context.build(wrapped_options, mutable_options, context_options)
21
+ }},
22
+ )
23
+
24
+ assert_times_slower result, 3
25
+ end
26
+
27
+ it "read" do
28
+ result = benchmark_ips(
29
+ base: { label: :read_from_default_hash, block: ->{ default_hash[:read] } },
30
+ target: { label: :read_from_indifferent_hash, block: ->{ indifferent_hash[:read] } },
31
+ )
32
+
33
+ assert_times_slower result, 1.4
34
+ end
35
+
36
+ it "unknown read" do
37
+ result = benchmark_ips(
38
+ base: { label: :unknown_read_from_default_hash, block: ->{ default_hash[:unknown] } },
39
+ target: { label: :unknown_read_from_indifferent_hash, block: ->{ indifferent_hash[:unknown] } },
40
+ )
41
+
42
+ assert_times_slower result, 3.5
43
+ end
44
+
45
+ it "write" do
46
+ result = benchmark_ips(
47
+ base: { label: :write_to_default_hash, block: ->{ default_hash[:write] = "" } },
48
+ target: { label: :write_to_indifferent_hash, block: ->{ indifferent_hash[:write] = "SKU-1" } },
49
+ )
50
+
51
+ assert_times_slower result, 2.3
52
+ end
53
+
54
+ it "delete" do
55
+ result = benchmark_ips(
56
+ base: { label: :delete_from_default_hash, block: ->{ default_hash.delete(:delete) } },
57
+ target: { label: :delete_from_indifferent_hash, block: ->{ indifferent_hash.delete(:delete) } },
58
+ )
59
+
60
+ assert_times_slower result, 2.4
61
+ end
62
+
63
+ it "merge" do
64
+ result = benchmark_ips(
65
+ base: { label: :merge_from_default_hash, block: ->{ default_hash.merge(merge: :object_id) } },
66
+ target: { label: :merge_from_indifferent_hash, block: ->{ indifferent_hash.merge(merge: :object_id) } },
67
+ )
68
+
69
+ assert_times_slower result, 5.55
70
+ end
71
+
72
+ it "to_hash" do
73
+ result = benchmark_ips(
74
+ base: { label: :default_to_hash, block: ->{ default_hash.to_hash } },
75
+ target: { label: :indifferent_to_hash, block: ->{ indifferent_hash.to_hash } },
76
+ )
77
+
78
+ assert_times_slower result, 1.3
79
+ end
80
+
81
+ it "decompose" do
82
+ result = benchmark_ips(
83
+ base: { label: :dup_default_hash, block: ->{ default_hash.to_hash } },
84
+ target: { label: :decompose, block: ->{ indifferent_hash.decompose } },
85
+ )
86
+
87
+ assert_times_slower result, 1.55
88
+ end
89
+ end
@@ -0,0 +1,73 @@
1
+ require_relative "benchmark_helper"
2
+
3
+ describe "Context::Aliasing Performance" do
4
+ wrapped_options = { model: Object, policy: Hash, representer: String }
5
+ mutable_options = { write: String, read: Integer, delete: Float, merge: Symbol }
6
+
7
+ context_options = {
8
+ container_class: Trailblazer::Context::Container::WithAliases,
9
+ replica_class: Trailblazer::Context::Store::IndifferentAccess,
10
+ aliases: { read: :reader }
11
+ }
12
+
13
+ default_hash = Hash(**wrapped_options, **mutable_options)
14
+ aliased_hash = Trailblazer::Context.build(wrapped_options, mutable_options, context_options)
15
+
16
+ it "initialize" do
17
+ result = benchmark_ips(
18
+ base: { label: :initialize_default_hash, block: ->{
19
+ Hash(**wrapped_options, **mutable_options)
20
+ }},
21
+ target: { label: :initialize_aliased_hash, block: ->{
22
+ Trailblazer::Context.build(wrapped_options, mutable_options, context_options)
23
+ }},
24
+ )
25
+
26
+ assert_times_slower result, 8
27
+ end
28
+
29
+ it "write" do
30
+ result = benchmark_ips(
31
+ base: { label: :write_to_default_hash, block: ->{ default_hash[:write] = "" } },
32
+ target: { label: :write_to_aliased_hash, block: ->{ aliased_hash[:write] = "" } },
33
+ )
34
+
35
+ assert_times_slower result, 3.66
36
+ end
37
+
38
+ it "read" do
39
+ result = benchmark_ips(
40
+ base: { label: :read_from_default_hash, block: ->{ default_hash[:read] } },
41
+ target: { label: :read_from_aliased_hash, block: ->{ aliased_hash[:reader] } },
42
+ )
43
+
44
+ assert_times_slower result, 1.5
45
+ end
46
+
47
+ it "delete" do
48
+ result = benchmark_ips(
49
+ base: { label: :delete_from_default_hash, block: ->{ default_hash.delete(:delete) } },
50
+ target: { label: :delete_from_aliased_hash, block: ->{ aliased_hash.delete(:delete) } },
51
+ )
52
+
53
+ assert_times_slower result, 4
54
+ end
55
+
56
+ it "merge" do
57
+ result = benchmark_ips(
58
+ base: { label: :merge_from_default_hash, block: ->{ default_hash.merge(merge: :object_id) } },
59
+ target: { label: :merge_from_aliased_hash, block: ->{ aliased_hash.merge(merge: :object_id) } },
60
+ )
61
+
62
+ assert_times_slower result, 8.5
63
+ end
64
+
65
+ it "to_hash" do
66
+ result = benchmark_ips(
67
+ base: { label: :default_to_hash, block: ->{ default_hash.to_hash } },
68
+ target: { label: :aliased_to_hash, block: ->{ aliased_hash.to_hash } },
69
+ )
70
+
71
+ assert_times_slower result, 1.5
72
+ end
73
+ end
@@ -0,0 +1,323 @@
1
+ require "test_helper"
2
+ require "trailblazer/container_chain"
3
+
4
+ class ArgsTest < Minitest::Spec
5
+ let(:immutable) { {repository: "User"} }
6
+
7
+ let(:ctx) { Trailblazer::Context(immutable) }
8
+
9
+ it do
10
+ ctx = Trailblazer::Context(immutable)
11
+
12
+ # it { }
13
+ #-
14
+ # options[] and options[]=
15
+ ctx[:model] = Module
16
+ ctx[:contract] = Integer
17
+ _(ctx[:model]) .must_equal Module
18
+ _(ctx[:contract]).must_equal Integer
19
+
20
+ # it { }
21
+ _(immutable.inspect).must_equal %({:repository=>\"User\"})
22
+ _(ctx.inspect).must_equal %{#<Trailblazer::Context::Container wrapped_options={:repository=>\"User\"} mutable_options={:model=>Module, :contract=>Integer}>}
23
+ end
24
+
25
+ it "allows false/nil values" do
26
+ ctx["x"] = false
27
+ _(ctx["x"]).must_equal false
28
+
29
+ ctx["x"] = nil
30
+ assert_nil ctx["x"]
31
+ end
32
+
33
+ #- #to_hash
34
+ it do
35
+ ctx = Trailblazer::Context(immutable)
36
+
37
+ # it { }
38
+ _(ctx.to_hash).must_equal(repository: "User")
39
+
40
+ # last added has precedence.
41
+ # only symbol keys.
42
+ # it { }
43
+ ctx[:a] = Symbol
44
+ ctx["a"] = String
45
+
46
+ _(ctx.to_hash).must_equal(repository: "User", a: String)
47
+ end
48
+
49
+ describe "#merge" do
50
+ it do
51
+ ctx = Trailblazer::Context(immutable)
52
+
53
+ merged = ctx.merge(current_user: Module)
54
+
55
+ _(merged.class).must_equal(Trailblazer::Context::Container)
56
+ _(merged.to_hash).must_equal(repository: "User", current_user: Module)
57
+ _(ctx.to_hash).must_equal(repository: "User")
58
+ end
59
+ end
60
+
61
+ describe "Enumerable behaviour" do
62
+ it { _(ctx.each.to_a).must_equal [[:repository, "User"]] }
63
+ it { _(ctx.find{ |k, _| k == :repository }).must_equal [:repository, "User"] }
64
+ it { _(ctx.inject([]){ |r, (k, _)| r << k}).must_equal [:repository] }
65
+ end
66
+
67
+ #- #decompose
68
+ it do
69
+ immutable = {repository: "User", model: Module, current_user: Class}
70
+ mutable = {error: RuntimeError}
71
+
72
+ _([immutable, mutable]).must_equal Trailblazer::Context(immutable, mutable).decompose
73
+ end
74
+ end
75
+
76
+ class ContextWithIndifferentAccessTest < Minitest::Spec
77
+ it do
78
+ flow_options = {
79
+ context_options: {
80
+ container_class: Trailblazer::Context::Container,
81
+ replica_class: Trailblazer::Context::Store::IndifferentAccess
82
+ }
83
+ }
84
+
85
+ circuit_options = {}
86
+
87
+ immutable = {model: Object, "policy" => Hash}
88
+
89
+ ctx = Trailblazer::Context.for_circuit(immutable, {}, [immutable, flow_options], **circuit_options)
90
+
91
+ _(ctx[:model]).must_equal Object
92
+ _(ctx["model"]).must_equal Object
93
+ _(ctx[:policy]).must_equal Hash
94
+ _(ctx["policy"]).must_equal Hash
95
+
96
+ ctx["contract.default"] = Module
97
+ _(ctx["contract.default"]).must_equal Module
98
+ _(ctx[:"contract.default"]).must_equal Module
99
+
100
+ # key?
101
+ _(ctx.key?("____contract.default")).must_equal false
102
+ _(ctx.key?("contract.default")).must_equal true
103
+ _(ctx.key?(:"contract.default")).must_equal true
104
+
105
+ # context in context
106
+ ctx2 = Trailblazer::Context.for_circuit(ctx, {}, [ctx, flow_options], **circuit_options)
107
+
108
+ _(ctx2[:model]).must_equal Object
109
+ _(ctx2["model"]).must_equal Object
110
+
111
+ ctx2["contract.default"] = Class
112
+ _(ctx2["contract.default"]).must_equal Class
113
+ _(ctx2[:"contract.default"]).must_equal Class
114
+
115
+ # key?
116
+ _(ctx2.key?("contract.default")).must_equal true
117
+ _(ctx2.key?(:"contract.default")).must_equal true
118
+ _(ctx2.key?("model")).must_equal true
119
+
120
+ # wrapped ctx doesn't change
121
+ _(ctx["contract.default"]).must_equal Module
122
+ _(ctx[:"contract.default"]).must_equal Module
123
+
124
+ # delete
125
+ ctx[:model] = Object
126
+ ctx.delete 'model'
127
+
128
+ _(ctx.key?(:model)).must_equal false
129
+ _(ctx.key?("model")).must_equal false
130
+
131
+ ctx3 = ctx.merge("result" => false)
132
+
133
+ _(ctx3["contract.default"]).must_equal Module
134
+ _(ctx3[:"contract.default"]).must_equal Module
135
+ _(ctx3["result"]).must_equal false
136
+ _(ctx3[:result]).must_equal false
137
+ _(ctx3.key?("result")).must_equal true
138
+ _(ctx3.key?(:result)).must_equal true
139
+ end
140
+
141
+ it "Aliasable" do
142
+ flow_options = {
143
+ context_options: {
144
+ container_class: Trailblazer::Context::Container::WithAliases,
145
+ replica_class: Trailblazer::Context::Store::IndifferentAccess,
146
+ aliases: { "contract.default" => :contract, "result.default"=>:result, "trace.stack" => :stack }
147
+ }
148
+ }
149
+
150
+ circuit_options = {}
151
+
152
+ immutable = {model: Object, "policy" => Hash}
153
+
154
+ ctx = Trailblazer::Context.for_circuit(immutable, {}, [immutable, flow_options], **circuit_options)
155
+ _(ctx.class).must_equal(Trailblazer::Context::Container::WithAliases)
156
+
157
+ _(ctx.inspect).must_equal %{#<Trailblazer::Context::Container::WithAliases wrapped_options={:model=>Object, \"policy\"=>Hash} mutable_options={} aliases={\"contract.default\"=>:contract, \"result.default\"=>:result, \"trace.stack\"=>:stack}>}
158
+
159
+ _(ctx.to_hash).must_equal(:model=>Object, :policy=>Hash)
160
+
161
+ _(ctx[:model]).must_equal Object
162
+ _(ctx["model"]).must_equal Object
163
+ _(ctx[:policy]).must_equal Hash
164
+ _(ctx["policy"]).must_equal Hash
165
+
166
+ ctx["contract.default"] = Module
167
+ _(ctx["contract.default"]).must_equal Module
168
+ _(ctx[:"contract.default"]).must_equal Module
169
+
170
+ # alias
171
+ assert_nil ctx[:result]
172
+ assert_nil ctx["result"]
173
+
174
+ _(ctx[:contract]).must_equal Module
175
+ _(ctx['contract']).must_equal Module
176
+
177
+ assert_nil ctx[:stack]
178
+ assert_nil ctx['stack']
179
+
180
+ # Set an aliased property via setter
181
+ ctx["trace.stack"] = Object
182
+ _(ctx[:stack]).must_equal Object
183
+ _(ctx["stack"]).must_equal Object
184
+ _(ctx["trace.stack"]).must_equal Object
185
+
186
+ # Set an aliased property with merge
187
+ ctx["trace.stack"] = String
188
+ merged = ctx.merge(stack: Integer)
189
+
190
+ _(merged.class).must_equal(Trailblazer::Context::Container::WithAliases)
191
+ _(merged.to_hash).must_equal(:model=>Object, :policy=>Hash, :contract=>Module, :"contract.default"=>Module, :stack=>Integer, :"trace.stack"=>Integer)
192
+
193
+ # key?
194
+ _(ctx.key?("____contract.default")).must_equal false
195
+ _(ctx.key?("contract.default")).must_equal true
196
+ _(ctx.key?(:"contract.default")).must_equal true
197
+ _(ctx.key?(:contract)).must_equal true
198
+ _(ctx.key?(:result)).must_equal false
199
+ _(ctx.key?(:stack)).must_equal true
200
+ _(ctx.key?("trace.stack")).must_equal true
201
+ _(ctx.key?(:"trace.stack")).must_equal true
202
+
203
+ # delete
204
+ ctx[:result] = Object
205
+ ctx.delete :result
206
+
207
+ _(ctx.key?(:result)).must_equal false
208
+ _(ctx.key?("result")).must_equal false
209
+
210
+ _(ctx.key?(:"result.default")).must_equal false
211
+ _(ctx.key?("result.default")).must_equal false
212
+
213
+
214
+ # to_hash
215
+ _(ctx.to_hash).must_equal(:model=>Object, :policy=>Hash, :contract=>Module, :"contract.default"=>Module, :stack=>String, :"trace.stack"=>String)
216
+
217
+ # context in context
218
+ ctx2 = Trailblazer::Context.for_circuit(ctx, {}, [ctx, flow_options], **circuit_options)
219
+
220
+ _(ctx2.key?("____contract.default")).must_equal false
221
+ _(ctx2.key?("contract.default")).must_equal true
222
+ _(ctx2.key?(:"contract.default")).must_equal true
223
+ _(ctx2.key?(:contract)).must_equal true
224
+ _(ctx2.key?(:result)).must_equal false
225
+ _(ctx2.key?("result.default")).must_equal false
226
+ _(ctx2.key?(:stack)).must_equal true
227
+ _(ctx2.key?("trace.stack")).must_equal true
228
+ _(ctx2.key?(:"trace.stack")).must_equal true
229
+
230
+ # Set aliased in new context via setter
231
+ ctx2["result.default"] = Class
232
+
233
+ _(ctx2[:result]).must_equal Class
234
+ _(ctx2[:"result.default"]).must_equal Class
235
+
236
+ _(ctx2.key?("result.default")).must_equal true
237
+ _(ctx2.key?(:"result.default")).must_equal true
238
+ _(ctx2.key?(:result)).must_equal true
239
+
240
+ # todo: TEST flow_options={context_class: SomethingElse}
241
+ end
242
+
243
+ it ".build accepts custom container class" do
244
+ MyContainer = Class.new(Trailblazer::Context::Container) do
245
+ def inspect
246
+ %{#<MyContainer wrapped=#{@wrapped_options} mutable=#{@mutable_options}>}
247
+ end
248
+ end
249
+
250
+ immutable = { model: Object }
251
+ options = { container_class: MyContainer, replica_class: Trailblazer::Context::Store::IndifferentAccess }
252
+
253
+ ctx = Trailblazer::Context.build(immutable, {}, options)
254
+ _(ctx.class).must_equal(MyContainer)
255
+ _(ctx.inspect).must_equal("#<MyContainer wrapped=#{immutable} mutable={}>")
256
+
257
+ _(ctx.to_hash).must_equal({ model: Object })
258
+
259
+ ctx[:integer] = Integer
260
+ _(ctx.to_hash).must_equal({ model: Object, integer: Integer })
261
+
262
+ ctx2 = ctx.merge(float: Float)
263
+ _(ctx2.class).must_equal(MyContainer)
264
+
265
+ _(ctx2.to_hash).must_equal({ model: Object, integer: Integer, float: Float })
266
+ end
267
+
268
+ it ".build accepts custom replica class (For example, To opt out from indifferent access)" do
269
+ MyReplica = Class.new(Hash) do
270
+ def initialize(*containers)
271
+ containers.each do |container|
272
+ container.each{ |key, value| self[key] = value }
273
+ end
274
+ end
275
+ end
276
+
277
+ immutable = { model: Object }
278
+ options = { container_class: Trailblazer::Context::Container, replica_class: MyReplica }
279
+
280
+ ctx = Trailblazer::Context.build(immutable, {}, options)
281
+ ctx[:integer] = Integer
282
+
283
+ _(ctx[:integer]).must_equal(Integer)
284
+ _(ctx['integer']).must_be_nil
285
+ end
286
+
287
+ it "Context() provides default args" do
288
+ immutable = {model: Object, "policy.default" => Hash}
289
+ options = {
290
+ container_class: Trailblazer::Context::Container::WithAliases,
291
+ aliases: { "policy.default" => :policy }
292
+ }
293
+
294
+ ctx = Trailblazer::Context(immutable, {}, options)
295
+
296
+ _(ctx[:model]).must_equal Object
297
+ _(ctx["model"]).must_equal Object
298
+ _(ctx[:policy]).must_equal Hash
299
+
300
+ ctx2 = ctx.merge(result: :success)
301
+
302
+
303
+ _(ctx2[:model]).must_equal Object
304
+ _(ctx2["model"]).must_equal Object
305
+ _(ctx2[:policy]).must_equal Hash
306
+ _(ctx2[:result]).must_equal :success
307
+ end
308
+
309
+ it "Context() throws RuntimeError if aliases are passed but container_class doesn't support it" do
310
+ immutable = {model: Object, "policy.default" => Hash}
311
+ options = {
312
+ aliases: { "policy.default" => :policy }
313
+ }
314
+
315
+ exception = assert_raises Trailblazer::Context::Container::UseWithAliases do
316
+ Trailblazer::Context(immutable, {}, options)
317
+ end
318
+
319
+ _(exception.message).must_equal %{Pass `Trailblazer::Context::Container::WithAliases` as `container_class` while defining `aliases`}
320
+ end
321
+ end
322
+
323
+ # TODO: test overriding Context.implementation.