trailblazer-context 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ require "hashie"
2
+
3
+ module Trailblazer
4
+ module Context
5
+ module Store
6
+ # Simple yet indifferently accessible hash store, used as replica in Context::Container.
7
+ # It maintains cache for multiple hashes (wrapped_options, mutable_options etc).
8
+ class IndifferentAccess < Hash
9
+ include Hashie::Extensions::IndifferentAccess
10
+
11
+ def initialize(hashes)
12
+ hashes.each do |hash|
13
+ hash.each do |key, value|
14
+ self[key] = value
15
+ end
16
+ end
17
+ end
18
+
19
+ # Override of Hashie::Extensions::IndifferentAccess#indifferent_value
20
+ # to not do deep indifferent access conversion.
21
+ # DISCUSS: Should we make this configurable ?
22
+ def indifferent_value(value)
23
+ value
24
+ end
25
+
26
+ # Override of Hashie::Extensions::IndifferentAccess#convert_key
27
+ # to store keys as Symbol by default instead of String.
28
+ # Why ? We need to pass `ctx` as keyword arguments most of the time.
29
+ def convert_key(key)
30
+ return key if Symbol === key
31
+ String === key ? key.to_sym : key
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,5 @@
1
1
  module Trailblazer
2
- class Context
3
- VERSION = "0.2.0"
2
+ module Context
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
@@ -1,78 +1,47 @@
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
23
- def self.call!(proc, *args, &block)
24
- proc.(*args, &block)
6
+ def self.call!(proc, *args, keyword_arguments: {}, **, &block)
7
+ # {**keyword_arguments} gets removed automatically if it's an empty hash.
8
+ # DISCUSS: is this a good practice?
9
+ proc.(*args, **keyword_arguments, &block)
25
10
  end
26
11
 
27
12
  # Note that both #evaluate_callable and #evaluate_method drop most of the args.
28
13
  # If you need those, override this class.
29
14
  # @private
30
- def self.evaluate_callable(proc, *args, **circuit_options, &block)
31
- call!(proc, *args, &block)
15
+ def self.evaluate_callable(proc, *args, **options, &block)
16
+ call!(proc, *args, **options, &block)
32
17
  end
33
18
 
34
19
  # Make the context's instance method a "lambda" and reuse #call!.
35
20
  # @private
36
- def self.evaluate_method(proc, *args, exec_context: raise("No :exec_context given."), **circuit_options, &block)
37
- call!(exec_context.method(proc), *args, &block)
21
+ def self.evaluate_method(proc, *args, exec_context: raise("No :exec_context given."), **options, &block)
22
+ call!(exec_context.method(proc), *args, **options, &block)
38
23
  end
39
24
 
40
- # Returns a {Proc} that, when called, invokes the `proc` argument with keyword arguments.
41
- # This is known as "step (call) interface".
42
- #
43
- # This is commonly used by `Operation::step` to wrap the argument and make it
44
- # callable in the circuit.
45
- #
46
- # my_proc = ->(options, **kws) { options["i got called"] = true }
47
- # task = Trailblazer::Option::KW(my_proc)
48
- # task.(options = {})
49
- # options["i got called"] #=> true
50
- #
51
- # Alternatively, you can pass a symbol and an `:exec_context`.
52
- #
53
- # my_proc = :some_method
54
- # task = Trailblazer::Option::KW(my_proc)
55
- #
56
- # class A
57
- # def some_method(options, **kws)
58
- # options["i got called"] = true
59
- # end
60
- # end
61
- #
62
- # task.(options = {}, exec_context: A.new)
63
- # options["i got called"] #=> true
64
- def self.KW(proc)
65
- Option.build(KW, proc)
25
+ # Generic builder for a callable "option".
26
+ # @param call_implementation [Class, Module] implements the process of calling the proc
27
+ # while passing arguments/options to it in a specific style (e.g. kw args, step interface).
28
+ # @return [Proc] when called, this proc will evaluate its option (at run-time).
29
+ def self.build(proc)
30
+ if proc.is_a? Symbol
31
+ ->(*args, **kws, &block) { Option.evaluate_method(proc, *args, **kws, &block) }
32
+ else
33
+ ->(*args, **kws, &block) {
34
+ Option.evaluate_callable(proc, *args, **kws, &block) }
35
+ end
66
36
  end
67
37
 
68
- # TODO: It would be cool if call! was typed and had `options SymbolizedHash` or something.
69
- class KW < Option
70
- # A different call implementation that calls `proc` with a "step interface".
71
- # your_code.(options, **options)
72
- # @private
73
- def self.call!(proc, options, *)
74
- proc.(options, **options.to_hash) # Step interface: (options, **)
75
- end
38
+ def self.KW(proc)
39
+ raise "The `Option::KW()` method has been removed in trailblazer-context-0.4.
40
+ Please use `Option(task, keyword_arguments: {...})` instead. Check https://trailblazer.to/2.1/docs/trailblazer.html#trailblazer-context-option"
76
41
  end
77
42
  end
43
+ # @note This might go to trailblazer-args along with `Context` at some point.
44
+ def self.Option(proc)
45
+ Option.build(proc)
46
+ end
78
47
  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
data/test/context_test.rb CHANGED
@@ -2,8 +2,6 @@ require "test_helper"
2
2
  require "trailblazer/container_chain"
3
3
 
4
4
  class ArgsTest < Minitest::Spec
5
- Context = Trailblazer::Context
6
-
7
5
  let(:immutable) { {repository: "User"} }
8
6
 
9
7
  let(:ctx) { Trailblazer::Context(immutable) }
@@ -16,16 +14,17 @@ class ArgsTest < Minitest::Spec
16
14
  # options[] and options[]=
17
15
  ctx[:model] = Module
18
16
  ctx[:contract] = Integer
19
- ctx[:model] .must_equal Module
20
- ctx[:contract].must_equal Integer
17
+ _(ctx[:model]) .must_equal Module
18
+ _(ctx[:contract]).must_equal Integer
21
19
 
22
20
  # it { }
23
- immutable.inspect.must_equal %({:repository=>\"User\"})
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}>}
24
23
  end
25
24
 
26
25
  it "allows false/nil values" do
27
26
  ctx["x"] = false
28
- ctx["x"].must_equal false
27
+ _(ctx["x"]).must_equal false
29
28
 
30
29
  ctx["x"] = nil
31
30
  assert_nil ctx["x"]
@@ -36,7 +35,7 @@ class ArgsTest < Minitest::Spec
36
35
  ctx = Trailblazer::Context(immutable)
37
36
 
38
37
  # it { }
39
- ctx.to_hash.must_equal(repository: "User")
38
+ _(ctx.to_hash).must_equal(repository: "User")
40
39
 
41
40
  # last added has precedence.
42
41
  # only symbol keys.
@@ -44,7 +43,7 @@ class ArgsTest < Minitest::Spec
44
43
  ctx[:a] = Symbol
45
44
  ctx["a"] = String
46
45
 
47
- ctx.to_hash.must_equal(repository: "User", a: String)
46
+ _(ctx.to_hash).must_equal(repository: "User", a: String)
48
47
  end
49
48
 
50
49
  describe "#merge" do
@@ -53,160 +52,271 @@ class ArgsTest < Minitest::Spec
53
52
 
54
53
  merged = ctx.merge(current_user: Module)
55
54
 
56
- merged.to_hash.must_equal(repository: "User", current_user: Module)
57
- ctx.to_hash.must_equal(repository: "User")
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
58
  end
59
59
  end
60
60
 
61
- #-
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
62
68
  it do
63
69
  immutable = {repository: "User", model: Module, current_user: Class}
70
+ mutable = {error: RuntimeError}
64
71
 
65
- Trailblazer::Context(immutable) do |_original, mutable|
66
- mutable
67
- end
72
+ _([immutable, mutable]).must_equal Trailblazer::Context(immutable, mutable).decompose
68
73
  end
69
74
  end
70
75
 
71
76
  class ContextWithIndifferentAccessTest < Minitest::Spec
72
77
  it do
73
- flow_options = {}
78
+ flow_options = {
79
+ context_options: {
80
+ container_class: Trailblazer::Context::Container,
81
+ replica_class: Trailblazer::Context::Store::IndifferentAccess
82
+ }
83
+ }
84
+
74
85
  circuit_options = {}
75
86
 
76
87
  immutable = {model: Object, "policy" => Hash}
77
88
 
78
- ctx = Trailblazer::Context.for_circuit(immutable, {}, [immutable, flow_options], circuit_options)
89
+ ctx = Trailblazer::Context.for_circuit(immutable, {}, [immutable, flow_options], **circuit_options)
79
90
 
80
- ctx[:model].must_equal Object
81
- ctx["model"].must_equal Object
82
- ctx[:policy].must_equal Hash
83
- ctx["policy"].must_equal Hash
91
+ _(ctx[:model]).must_equal Object
92
+ _(ctx["model"]).must_equal Object
93
+ _(ctx[:policy]).must_equal Hash
94
+ _(ctx["policy"]).must_equal Hash
84
95
 
85
96
  ctx["contract.default"] = Module
86
- ctx["contract.default"].must_equal Module
87
- ctx[:"contract.default"].must_equal Module
97
+ _(ctx["contract.default"]).must_equal Module
98
+ _(ctx[:"contract.default"]).must_equal Module
88
99
 
89
100
  # key?
90
- ctx.key?("____contract.default").must_equal false
91
- ctx.key?("contract.default").must_equal true
92
- ctx.key?(:"contract.default").must_equal true
101
+ _(ctx.key?("____contract.default")).must_equal false
102
+ _(ctx.key?("contract.default")).must_equal true
103
+ _(ctx.key?(:"contract.default")).must_equal true
93
104
 
94
105
  # context in context
95
- ctx2 = Trailblazer::Context.for_circuit(ctx, {}, [ctx, flow_options], circuit_options)
106
+ ctx2 = Trailblazer::Context.for_circuit(ctx, {}, [ctx, flow_options], **circuit_options)
96
107
 
97
- ctx2[:model].must_equal Object
98
- ctx2["model"].must_equal Object
108
+ _(ctx2[:model]).must_equal Object
109
+ _(ctx2["model"]).must_equal Object
99
110
 
100
111
  ctx2["contract.default"] = Class
101
- ctx2["contract.default"].must_equal Class
102
- ctx2[:"contract.default"].must_equal Class
112
+ _(ctx2["contract.default"]).must_equal Class
113
+ _(ctx2[:"contract.default"]).must_equal Class
103
114
 
104
115
  # key?
105
- ctx2.key?("contract.default").must_equal true
106
- ctx2.key?(:"contract.default").must_equal true
107
- ctx2.key?("model").must_equal true
116
+ _(ctx2.key?("contract.default")).must_equal true
117
+ _(ctx2.key?(:"contract.default")).must_equal true
118
+ _(ctx2.key?("model")).must_equal true
108
119
 
109
120
  # wrapped ctx doesn't change
110
- ctx["contract.default"].must_equal Module
111
- ctx[:"contract.default"].must_equal Module
121
+ _(ctx["contract.default"]).must_equal Module
122
+ _(ctx[:"contract.default"]).must_equal Module
112
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
113
130
 
114
131
  ctx3 = ctx.merge("result" => false)
115
132
 
116
- ctx3["contract.default"].must_equal Module
117
- ctx3[:"contract.default"].must_equal Module
118
- ctx3["result"].must_equal false
119
- ctx3[:result].must_equal false
120
- ctx3.key?("result").must_equal true
121
- ctx3.key?(:result).must_equal true
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
122
139
  end
123
140
 
124
141
  it "Aliasable" do
125
- flow_options = {context_alias: {"contract.default" => :contract, "result.default"=>:result, "trace.stack" => :stack}}
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
+
126
150
  circuit_options = {}
127
151
 
128
152
  immutable = {model: Object, "policy" => Hash}
129
153
 
130
- ctx = Trailblazer::Context.for_circuit(immutable, {}, [immutable, flow_options], circuit_options)
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)
131
160
 
132
- ctx[:model].must_equal Object
133
- ctx["model"].must_equal Object
134
- ctx[:policy].must_equal Hash
135
- ctx["policy"].must_equal Hash
161
+ _(ctx[:model]).must_equal Object
162
+ _(ctx["model"]).must_equal Object
163
+ _(ctx[:policy]).must_equal Hash
164
+ _(ctx["policy"]).must_equal Hash
136
165
 
137
166
  ctx["contract.default"] = Module
138
- ctx["contract.default"].must_equal Module
139
- ctx[:"contract.default"].must_equal Module
167
+ _(ctx["contract.default"]).must_equal Module
168
+ _(ctx[:"contract.default"]).must_equal Module
140
169
 
141
170
  # alias
142
- ctx[:result].must_equal nil
143
- ctx["result"].must_equal nil
171
+ assert_nil ctx[:result]
172
+ assert_nil ctx["result"]
144
173
 
145
- ctx[:contract].must_equal Module
174
+ _(ctx[:contract]).must_equal Module
175
+ _(ctx['contract']).must_equal Module
146
176
 
147
- ctx[:stack].must_equal nil
177
+ assert_nil ctx[:stack]
178
+ assert_nil ctx['stack']
148
179
 
149
180
  # Set an aliased property via setter
150
181
  ctx["trace.stack"] = Object
151
- ctx[:stack].must_equal Object
152
- ctx["trace.stack"].must_equal 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)
153
192
 
154
193
  # key?
155
- ctx.key?("____contract.default").must_equal false
156
- ctx.key?("contract.default").must_equal true
157
- ctx.key?(:"contract.default").must_equal true
158
- ctx.key?(:contract).must_equal true
159
- ctx.key?(:result).must_equal false
160
- ctx.key?(:stack).must_equal true
161
- ctx.key?("trace.stack").must_equal true
162
- ctx.key?(:"trace.stack").must_equal true
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
+
163
213
 
164
214
  # to_hash
165
- ctx.to_hash.must_equal(:model=>Object, :policy=>Hash, :"contract.default"=>Module, :"trace.stack"=>Object, :contract=>Module, :stack=>Object)
215
+ _(ctx.to_hash).must_equal(:model=>Object, :policy=>Hash, :contract=>Module, :"contract.default"=>Module, :stack=>String, :"trace.stack"=>String)
166
216
 
167
217
  # context in context
168
- ctx2 = Trailblazer::Context.for_circuit(ctx, {}, [ctx, flow_options], circuit_options)
169
-
170
- ctx2.key?("____contract.default").must_equal false
171
- ctx2.key?("contract.default").must_equal true
172
- ctx2.key?(:"contract.default").must_equal true
173
- ctx2.key?(:contract).must_equal true
174
- ctx2.key?(:result).must_equal false
175
- ctx2.key?("result.default").must_equal false
176
- ctx2.key?(:stack).must_equal true
177
- ctx2.key?("trace.stack").must_equal true
178
- ctx2.key?(:"trace.stack").must_equal true
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
179
229
 
180
230
  # Set aliased in new context via setter
181
231
  ctx2["result.default"] = Class
182
232
 
183
- ctx2[:result].must_equal Class
184
- ctx2[:"result.default"].must_equal Class
233
+ _(ctx2[:result]).must_equal Class
234
+ _(ctx2[:"result.default"]).must_equal Class
185
235
 
186
- ctx2.key?("result.default").must_equal true
187
- ctx2.key?(:"result.default").must_equal true
188
- ctx2.key?(:result).must_equal true
236
+ _(ctx2.key?("result.default")).must_equal true
237
+ _(ctx2.key?(:"result.default")).must_equal true
238
+ _(ctx2.key?(:result)).must_equal true
189
239
 
190
240
  # todo: TEST flow_options={context_class: SomethingElse}
191
241
  end
192
242
 
193
- it ".build provides default args" do
194
- immutable = {model: Object, "policy.default" => Hash}
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={}>")
195
256
 
196
- # {Aliasing#initialize}
197
- ctx = Trailblazer::Context::IndifferentAccess.new(immutable, {}, context_alias: {"policy.default" => :policy})
257
+ _(ctx.to_hash).must_equal({ model: Object })
198
258
 
199
- ctx[:model].must_equal Object
200
- ctx["model"].must_equal Object
201
- ctx[:policy].must_equal Hash
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
202
299
 
203
300
  ctx2 = ctx.merge(result: :success)
204
301
 
205
302
 
206
- ctx2[:model].must_equal Object
207
- ctx2["model"].must_equal Object
208
- ctx2[:policy].must_equal Hash
209
- ctx2[:result].must_equal :success
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`}
210
320
  end
211
321
  end
212
322