trailblazer-context 0.3.0 → 0.3.1

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