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 +4 -4
- data/.gitignore +1 -0
- data/CHANGES.md +9 -0
- data/Rakefile +6 -0
- data/lib/trailblazer-context.rb +1 -0
- data/lib/trailblazer/context.rb +13 -84
- data/lib/trailblazer/context/container.rb +100 -0
- data/lib/trailblazer/context/container/with_aliases.rb +102 -0
- data/lib/trailblazer/context/store/indifferent_access.rb +36 -0
- data/lib/trailblazer/context/version.rb +2 -2
- data/test/benchmark/benchmark_helper.rb +32 -0
- data/test/benchmark/indifferent_access_test.rb +89 -0
- data/test/benchmark/indifferent_access_with_aliasing_test.rb +73 -0
- data/test/context_test.rb +123 -11
- data/trailblazer-context.gemspec +2 -0
- metadata +26 -5
- data/lib/trailblazer/context/aliasing.rb +0 -38
- data/lib/trailblazer/context/indifferent_access.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74b7b17574531adba20826815dc6bc735063774b17b6f652d13d2800f48972b1
|
4
|
+
data.tar.gz: f761aa2b0a53ffe8699df2cb7dc97d724d52d794e218813e9691f9a5762be211
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 718ce62a0c23a69c017580a140af0e273b51b8724654ed55cc0d0594162faf7f8885b64778f34b4c45cfb292f564955213ae297606f4cab81062d677e52df432
|
7
|
+
data.tar.gz: 00dbfe9f61ecb3ae20c06a715d8607a3320611a6b05d099c162f58c7043a598ad06e6902dce1860ea00b9f8bc210043aa18ac04efd563d6ffb2ac6f027235db1
|
data/.gitignore
CHANGED
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
data/lib/trailblazer-context.rb
CHANGED
data/lib/trailblazer/context.rb
CHANGED
@@ -1,104 +1,33 @@
|
|
1
|
-
require "trailblazer/option"
|
2
1
|
# TODO: mark/make all but mutable_options as frozen.
|
3
|
-
# The idea of
|
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
|
-
|
17
|
-
|
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
|
-
|
14
|
+
module Store
|
15
|
+
autoload :IndifferentAccess, "trailblazer/context/store/indifferent_access"
|
28
16
|
end
|
29
17
|
|
30
|
-
|
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
|
-
|
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
|
65
|
-
|
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
|
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
|
@@ -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
@@ -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
|
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 = {
|
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, :
|
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
|
192
|
-
|
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
|
-
|
195
|
-
|
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.
|
data/trailblazer-context.gemspec
CHANGED
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.
|
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-
|
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/
|
74
|
-
- lib/trailblazer/context/
|
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.
|
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
|