trailblazer-context 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +17 -0
- data/.gitignore +1 -0
- data/CHANGES.md +40 -0
- data/README.md +0 -1
- data/Rakefile +6 -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 +3 -1
- metadata +31 -13
- data/.travis.yml +0 -12
- data/lib/trailblazer/context/aliasing.rb +0 -38
- data/lib/trailblazer/context/indifferent_access.rb +0 -38
- data/lib/trailblazer/option.rb +0 -77
- data/test/option_test.rb +0 -186
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2f68ea3324607256a507718188b38dcb7d0252f17bf49221b97b5e62be4d252
|
4
|
+
data.tar.gz: 6841b846db299f4c21352f03703c3b8780539e2eb59efbadf0bd1ecd780bb0d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1ec16cbdba1cfeca1f3820e188dc5c8b449ff0295ee8849f5509297123b5c8b5046a9265206dff83051457e42aa7a7771387c82a4dfb5da6be9b2d93387b3aa
|
7
|
+
data.tar.gz: ee014562e35ad36355a8e62f18b8e8c2e497c86014aabac143ef308099e1f79f0b740d3f3e78fdeb0d9e5155b440ddb31e7f945da02fd48fc42eba1d7197f681
|
@@ -0,0 +1,17 @@
|
|
1
|
+
name: CI
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
strategy:
|
6
|
+
fail-fast: false
|
7
|
+
matrix:
|
8
|
+
# Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
|
9
|
+
ruby: [2.5, 2.6, 2.7, '3.0', head, jruby, jruby-head]
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
steps:
|
12
|
+
- uses: actions/checkout@v2
|
13
|
+
- uses: ruby/setup-ruby@v1
|
14
|
+
with:
|
15
|
+
ruby-version: ${{ matrix.ruby }}
|
16
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
17
|
+
- run: bundle exec rake
|
data/.gitignore
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,43 @@
|
|
1
|
+
# 0.5.0
|
2
|
+
|
3
|
+
* Extracted `Option` into its own repository, `trailblazer-option` ✨
|
4
|
+
|
5
|
+
# 0.4.0
|
6
|
+
|
7
|
+
* Ready for Ruby 3.0. :heart:
|
8
|
+
* Remove `Option::KW`, after many years it's been superseded by the original `Trailblazer::Option`.
|
9
|
+
|
10
|
+
To achieve an invocation such as
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
Option::KW(proc).(ctx, another_positional_arg, **circuit_options)
|
14
|
+
```
|
15
|
+
|
16
|
+
you can use the new `:keyword_arguments` options.
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
Option(proc).(ctx, another_positional_arg, keyword_arguments: ctx.to_hash, **circuit_options)
|
20
|
+
```
|
21
|
+
|
22
|
+
This way, no more guessing is happening about what positional arg is the actual `circuit_options`.
|
23
|
+
|
24
|
+
# 0.3.3
|
25
|
+
|
26
|
+
* Remove an unsolicited `puts`.
|
27
|
+
|
28
|
+
# 0.3.2
|
29
|
+
|
30
|
+
* Relax gem dependency: `hashie` >= 3.0.
|
31
|
+
|
32
|
+
# 0.3.1
|
33
|
+
|
34
|
+
* Even though this is a patch version, but it contains major changes.
|
35
|
+
* `to_hash` speed improvement - Same-ish as `Hash#to_hash`.
|
36
|
+
* Maintains replica for faster access and copy actions.
|
37
|
+
* Support all other `Hash` features (find, dig, collect etc) on `ctx` object.
|
38
|
+
* Namespace context related options within `flow_options`. (`{ flow_options: { context_options: { aliases: {}, ** } } }`).
|
39
|
+
* Add `Trailblazer::Context()` API with standard default container & replica class.
|
40
|
+
|
1
41
|
# 0.3.0
|
2
42
|
* Add support for ruby 2.7
|
3
43
|
* Drop support for ruby 2.0
|
data/README.md
CHANGED
@@ -5,5 +5,4 @@ _Argument-specific data structures for Trailblazer._
|
|
5
5
|
This gem provides data structures needed across `Activity`, `Workflow` and `Operation`, such as the following.
|
6
6
|
|
7
7
|
* `Trailblazer::Context` implements the so-called `options` hash that is passed between steps and implements the keyword arguments.
|
8
|
-
* `Trailblazer::Option` is often used to wrap an option at compile-time and `call` it at runtime, which allows to have the common `-> ()`, `:method` or `Callable` pattern used for most options.
|
9
8
|
* `Trailblazer::ContainerChain` to implement chained lookups of properties and allow including containers such as `Dry::Container` in this chain. This is experimental.
|
data/Rakefile
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
|