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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f6c3c7ff9c8b061c5ac0d5aca954f03cd1b5ff0bf286aa787ede12f1e187890
4
- data.tar.gz: 0fdc45ffab9316c7799f2ce59f54c6409305cf91fa01349d30d026eac998c188
3
+ metadata.gz: f2f68ea3324607256a507718188b38dcb7d0252f17bf49221b97b5e62be4d252
4
+ data.tar.gz: 6841b846db299f4c21352f03703c3b8780539e2eb59efbadf0bd1ecd780bb0d8
5
5
  SHA512:
6
- metadata.gz: 2c9f349028afca126e1961e50ebb0ff661a5fe5a0e2573108c9c79f762694cdb03c6914beca4eec4c5b3b25aecae03d4706c47c84854d0ebb57b6e8a42ec279f
7
- data.tar.gz: 1fba9db52f9e3680cccb3b89624c8a159bbd144fbd0b86b3a71972ea1feff96978b82867c976a568aec709b09d0fe859d173a1a813fa5747cb42d179bf6c7520
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
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  *.rbc
3
+ *.swp
3
4
  /.config
4
5
  /coverage/
5
6
  /InstalledFiles
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
@@ -7,4 +7,10 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList["test/*_test.rb"]
8
8
  end
9
9
 
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
+
10
16
  task default: %i[test]
@@ -1,104 +1,33 @@
1
- require "trailblazer/option"
2
1
  # TODO: mark/make all but mutable_options as frozen.
3
- # The idea of Skill is to have a generic, ordered read/write interface that
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
- class Context
17
- # NOTE: In the future, we might look up the Context to use in the ctx.
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
- context_class.build(wrapped_options, mutable_options, [ctx, flow_options], circuit_options)
14
+ module Store
15
+ autoload :IndifferentAccess, "trailblazer/context/store/indifferent_access"
28
16
  end
29
17
 
30
- # @public
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
- # TODO: use ContainerChain.find here for a generic optimization
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 []=(name, value)
65
- @mutable_options[name] = value
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.new(wrapped_options, mutable_options)
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