trailblazer-context 0.1.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.