trailblazer-context 0.1.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.travis.yml +12 -0
- data/CHANGES.md +31 -0
- data/Gemfile +2 -2
- data/LICENSE +1 -1
- data/Rakefile +8 -2
- data/lib/trailblazer-context.rb +1 -0
- data/lib/trailblazer/container_chain.rb +5 -3
- data/lib/trailblazer/context.rb +15 -61
- 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/lib/trailblazer/option.rb +18 -19
- 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 +323 -0
- data/test/option_test.rb +186 -0
- data/test/test_helper.rb +3 -0
- data/trailblazer-context.gemspec +14 -11
- metadata +49 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
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/.travis.yml
ADDED
data/CHANGES.md
CHANGED
@@ -1,3 +1,34 @@
|
|
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
|
+
|
10
|
+
# 0.3.0
|
11
|
+
* Add support for ruby 2.7
|
12
|
+
* Drop support for ruby 2.0
|
13
|
+
|
14
|
+
# 0.2.0
|
15
|
+
|
16
|
+
* Added `Context::IndifferentAccess`.
|
17
|
+
* Added `Context::Aliasing`.
|
18
|
+
* `Context.for_circuit` is not the authorative builder for creating a context.
|
19
|
+
|
20
|
+
# 0.1.5
|
21
|
+
|
22
|
+
* `Context.build` allows quickly building a Context without requiring the circuit interface.
|
23
|
+
|
24
|
+
# 0.1.4
|
25
|
+
|
26
|
+
* Fix the `IndifferentAccess` name lookup. Since we can't convert all keys to symbols internally (not every options structure has `collect`) we need to have a lookup chain.
|
27
|
+
|
28
|
+
# 0.1.3
|
29
|
+
|
30
|
+
* Introduce `Context::IndifferentAccess` which converts all keys to symbol. This, in turn, allows to use both string and symbol keys everywhere. Currently, the implementation is set via the global method `Context.implementation` and defaults to the new `IndifferentAccess`.
|
31
|
+
|
1
32
|
# 0.1.2
|
2
33
|
|
3
34
|
* More meaningful error message: "No :exec_context given.".
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
data/Rakefile
CHANGED
@@ -4,7 +4,13 @@ require "rake/testtask"
|
|
4
4
|
Rake::TestTask.new(:test) do |t|
|
5
5
|
t.libs << "test"
|
6
6
|
t.libs << "lib"
|
7
|
-
t.test_files = FileList[
|
7
|
+
t.test_files = FileList["test/*_test.rb"]
|
8
8
|
end
|
9
9
|
|
10
|
-
|
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
|
+
|
16
|
+
task default: %i[test]
|
data/lib/trailblazer-context.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# @private
|
2
|
-
|
2
|
+
# used to be called Resolver.
|
3
|
+
class Trailblazer::Context::ContainerChain
|
3
4
|
# Keeps a list of containers. When looking up a key/value, containers are traversed in
|
4
5
|
# the order they were added until key is found.
|
5
6
|
#
|
@@ -28,11 +29,12 @@ class Trailblazer::Context::ContainerChain # used to be called Resolver.
|
|
28
29
|
|
29
30
|
def keys
|
30
31
|
@containers.collect(&:keys).flatten
|
31
|
-
end
|
32
|
+
end
|
32
33
|
|
33
34
|
# @private
|
34
35
|
def to_hash
|
35
|
-
|
36
|
+
# FIXME: introduce pattern matching so we can have different "transformers" for each container type.
|
37
|
+
return @to_hash.(@containers) if @to_hash
|
36
38
|
@containers.each_with_object({}) { |container, hash| hash.merge!(container.to_hash) }
|
37
39
|
end
|
38
40
|
end
|
data/lib/trailblazer/context.rb
CHANGED
@@ -1,79 +1,33 @@
|
|
1
1
|
# TODO: mark/make all but mutable_options as frozen.
|
2
|
-
# The idea of
|
2
|
+
# The idea of Context is to have a generic, ordered read/write interface that
|
3
3
|
# collects mutable runtime-computed data while providing access to compile-time
|
4
4
|
# information.
|
5
5
|
# The runtime-data takes precedence over the class data.
|
6
|
-
#
|
7
|
-
# notes
|
8
|
-
# a context is a ContainerChain with two elements (when reading)
|
9
6
|
module Trailblazer
|
10
7
|
# Holds local options (aka `mutable_options`) and "original" options from the "outer"
|
11
8
|
# activity (aka wrapped_options).
|
12
|
-
|
13
9
|
# only public creator: Build
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
# TODO: wrapped_options should be optimized for lookups here since it could also be a Context instance, but should be a ContainerChain.
|
18
|
-
end
|
19
|
-
|
20
|
-
def [](name)
|
21
|
-
# ContainerChain.find( [@mutable_options, @wrapped_options], name )
|
22
|
-
|
23
|
-
# in 99.9% or cases @mutable_options will be a Hash, and these are already optimized for lookups.
|
24
|
-
# it's up to the ContainerChain to optimize itself.
|
25
|
-
return @mutable_options[name] if @mutable_options.key?(name)
|
26
|
-
@wrapped_options[name]
|
27
|
-
end
|
28
|
-
|
29
|
-
# TODO: use ContainerChain.find here for a generic optimization
|
30
|
-
#
|
31
|
-
# the version here is about 4x faster for now.
|
32
|
-
def key?(name)
|
33
|
-
# ContainerChain.find( [@mutable_options, @wrapped_options], name )
|
34
|
-
@mutable_options.key?(name) || @wrapped_options.key?(name)
|
35
|
-
end
|
10
|
+
# :data object:
|
11
|
+
module Context
|
12
|
+
autoload :Container, "trailblazer/context/container"
|
36
13
|
|
37
|
-
|
38
|
-
|
14
|
+
module Store
|
15
|
+
autoload :IndifferentAccess, "trailblazer/context/store/indifferent_access"
|
39
16
|
end
|
40
17
|
|
41
|
-
|
42
|
-
#
|
43
|
-
# This method might be removed.
|
44
|
-
def merge(hash)
|
45
|
-
original, mutable_options = decompose
|
18
|
+
module_function
|
46
19
|
|
47
|
-
|
20
|
+
def for_circuit(wrapped_options, mutable_options, (_, flow_options), **)
|
21
|
+
build(wrapped_options, mutable_options, flow_options.fetch(:context_options))
|
48
22
|
end
|
49
23
|
|
50
|
-
|
51
|
-
|
52
|
-
def decompose
|
53
|
-
[ @wrapped_options, @mutable_options ]
|
54
|
-
end
|
55
|
-
|
56
|
-
def keys
|
57
|
-
@mutable_options.keys + @wrapped_options.keys # FIXME.
|
58
|
-
end
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
# TODO: maybe we shouldn't allow to_hash from context?
|
63
|
-
# TODO: massive performance bottleneck. also, we could already "know" here what keys the
|
64
|
-
# transformation wants.
|
65
|
-
# FIXME: ToKeywordArguments()
|
66
|
-
def to_hash
|
67
|
-
{}.tap do |hash|
|
68
|
-
# the "key" here is to call to_hash on all containers.
|
69
|
-
[ @wrapped_options.to_hash, @mutable_options.to_hash ].each do |options|
|
70
|
-
options.each { |k, v| hash[k.to_sym] = v }
|
71
|
-
end
|
72
|
-
end
|
24
|
+
def build(wrapped_options, mutable_options, container_class:, **context_options)
|
25
|
+
container_class.new(wrapped_options, mutable_options, context_options)
|
73
26
|
end
|
74
27
|
end
|
75
28
|
|
76
|
-
def self.Context(wrapped_options, mutable_options={})
|
77
|
-
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) ))
|
78
32
|
end
|
79
|
-
end
|
33
|
+
end
|
@@ -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
|