trailblazer-context 0.2.0 → 0.4.0

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.
@@ -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