trailblazer-context 0.1.4 → 0.3.3
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/.travis.yml +9 -5
- data/CHANGES.md +31 -0
- data/LICENSE +1 -1
- data/Rakefile +5 -2
- data/lib/trailblazer-context.rb +1 -0
- data/lib/trailblazer/container_chain.rb +0 -2
- data/lib/trailblazer/context.rb +13 -72
- 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 +239 -41
- data/test/option_test.rb +6 -6
- data/test/test_helper.rb +0 -1
- data/trailblazer-context.gemspec +3 -2
- metadata +20 -15
- data/.rubocop-https---raw-githubusercontent-com-trailblazer-meta-master-rubocop-yml +0 -136
- data/.rubocop.yml +0 -40
- data/lib/trailblazer/context/indifferent_access.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 278837bbeb41d58a08abe15cfa239e7c2d0030cf4d4d7de19e98f0982fd70ea1
|
4
|
+
data.tar.gz: 1dffd41a315387722858cf987a4289211cc8ea537664c98b5042539fcee11e6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 553c3dd7b3d9ff2ed9493c4a0d44369d1f6f4442ab9b53679e6e1147f7e052d98974273fa38466056e377e2de03fdd662f8e12d28b99cfde99d1074c5d4aa9e3
|
7
|
+
data.tar.gz: abfddac36f7d1a789f45018d3a79e98f5443e4934a29513f80d728453e5c12cf10e85bd8f061eead6b96006c3f0df85a26a2a80c9184524255b9e7b7a197504c
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,34 @@
|
|
1
|
+
# 0.3.3
|
2
|
+
|
3
|
+
* Remove an unsolicited `puts`.
|
4
|
+
|
5
|
+
# 0.3.2
|
6
|
+
|
7
|
+
* Relax gem dependency: `hashie` >= 3.0.
|
8
|
+
|
9
|
+
# 0.3.1
|
10
|
+
|
11
|
+
* Even though this is a patch version, but it contains major changes.
|
12
|
+
* `to_hash` speed improvement - Same-ish as `Hash#to_hash`.
|
13
|
+
* Maintains replica for faster access and copy actions.
|
14
|
+
* Support all other `Hash` features (find, dig, collect etc) on `ctx` object.
|
15
|
+
* Namespace context related options within `flow_options`. (`{ flow_options: { context_options: { aliases: {}, ** } } }`).
|
16
|
+
* Add `Trailblazer::Context()` API with standard default container & replica class.
|
17
|
+
|
18
|
+
# 0.3.0
|
19
|
+
* Add support for ruby 2.7
|
20
|
+
* Drop support for ruby 2.0
|
21
|
+
|
22
|
+
# 0.2.0
|
23
|
+
|
24
|
+
* Added `Context::IndifferentAccess`.
|
25
|
+
* Added `Context::Aliasing`.
|
26
|
+
* `Context.for_circuit` is not the authorative builder for creating a context.
|
27
|
+
|
28
|
+
# 0.1.5
|
29
|
+
|
30
|
+
* `Context.build` allows quickly building a Context without requiring the circuit interface.
|
31
|
+
|
1
32
|
# 0.1.4
|
2
33
|
|
3
34
|
* 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.
|
data/LICENSE
CHANGED
data/Rakefile
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
|
-
require "rubocop/rake_task"
|
4
3
|
|
5
4
|
Rake::TestTask.new(:test) do |t|
|
6
5
|
t.libs << "test"
|
@@ -8,6 +7,10 @@ Rake::TestTask.new(:test) do |t|
|
|
8
7
|
t.test_files = FileList["test/*_test.rb"]
|
9
8
|
end
|
10
9
|
|
11
|
-
|
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
|
12
15
|
|
13
16
|
task default: %i[test]
|
data/lib/trailblazer-context.rb
CHANGED
@@ -39,11 +39,9 @@ class Trailblazer::Context::ContainerChain
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
# rubocop:disable Style/AsciiComments
|
43
42
|
# alternative implementation:
|
44
43
|
# containers.reverse.each do |container| @mutable_options.merge!(container) end
|
45
44
|
#
|
46
45
|
# benchmark, merging in #initialize vs. this resolver.
|
47
46
|
# merge 39.678k (± 9.1%) i/s - 198.700k in 5.056653s
|
48
47
|
# resolver 68.928k (± 6.4%) i/s - 342.836k in 5.001610s
|
49
|
-
# rubocop:enable Style/AsciiComments
|
data/lib/trailblazer/context.rb
CHANGED
@@ -1,92 +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 options we pass in here to be forward-compatible.
|
19
|
-
def self.for(wrapped_options, (ctx, flow_options), circuit_options)
|
20
|
-
implementation.new(wrapped_options, {})
|
21
|
-
end
|
22
|
-
|
23
|
-
# I hate globals, but currently this is the only easy way for setting the implementation.
|
24
|
-
def self.implementation
|
25
|
-
IndifferentAccess
|
26
|
-
end
|
27
|
-
|
28
|
-
def initialize(wrapped_options, mutable_options)
|
29
|
-
@wrapped_options = wrapped_options
|
30
|
-
@mutable_options = mutable_options
|
31
|
-
# TODO: wrapped_options should be optimized for lookups here since
|
32
|
-
# it could also be a Context instance, but should be a ContainerChain.
|
33
|
-
end
|
34
|
-
|
35
|
-
def [](name)
|
36
|
-
# ContainerChain.find( [@mutable_options, @wrapped_options], name )
|
37
|
-
|
38
|
-
# in 99.9% or cases @mutable_options will be a Hash, and these are already optimized for lookups.
|
39
|
-
# it's up to the ContainerChain to optimize itself.
|
40
|
-
return @mutable_options[name] if @mutable_options.key?(name)
|
41
|
-
@wrapped_options[name]
|
42
|
-
end
|
43
|
-
|
44
|
-
# TODO: use ContainerChain.find here for a generic optimization
|
45
|
-
#
|
46
|
-
# the version here is about 4x faster for now.
|
47
|
-
def key?(name)
|
48
|
-
# ContainerChain.find( [@mutable_options, @wrapped_options], name )
|
49
|
-
@mutable_options.key?(name) || @wrapped_options.key?(name)
|
50
|
-
end
|
51
|
-
|
52
|
-
def []=(name, value)
|
53
|
-
@mutable_options[name] = value
|
54
|
-
end
|
55
|
-
|
56
|
-
# @private
|
57
|
-
def merge(hash)
|
58
|
-
original, mutable_options = decompose
|
11
|
+
module Context
|
12
|
+
autoload :Container, "trailblazer/context/container"
|
59
13
|
|
60
|
-
|
14
|
+
module Store
|
15
|
+
autoload :IndifferentAccess, "trailblazer/context/store/indifferent_access"
|
61
16
|
end
|
62
17
|
|
63
|
-
|
64
|
-
# the next activity.
|
65
|
-
def decompose
|
66
|
-
[@wrapped_options, @mutable_options]
|
67
|
-
end
|
18
|
+
module_function
|
68
19
|
|
69
|
-
def
|
70
|
-
|
20
|
+
def for_circuit(wrapped_options, mutable_options, (_, flow_options), **)
|
21
|
+
build(wrapped_options, mutable_options, flow_options.fetch(:context_options))
|
71
22
|
end
|
72
23
|
|
73
|
-
|
74
|
-
|
75
|
-
# transformation wants.
|
76
|
-
# FIXME: ToKeywordArguments()
|
77
|
-
def to_hash
|
78
|
-
{}.tap do |hash|
|
79
|
-
# the "key" here is to call to_hash on all containers.
|
80
|
-
[@wrapped_options.to_hash, @mutable_options.to_hash].each do |options|
|
81
|
-
options.each { |k, v| hash[k.to_sym] = v }
|
82
|
-
end
|
83
|
-
end
|
24
|
+
def build(wrapped_options, mutable_options, container_class:, **context_options)
|
25
|
+
container_class.new(wrapped_options, mutable_options, context_options)
|
84
26
|
end
|
85
27
|
end
|
86
28
|
|
87
|
-
def self.Context(wrapped_options, mutable_options = {})
|
88
|
-
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) ))
|
89
32
|
end
|
90
33
|
end
|
91
|
-
|
92
|
-
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
|