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 +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
|