trailblazer-context 0.1.2 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 067526c4453158c411b7c4416eb16c6054f51b92
4
- data.tar.gz: 8c5d5b8cddb29477858653ec4e3ece78c6d08e99
2
+ SHA256:
3
+ metadata.gz: 74b7b17574531adba20826815dc6bc735063774b17b6f652d13d2800f48972b1
4
+ data.tar.gz: f761aa2b0a53ffe8699df2cb7dc97d724d52d794e218813e9691f9a5762be211
5
5
  SHA512:
6
- metadata.gz: a83649f3eb107686ce63c1f5474f52a5bbb8a9dbab165117712bedfce838dd05c155764eb723bb5b43a9eeb93829305dd4efe2a3c041ca6c5eb52d66d4e2ccab
7
- data.tar.gz: ebcec8767f92ef3923b88a0b11eb9f9fa3075e7f55cd0052b80d9a074fcd45ed3b00fa6d7c42dc73451c60a35b34499d748c3bc85fde698e3f6ad8e8d69f3723
6
+ metadata.gz: 718ce62a0c23a69c017580a140af0e273b51b8724654ed55cc0d0594162faf7f8885b64778f34b4c45cfb292f564955213ae297606f4cab81062d677e52df432
7
+ data.tar.gz: 00dbfe9f61ecb3ae20c06a715d8607a3320611a6b05d099c162f58c7043a598ad06e6902dce1860ea00b9f8bc210043aa18ac04efd563d6ffb2ac6f027235db1
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  *.rbc
3
+ *.swp
3
4
  /.config
4
5
  /coverage/
5
6
  /InstalledFiles
@@ -9,6 +10,7 @@
9
10
  /test/tmp/
10
11
  /test/version_tmp/
11
12
  /tmp/
13
+ Gemfile.lock
12
14
 
13
15
  # Used by dotenv library to load environment variables.
14
16
  # .env
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ before_install: gem install bundler
3
+ cache: bundler
4
+ rvm:
5
+ - ruby-head
6
+ - 2.7
7
+ - 2.6
8
+ - 2.5
9
+ - 2.4
10
+ jobs:
11
+ allow_failures:
12
+ - rvm: ruby-head
data/CHANGES.md CHANGED
@@ -1,3 +1,34 @@
1
+ # 0.3.1
2
+
3
+ * Even though this is a patch version, but it contains major changes.
4
+ * `to_hash` speed improvement - Same-ish as `Hash#to_hash`.
5
+ * Maintains replica for faster access and copy actions.
6
+ * Support all other `Hash` features (find, dig, collect etc) on `ctx` object.
7
+ * Namespace context related options within `flow_options`. (`{ flow_options: { context_options: { aliases: {}, ** } } }`).
8
+ * Add `Trailblazer::Context()` API with standard default container & replica class.
9
+
10
+ # 0.3.0
11
+ * Add support for ruby 2.7
12
+ * Drop support for ruby 2.0
13
+
14
+ # 0.2.0
15
+
16
+ * Added `Context::IndifferentAccess`.
17
+ * Added `Context::Aliasing`.
18
+ * `Context.for_circuit` is not the authorative builder for creating a context.
19
+
20
+ # 0.1.5
21
+
22
+ * `Context.build` allows quickly building a Context without requiring the circuit interface.
23
+
24
+ # 0.1.4
25
+
26
+ * Fix the `IndifferentAccess` name lookup. Since we can't convert all keys to symbols internally (not every options structure has `collect`) we need to have a lookup chain.
27
+
28
+ # 0.1.3
29
+
30
+ * Introduce `Context::IndifferentAccess` which converts all keys to symbol. This, in turn, allows to use both string and symbol keys everywhere. Currently, the implementation is set via the global method `Context.implementation` and defaults to the new `IndifferentAccess`.
31
+
1
32
  # 0.1.2
2
33
 
3
34
  * More meaningful error message: "No :exec_context given.".
data/Gemfile CHANGED
@@ -1,5 +1,5 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
  gemspec
3
3
 
4
- gem "minitest-line"
5
4
  gem "benchmark-ips"
5
+ 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
@@ -4,7 +4,13 @@ require "rake/testtask"
4
4
  Rake::TestTask.new(:test) do |t|
5
5
  t.libs << "test"
6
6
  t.libs << "lib"
7
- t.test_files = FileList['test/*_test.rb']
7
+ t.test_files = FileList["test/*_test.rb"]
8
8
  end
9
9
 
10
- task :default => :test
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
+
16
+ task default: %i[test]
@@ -1,2 +1,3 @@
1
+ require "trailblazer/context/version"
1
2
  require "trailblazer/context"
2
3
  require "trailblazer/option"
@@ -1,5 +1,6 @@
1
1
  # @private
2
- class Trailblazer::Context::ContainerChain # used to be called Resolver.
2
+ # used to be called Resolver.
3
+ class Trailblazer::Context::ContainerChain
3
4
  # Keeps a list of containers. When looking up a key/value, containers are traversed in
4
5
  # the order they were added until key is found.
5
6
  #
@@ -28,11 +29,12 @@ class Trailblazer::Context::ContainerChain # used to be called Resolver.
28
29
 
29
30
  def keys
30
31
  @containers.collect(&:keys).flatten
31
- end
32
+ end
32
33
 
33
34
  # @private
34
35
  def to_hash
35
- return @to_hash.(@containers) if @to_hash # FIXME: introduce pattern matching so we can have different "transformers" for each container type.
36
+ # FIXME: introduce pattern matching so we can have different "transformers" for each container type.
37
+ return @to_hash.(@containers) if @to_hash
36
38
  @containers.each_with_object({}) { |container, hash| hash.merge!(container.to_hash) }
37
39
  end
38
40
  end
@@ -1,79 +1,33 @@
1
1
  # TODO: mark/make all but mutable_options as frozen.
2
- # 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
3
3
  # collects mutable runtime-computed data while providing access to compile-time
4
4
  # information.
5
5
  # The runtime-data takes precedence over the class data.
6
- #
7
- # notes
8
- # a context is a ContainerChain with two elements (when reading)
9
6
  module Trailblazer
10
7
  # Holds local options (aka `mutable_options`) and "original" options from the "outer"
11
8
  # activity (aka wrapped_options).
12
-
13
9
  # only public creator: Build
14
- class Context # :data object:
15
- def initialize(wrapped_options, mutable_options)
16
- @wrapped_options, @mutable_options = wrapped_options, mutable_options
17
- # TODO: wrapped_options should be optimized for lookups here since it could also be a Context instance, but should be a ContainerChain.
18
- end
19
-
20
- def [](name)
21
- # ContainerChain.find( [@mutable_options, @wrapped_options], name )
22
-
23
- # in 99.9% or cases @mutable_options will be a Hash, and these are already optimized for lookups.
24
- # it's up to the ContainerChain to optimize itself.
25
- return @mutable_options[name] if @mutable_options.key?(name)
26
- @wrapped_options[name]
27
- end
28
-
29
- # TODO: use ContainerChain.find here for a generic optimization
30
- #
31
- # the version here is about 4x faster for now.
32
- def key?(name)
33
- # ContainerChain.find( [@mutable_options, @wrapped_options], name )
34
- @mutable_options.key?(name) || @wrapped_options.key?(name)
35
- end
10
+ # :data object:
11
+ module Context
12
+ autoload :Container, "trailblazer/context/container"
36
13
 
37
- def []=(name, value)
38
- @mutable_options[name] = value
14
+ module Store
15
+ autoload :IndifferentAccess, "trailblazer/context/store/indifferent_access"
39
16
  end
40
17
 
41
- # @private
42
- #
43
- # This method might be removed.
44
- def merge(hash)
45
- original, mutable_options = decompose
18
+ module_function
46
19
 
47
- ctx = Trailblazer::Context( original, mutable_options.merge(hash) )
20
+ def for_circuit(wrapped_options, mutable_options, (_, flow_options), **)
21
+ build(wrapped_options, mutable_options, flow_options.fetch(:context_options))
48
22
  end
49
23
 
50
- # Return the Context's two components. Used when computing the new output for
51
- # the next activity.
52
- def decompose
53
- [ @wrapped_options, @mutable_options ]
54
- end
55
-
56
- def keys
57
- @mutable_options.keys + @wrapped_options.keys # FIXME.
58
- end
59
-
60
-
61
-
62
- # TODO: maybe we shouldn't allow to_hash from context?
63
- # TODO: massive performance bottleneck. also, we could already "know" here what keys the
64
- # transformation wants.
65
- # FIXME: ToKeywordArguments()
66
- def to_hash
67
- {}.tap do |hash|
68
- # the "key" here is to call to_hash on all containers.
69
- [ @wrapped_options.to_hash, @mutable_options.to_hash ].each do |options|
70
- options.each { |k, v| hash[k.to_sym] = v }
71
- end
72
- end
24
+ def build(wrapped_options, mutable_options, container_class:, **context_options)
25
+ container_class.new(wrapped_options, mutable_options, context_options)
73
26
  end
74
27
  end
75
28
 
76
- def self.Context(wrapped_options, mutable_options={})
77
- 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) ))
78
32
  end
79
- end # Trailblazer
33
+ end
@@ -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