trailblazer-context 0.1.2 → 0.3.1

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
- 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