trailblazer-context 0.3.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f6c3c7ff9c8b061c5ac0d5aca954f03cd1b5ff0bf286aa787ede12f1e187890
4
- data.tar.gz: 0fdc45ffab9316c7799f2ce59f54c6409305cf91fa01349d30d026eac998c188
3
+ metadata.gz: 74b7b17574531adba20826815dc6bc735063774b17b6f652d13d2800f48972b1
4
+ data.tar.gz: f761aa2b0a53ffe8699df2cb7dc97d724d52d794e218813e9691f9a5762be211
5
5
  SHA512:
6
- metadata.gz: 2c9f349028afca126e1961e50ebb0ff661a5fe5a0e2573108c9c79f762694cdb03c6914beca4eec4c5b3b25aecae03d4706c47c84854d0ebb57b6e8a42ec279f
7
- data.tar.gz: 1fba9db52f9e3680cccb3b89624c8a159bbd144fbd0b86b3a71972ea1feff96978b82867c976a568aec709b09d0fe859d173a1a813fa5747cb42d179bf6c7520
6
+ metadata.gz: 718ce62a0c23a69c017580a140af0e273b51b8724654ed55cc0d0594162faf7f8885b64778f34b4c45cfb292f564955213ae297606f4cab81062d677e52df432
7
+ data.tar.gz: 00dbfe9f61ecb3ae20c06a715d8607a3320611a6b05d099c162f58c7043a598ad06e6902dce1860ea00b9f8bc210043aa18ac04efd563d6ffb2ac6f027235db1
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  *.rbc
3
+ *.swp
3
4
  /.config
4
5
  /coverage/
5
6
  /InstalledFiles
data/CHANGES.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.3.1
2
+
3
+ * Even though this is a patch version, but it contains major changes.
4
+ * `to_hash` speed improvement - Same-ish as `Hash#to_hash`.
5
+ * Maintains replica for faster access and copy actions.
6
+ * Support all other `Hash` features (find, dig, collect etc) on `ctx` object.
7
+ * Namespace context related options within `flow_options`. (`{ flow_options: { context_options: { aliases: {}, ** } } }`).
8
+ * Add `Trailblazer::Context()` API with standard default container & replica class.
9
+
1
10
  # 0.3.0
2
11
  * Add support for ruby 2.7
3
12
  * Drop support for ruby 2.0
data/Rakefile CHANGED
@@ -7,4 +7,10 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList["test/*_test.rb"]
8
8
  end
9
9
 
10
+ Rake::TestTask.new(:benchmark) do |t|
11
+ t.libs << "test"
12
+ t.libs << "lib"
13
+ t.test_files = FileList["test/benchmark/*_test.rb"]
14
+ end
15
+
10
16
  task default: %i[test]
@@ -1,2 +1,3 @@
1
1
  require "trailblazer/context/version"
2
2
  require "trailblazer/context"
3
+ require "trailblazer/option"
@@ -1,104 +1,33 @@
1
- require "trailblazer/option"
2
1
  # TODO: mark/make all but mutable_options as frozen.
3
- # The idea of Skill is to have a generic, ordered read/write interface that
2
+ # The idea of Context is to have a generic, ordered read/write interface that
4
3
  # collects mutable runtime-computed data while providing access to compile-time
5
4
  # information.
6
5
  # The runtime-data takes precedence over the class data.
7
- #
8
- # notes
9
- # a context is a ContainerChain with two elements (when reading)
10
6
  module Trailblazer
11
7
  # Holds local options (aka `mutable_options`) and "original" options from the "outer"
12
8
  # activity (aka wrapped_options).
13
-
14
9
  # only public creator: Build
15
10
  # :data object:
16
- class Context
17
- # NOTE: In the future, we might look up the Context to use in the ctx.
18
- # The demanding signature is for forward-compat.
19
- # @private
20
- def self.for(wrapped_options, (ctx, flow_options), **circuit_options) # TODO: remove
21
- implementation.build(wrapped_options, {}, [ctx, flow_options], circuit_options)
22
- end
23
-
24
- def self.for_circuit(wrapped_options, mutable_options, (ctx, flow_options), **circuit_options)
25
- context_class = flow_options[:context_class] || implementation # Context::IndifferentAccess
11
+ module Context
12
+ autoload :Container, "trailblazer/context/container"
26
13
 
27
- context_class.build(wrapped_options, mutable_options, [ctx, flow_options], circuit_options)
14
+ module Store
15
+ autoload :IndifferentAccess, "trailblazer/context/store/indifferent_access"
28
16
  end
29
17
 
30
- # @public
31
- def self.build(wrapped_options, *)
32
- new(wrapped_options)
33
- end
34
-
35
- # I hate globals, but currently this is the only easy way for setting the implementation.
36
- def self.implementation
37
- IndifferentAccess
38
- end
39
-
40
- def initialize(wrapped_options, mutable_options, *)
41
- @wrapped_options = wrapped_options
42
- @mutable_options = mutable_options
43
- # TODO: wrapped_options should be optimized for lookups here since
44
- # it could also be a Context instance, but should be a ContainerChain.
45
- end
46
-
47
- def [](name)
48
- # ContainerChain.find( [@mutable_options, @wrapped_options], name )
49
-
50
- # in 99.9% or cases @mutable_options will be a Hash, and these are already optimized for lookups.
51
- # it's up to the ContainerChain to optimize itself.
52
- return @mutable_options[name] if @mutable_options.key?(name)
53
- @wrapped_options[name]
54
- end
18
+ module_function
55
19
 
56
- # TODO: use ContainerChain.find here for a generic optimization
57
- #
58
- # the version here is about 4x faster for now.
59
- def key?(name)
60
- # ContainerChain.find( [@mutable_options, @wrapped_options], name )
61
- @mutable_options.key?(name) || @wrapped_options.key?(name)
20
+ def for_circuit(wrapped_options, mutable_options, (_, flow_options), **)
21
+ build(wrapped_options, mutable_options, flow_options.fetch(:context_options))
62
22
  end
63
23
 
64
- def []=(name, value)
65
- @mutable_options[name] = value
66
- end
67
-
68
- # @private
69
- def merge(hash)
70
- original, mutable_options = decompose
71
-
72
- self.class.new(original, mutable_options.merge(hash))
73
- end
74
-
75
- # Return the Context's two components. Used when computing the new output for
76
- # the next activity.
77
- def decompose
78
- [@wrapped_options, @mutable_options]
79
- end
80
-
81
- def keys
82
- @mutable_options.keys + @wrapped_options.keys # FIXME.
83
- end
84
-
85
- # TODO: maybe we shouldn't allow to_hash from context?
86
- # TODO: massive performance bottleneck. also, we could already "know" here what keys the
87
- # transformation wants.
88
- # FIXME: ToKeywordArguments()
89
- def to_hash
90
- {}.tap do |hash|
91
- # the "key" here is to call to_hash on all containers.
92
- [@wrapped_options.to_hash, @mutable_options.to_hash].each do |options|
93
- options.each { |k, v| hash[k.to_sym] = v }
94
- end
95
- end
24
+ def build(wrapped_options, mutable_options, container_class:, **context_options)
25
+ container_class.new(wrapped_options, mutable_options, context_options)
96
26
  end
97
27
  end
98
28
 
99
- def self.Context(wrapped_options, mutable_options = {})
100
- Context.new(wrapped_options, mutable_options)
29
+ def self.Context(wrapped_options, mutable_options = {}, context_options = nil)
30
+ defaults = { container_class: Context::Container, replica_class: Context::Store::IndifferentAccess }
31
+ Context.build(wrapped_options, mutable_options, defaults.merge( Hash(context_options) ))
101
32
  end
102
33
  end
103
-
104
- require "trailblazer/context/indifferent_access"
@@ -0,0 +1,100 @@
1
+ require "forwardable"
2
+
3
+ module Trailblazer
4
+ module Context
5
+ class Container
6
+ autoload :WithAliases, "trailblazer/context/container/with_aliases"
7
+
8
+ class UseWithAliases < RuntimeError
9
+ def message
10
+ %{Pass `Trailblazer::Context::Container::WithAliases` as `container_class` while defining `aliases`}
11
+ end
12
+ end
13
+
14
+ def initialize(wrapped_options, mutable_options, replica_class:, aliases: nil, **)
15
+ raise UseWithAliases if aliases
16
+
17
+ @wrapped_options = wrapped_options
18
+ @mutable_options = mutable_options
19
+ @replica_class = replica_class
20
+
21
+ @replica = initialize_replica_store
22
+ end
23
+
24
+ # Return the Context's two components. Used when computing the new output for
25
+ # the next activity.
26
+ def decompose
27
+ [@wrapped_options, @mutable_options]
28
+ end
29
+
30
+ def inspect
31
+ %{#<Trailblazer::Context::Container wrapped_options=#{@wrapped_options} mutable_options=#{@mutable_options}>}
32
+ end
33
+ alias_method :to_s, :inspect
34
+
35
+ private def initialize_replica_store
36
+ @replica_class.new([ @wrapped_options, @mutable_options ])
37
+ end
38
+
39
+ # Some common methods made available directly in Context::Container for
40
+ # performance tuning, extensions and to avoid `@replica` delegations.
41
+ module CommonMethods
42
+ def [](key)
43
+ @replica[key]
44
+ end
45
+
46
+ def []=(key, value)
47
+ @replica[key] = value
48
+ @mutable_options[key] = value
49
+ end
50
+ alias_method :store, :[]=
51
+
52
+ def delete(key)
53
+ @replica.delete(key)
54
+ @mutable_options.delete(key)
55
+ end
56
+
57
+ def merge(other_hash)
58
+ self.class.new(
59
+ @wrapped_options,
60
+ @mutable_options.merge(other_hash),
61
+ replica_class: @replica_class,
62
+ )
63
+ end
64
+
65
+ def fetch(key, default = nil, &block)
66
+ @replica.fetch(key, default, &block)
67
+ end
68
+
69
+ def keys; @replica.keys; end
70
+
71
+ def key?(key); @replica.key?(key); end
72
+
73
+ def values; @replica.values; end
74
+
75
+ def value?(value); @replica.value?(value); end
76
+
77
+ def to_hash; @replica.to_hash; end
78
+
79
+ def each(&block); @replica.each(&block); end
80
+ include Enumerable
81
+ end
82
+
83
+ # Additional methods being forwarded on Context::Container
84
+ # NOTE: def_delegated method calls incurs additional cost
85
+ # compared to actual method defination calls.
86
+ # https://github.com/JuanitoFatas/fast-ruby/pull/182
87
+ module Delegations
88
+ extend Forwardable
89
+ def_delegators :@replica,
90
+ :default, :default=, :default_proc, :default_proc=,
91
+ :fetch_values, :index, :dig, :slice,
92
+ :key, :each_key,
93
+ :each_value, :values_at
94
+ end
95
+
96
+ include CommonMethods
97
+ extend Delegations
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,102 @@
1
+ module Trailblazer
2
+ module Context
3
+ class Container
4
+ # Extension to replace Context::Container writers with aliased writers.
5
+ # It'll mutate the well known `@mutable_options` with only original keys and
6
+ # `@replica` with both orignal and aliased keys
7
+ class WithAliases < Container
8
+ def initialize(wrapped_options, mutable_options, aliases:, replica_class:, **)
9
+ @wrapped_options = wrapped_options
10
+ @mutable_options = mutable_options
11
+
12
+ # { "contract.default" => :contract, "result.default" => :result }
13
+ @aliases = aliases
14
+
15
+ @replica_class = replica_class
16
+ @replica = initialize_replica_store
17
+ end
18
+
19
+ def inspect
20
+ %{#<Trailblazer::Context::Container::WithAliases wrapped_options=#{@wrapped_options} mutable_options=#{@mutable_options} aliases=#{@aliases}>}
21
+ end
22
+
23
+ # @public
24
+ def aliased_writer(key, value)
25
+ _key, _alias = alias_mapping_for(key)
26
+
27
+ @mutable_options[_key] = value
28
+ @replica[_key] = value
29
+ @replica[_alias] = value if _alias
30
+ end
31
+ alias_method :[]=, :aliased_writer
32
+
33
+ # @public
34
+ def aliased_delete(key)
35
+ _key, _alias = alias_mapping_for(key)
36
+
37
+ @mutable_options.delete(_key)
38
+ @replica.delete(_key)
39
+ @replica.delete(_alias) if _alias
40
+ end
41
+ alias_method :delete, :aliased_delete
42
+
43
+ # @public
44
+ def aliased_merge(other_hash)
45
+ # other_hash could have aliases and we don't want to store them in @mutable_options.
46
+ _other_hash = replace_aliases_with_original_keys(other_hash)
47
+
48
+ options = { aliases: @aliases, replica_class: @replica_class }
49
+ self.class.new(@wrapped_options, @mutable_options.merge(_other_hash), **options)
50
+ end
51
+ alias_method :merge, :aliased_merge
52
+
53
+ # Returns key and it's mapped alias. `key` could be an alias too.
54
+ #
55
+ # aliases => { "contract.default" => :contract, "result.default"=>:result }
56
+ # key, _alias = alias_mapping_for(:contract)
57
+ # key, _alias = alias_mapping_for("contract.default")
58
+ #
59
+ # @public
60
+ def alias_mapping_for(key)
61
+ # when key has an alias
62
+ return [ key, @aliases[key] ] if @aliases.key?(key)
63
+
64
+ # when key is an alias
65
+ return [ @aliases.key(key), key ] if @aliases.value?(key)
66
+
67
+ # when there is no alias
68
+ return [ key, nil ]
69
+ end
70
+
71
+ private
72
+
73
+ # Maintain aliases in `@replica` to make ctx actions faster™
74
+ def initialize_replica_store
75
+ replica = @replica_class.new([ @wrapped_options, @mutable_options ])
76
+
77
+ @aliases.each do |original_key, _alias|
78
+ replica[_alias] = replica[original_key] if replica.key?(original_key)
79
+ end
80
+
81
+ replica
82
+ end
83
+
84
+ # Replace aliases from `hash` with their orignal keys.
85
+ # This is used while doing a `merge` which initializes new Container
86
+ # with original keys and their aliases.
87
+ def replace_aliases_with_original_keys(hash)
88
+ # DISCUSS: Better way to check for alias presence in `hash`
89
+ return hash unless (hash.keys & @aliases.values).any?
90
+
91
+ _hash = hash.dup
92
+
93
+ @aliases.each do |original_key, _alias|
94
+ _hash[original_key] = _hash.delete(_alias) if _hash.key?(_alias)
95
+ end
96
+
97
+ return _hash
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -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.3.0"
2
+ module Context
3
+ VERSION = "0.3.1"
4
4
  end
5
5
  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
@@ -19,6 +19,7 @@ class ArgsTest < Minitest::Spec
19
19
 
20
20
  # it { }
21
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}>}
22
23
  end
23
24
 
24
25
  it "allows false/nil values" do
@@ -51,24 +52,36 @@ class ArgsTest < Minitest::Spec
51
52
 
52
53
  merged = ctx.merge(current_user: Module)
53
54
 
55
+ _(merged.class).must_equal(Trailblazer::Context::Container)
54
56
  _(merged.to_hash).must_equal(repository: "User", current_user: Module)
55
57
  _(ctx.to_hash).must_equal(repository: "User")
56
58
  end
57
59
  end
58
60
 
59
- #-
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
60
68
  it do
61
69
  immutable = {repository: "User", model: Module, current_user: Class}
70
+ mutable = {error: RuntimeError}
62
71
 
63
- Trailblazer::Context(immutable) do |_original, mutable|
64
- mutable
65
- end
72
+ _([immutable, mutable]).must_equal Trailblazer::Context(immutable, mutable).decompose
66
73
  end
67
74
  end
68
75
 
69
76
  class ContextWithIndifferentAccessTest < Minitest::Spec
70
77
  it do
71
- flow_options = {}
78
+ flow_options = {
79
+ context_options: {
80
+ container_class: Trailblazer::Context::Container,
81
+ replica_class: Trailblazer::Context::Store::IndifferentAccess
82
+ }
83
+ }
84
+
72
85
  circuit_options = {}
73
86
 
74
87
  immutable = {model: Object, "policy" => Hash}
@@ -108,6 +121,12 @@ class ContextWithIndifferentAccessTest < Minitest::Spec
108
121
  _(ctx["contract.default"]).must_equal Module
109
122
  _(ctx[:"contract.default"]).must_equal Module
110
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
111
130
 
112
131
  ctx3 = ctx.merge("result" => false)
113
132
 
@@ -120,12 +139,24 @@ class ContextWithIndifferentAccessTest < Minitest::Spec
120
139
  end
121
140
 
122
141
  it "Aliasable" do
123
- 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
+
124
150
  circuit_options = {}
125
151
 
126
152
  immutable = {model: Object, "policy" => Hash}
127
153
 
128
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)
129
160
 
130
161
  _(ctx[:model]).must_equal Object
131
162
  _(ctx["model"]).must_equal Object
@@ -141,14 +172,24 @@ class ContextWithIndifferentAccessTest < Minitest::Spec
141
172
  assert_nil ctx["result"]
142
173
 
143
174
  _(ctx[:contract]).must_equal Module
175
+ _(ctx['contract']).must_equal Module
144
176
 
145
177
  assert_nil ctx[:stack]
178
+ assert_nil ctx['stack']
146
179
 
147
180
  # Set an aliased property via setter
148
181
  ctx["trace.stack"] = Object
149
182
  _(ctx[:stack]).must_equal Object
183
+ _(ctx["stack"]).must_equal Object
150
184
  _(ctx["trace.stack"]).must_equal Object
151
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
+
152
193
  # key?
153
194
  _(ctx.key?("____contract.default")).must_equal false
154
195
  _(ctx.key?("contract.default")).must_equal true
@@ -159,8 +200,19 @@ class ContextWithIndifferentAccessTest < Minitest::Spec
159
200
  _(ctx.key?("trace.stack")).must_equal true
160
201
  _(ctx.key?(:"trace.stack")).must_equal true
161
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
+
162
214
  # to_hash
163
- _(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)
164
216
 
165
217
  # context in context
166
218
  ctx2 = Trailblazer::Context.for_circuit(ctx, {}, [ctx, flow_options], **circuit_options)
@@ -188,11 +240,58 @@ class ContextWithIndifferentAccessTest < Minitest::Spec
188
240
  # todo: TEST flow_options={context_class: SomethingElse}
189
241
  end
190
242
 
191
- it ".build provides default args" do
192
- 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={}>")
193
256
 
194
- # {Aliasing#initialize}
195
- ctx = Trailblazer::Context::IndifferentAccess.new(immutable, {}, context_alias: {"policy.default" => :policy})
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)
196
295
 
197
296
  _(ctx[:model]).must_equal Object
198
297
  _(ctx["model"]).must_equal Object
@@ -206,6 +305,19 @@ class ContextWithIndifferentAccessTest < Minitest::Spec
206
305
  _(ctx2[:policy]).must_equal Hash
207
306
  _(ctx2[:result]).must_equal :success
208
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
209
321
  end
210
322
 
211
323
  # TODO: test overriding Context.implementation.
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.require_paths = ["lib"]
22
22
 
23
+ spec.add_dependency "hashie", "~> 4.1"
24
+
23
25
  spec.add_development_dependency "bundler"
24
26
  spec.add_development_dependency "minitest"
25
27
  spec.add_development_dependency "rake"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trailblazer-context
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-29 00:00:00.000000000 Z
11
+ date: 2020-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hashie
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.1'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -70,10 +84,14 @@ files:
70
84
  - lib/trailblazer-context.rb
71
85
  - lib/trailblazer/container_chain.rb
72
86
  - lib/trailblazer/context.rb
73
- - lib/trailblazer/context/aliasing.rb
74
- - lib/trailblazer/context/indifferent_access.rb
87
+ - lib/trailblazer/context/container.rb
88
+ - lib/trailblazer/context/container/with_aliases.rb
89
+ - lib/trailblazer/context/store/indifferent_access.rb
75
90
  - lib/trailblazer/context/version.rb
76
91
  - lib/trailblazer/option.rb
92
+ - test/benchmark/benchmark_helper.rb
93
+ - test/benchmark/indifferent_access_test.rb
94
+ - test/benchmark/indifferent_access_with_aliasing_test.rb
77
95
  - test/context_test.rb
78
96
  - test/option_test.rb
79
97
  - test/test_helper.rb
@@ -97,11 +115,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
115
  - !ruby/object:Gem::Version
98
116
  version: '0'
99
117
  requirements: []
100
- rubygems_version: 3.0.3
118
+ rubygems_version: 3.0.8
101
119
  signing_key:
102
120
  specification_version: 4
103
121
  summary: Argument-specific data structures for Trailblazer.
104
122
  test_files:
123
+ - test/benchmark/benchmark_helper.rb
124
+ - test/benchmark/indifferent_access_test.rb
125
+ - test/benchmark/indifferent_access_with_aliasing_test.rb
105
126
  - test/context_test.rb
106
127
  - test/option_test.rb
107
128
  - test/test_helper.rb
@@ -1,38 +0,0 @@
1
- module Trailblazer
2
- class Context
3
- module Aliasing
4
- def initialize(wrapped_options, mutable_options, context_alias: {}, **)
5
- super(wrapped_options, mutable_options)
6
-
7
- @aliases = context_alias.invert
8
- end
9
-
10
- def [](key)
11
- return super unless (aka = @aliases[key]) # yepp, nil/false won't work
12
-
13
- super(aka)
14
- end
15
-
16
- def key?(key)
17
- return super unless (aka = @aliases[key]) # yepp, nil/false won't work
18
-
19
- super(aka)
20
- end
21
-
22
- # @private ?
23
- def merge(hash)
24
- original, mutable_options = decompose
25
-
26
- self.class.new(
27
- original,
28
- mutable_options.merge(hash),
29
- context_alias: @aliases.invert # DISCUSS: maybe we can speed up by remembering the original options?
30
- )
31
- end
32
-
33
- def to_hash
34
- super.merge(Hash[@aliases.collect { |aka, k| key?(k) ? [aka, self[k]] : nil }.compact]) # FIXME: performance!
35
- end
36
- end
37
- end
38
- end
@@ -1,38 +0,0 @@
1
- require "trailblazer/context/aliasing"
2
-
3
- module Trailblazer
4
- class Context
5
- class IndifferentAccess < Context
6
- module InstanceMethods
7
- def [](name)
8
- # TODO: well...
9
- @mutable_options.key?(name.to_sym) and return @mutable_options[name.to_sym]
10
- @mutable_options.key?(name.to_s) and return @mutable_options[name.to_s]
11
- @wrapped_options.key?(name.to_sym) and return @wrapped_options[name.to_sym]
12
- @wrapped_options[name.to_s]
13
- end
14
-
15
- def self.build(wrapped_options, mutable_options, (_ctx, flow_options), **)
16
- new(wrapped_options, mutable_options, flow_options)
17
- end
18
- end
19
- include InstanceMethods
20
-
21
- def key?(name)
22
- super(name.to_sym) || super(name.to_s)
23
- end
24
-
25
- include Aliasing # FIXME
26
-
27
-
28
- class << self
29
- # This also builds IndifferentAccess::Aliasing.
30
- # The {#build} method is designed to take all args from {for_circuit} and then
31
- # translate that to the constructor.
32
- def build(wrapped_options, mutable_options, (_ctx, flow_options), **)
33
- new(wrapped_options, mutable_options, **flow_options)
34
- end
35
- end
36
- end
37
- end
38
- end