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