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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fec7a9c801fedda32fee29585bf8108b6a730980fe826df22d00cd7f0b6c8d1b
4
- data.tar.gz: 6a39db80e891a99c802c1262b26434389869fc2b367c34cfffb5923acdd574b4
3
+ metadata.gz: 68c328cb817dbf1889d8e4619afa0b950786a839d8685c145ac9cda543b63c5b
4
+ data.tar.gz: 3bbe468cf341ebb429715983c3ea284b02251eedfe0084b6225252fbf5240e4b
5
5
  SHA512:
6
- metadata.gz: cdfce2ccb6119793c8971e70d0161f8b330b5941f07c7b06467713f3a3ebe42ce2563bcc9c0259bc412fbb12c2264586dde4414456d24f7d3eb2b2c4eec65fc7
7
- data.tar.gz: 8f34a42f0b7a245586e86a45482e5d797d40edf9c6164cb149fd11de4bdbfe750b067d69a19eebdd747631df85bd40ce0789b2ce65c050c9309f87bed398795d
6
+ metadata.gz: 0001ec043007eec13ac0ce4c836f406a4fbfcfee8c759fb85c2059d0fffce774423d8986f504a9b0fc36c31e4d0bd75673a02f2d1e9f92b1229f95af3115f029
7
+ data.tar.gz: 85848da20e2f7639183f4cefc2095294f183f3217927b8ed573ba307ed122871549f1a5409d3369e767a896f8a64c18af598e30fbf10bc33de3918e546e2c6dd
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/.travis.yml CHANGED
@@ -1,8 +1,13 @@
1
- sudo: false
2
1
  language: ruby
3
2
  before_install: gem install bundler
3
+ cache: bundler
4
4
  rvm:
5
- - 2.5.1
6
- - 2.4.4
7
- - 2.3.7
8
- - 2.2.10
5
+ - ruby-head
6
+ - 3.0
7
+ - 2.7
8
+ - 2.6
9
+ - 2.5
10
+ - 2.4
11
+ jobs:
12
+ allow_failures:
13
+ - rvm: ruby-head
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
@@ -1,5 +1,4 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
3
 
4
- gem "benchmark-ips"
5
4
  gem "minitest-line"
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2017 TRAILBLAZER GmbH
3
+ Copyright (c) 2017-2020 TRAILBLAZER GmbH
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
- RuboCop::RakeTask.new(:rubocop)
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]
@@ -1,2 +1,3 @@
1
1
  require "trailblazer/context/version"
2
2
  require "trailblazer/context"
3
+ require "trailblazer/option"
@@ -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
@@ -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