trailblazer-context 0.1.2 → 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 +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
|