trailblazer-context 0.2.0 → 0.4.0
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 +10 -5
- data/CHANGES.md +40 -0
- data/Gemfile +0 -1
- 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 -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/lib/trailblazer/option.rb +26 -57
- 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 +201 -91
- data/test/option_test.rb +34 -73
- data/test/test_helper.rb +0 -1
- data/trailblazer-context.gemspec +4 -3
- metadata +21 -17
- data/.rubocop-https---raw-githubusercontent-com-trailblazer-meta-master-rubocop-yml +0 -136
- data/.rubocop.yml +0 -40
- data/lib/trailblazer/context/aliasing.rb +0 -36
- data/lib/trailblazer/context/indifferent_access.rb +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68c328cb817dbf1889d8e4619afa0b950786a839d8685c145ac9cda543b63c5b
|
4
|
+
data.tar.gz: 3bbe468cf341ebb429715983c3ea284b02251eedfe0084b6225252fbf5240e4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0001ec043007eec13ac0ce4c836f406a4fbfcfee8c759fb85c2059d0fffce774423d8986f504a9b0fc36c31e4d0bd75673a02f2d1e9f92b1229f95af3115f029
|
7
|
+
data.tar.gz: 85848da20e2f7639183f4cefc2095294f183f3217927b8ed573ba307ed122871549f1a5409d3369e767a896f8a64c18af598e30fbf10bc33de3918e546e2c6dd
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,43 @@
|
|
1
|
+
# 0.4.0
|
2
|
+
|
3
|
+
* Ready for Ruby 3.0. :heart:
|
4
|
+
* Remove `Option::KW`, after many years it's been superseded by the original `Trailblazer::Option`.
|
5
|
+
|
6
|
+
To achieve an invocation such as
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
Option::KW(proc).(ctx, another_positional_arg, **circuit_options)
|
10
|
+
```
|
11
|
+
|
12
|
+
you can use the new `:keyword_arguments` options.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
Option(proc).(ctx, another_positional_arg, keyword_arguments: ctx.to_hash, **circuit_options)
|
16
|
+
```
|
17
|
+
|
18
|
+
This way, no more guessing is happening about what positional arg is the actual `circuit_options`.
|
19
|
+
|
20
|
+
# 0.3.3
|
21
|
+
|
22
|
+
* Remove an unsolicited `puts`.
|
23
|
+
|
24
|
+
# 0.3.2
|
25
|
+
|
26
|
+
* Relax gem dependency: `hashie` >= 3.0.
|
27
|
+
|
28
|
+
# 0.3.1
|
29
|
+
|
30
|
+
* Even though this is a patch version, but it contains major changes.
|
31
|
+
* `to_hash` speed improvement - Same-ish as `Hash#to_hash`.
|
32
|
+
* Maintains replica for faster access and copy actions.
|
33
|
+
* Support all other `Hash` features (find, dig, collect etc) on `ctx` object.
|
34
|
+
* Namespace context related options within `flow_options`. (`{ flow_options: { context_options: { aliases: {}, ** } } }`).
|
35
|
+
* Add `Trailblazer::Context()` API with standard default container & replica class.
|
36
|
+
|
37
|
+
# 0.3.0
|
38
|
+
* Add support for ruby 2.7
|
39
|
+
* Drop support for ruby 2.0
|
40
|
+
|
1
41
|
# 0.2.0
|
2
42
|
|
3
43
|
* Added `Context::IndifferentAccess`.
|
data/Gemfile
CHANGED
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,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
|