trailblazer-context 0.1.3 → 0.3.2

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
2
  SHA256:
3
- metadata.gz: 65fadb8598cd610a092a09bc79d5ebc754d8f09372dd2dd7a044e2435aea6759
4
- data.tar.gz: 9b12d7531efdedaf4786cf0af148657f1db85a500db82538e093254c0ab662bc
3
+ metadata.gz: fbf6e1e7f36a5e14eb97f31fd25a038a300701365b6db3b22ae4c84c46224bd3
4
+ data.tar.gz: de24df40ebb1626a2163684a25c8998fe78683b17ea7c2c2a22a90279eb1e3ad
5
5
  SHA512:
6
- metadata.gz: 80552f09f7744038d0d821abea3ed69e085cd5fea0d8416734f3e2fd9b825c8532743049d0a43123023c58cd2162237360542d5256dc382c4fc2d9c434e43f1a
7
- data.tar.gz: e32733db3688e262e11ae3a955ca75ab3a9546ae172b8815c6c9c1a4d1566ac604d8d0b752707c19dd59b7f8534b8579bf8e013fcdfe9a08968cc531fc261e4d
6
+ metadata.gz: f42c1473f4f53e3ba9f53f12f1fad86d69dc899024971bf18d983d91717a7ede222185ef808e98f2aae1faf344d5ce2d6e49fe401d671d25fc0ead83d805a691
7
+ data.tar.gz: e0d659a6d4bcd7b5df7064d674a17874fc9c859fd4b403d7f9affad16cdd1716f51507957a8c8930367258525b48579be044a3d9e57edd3db024ec762d93d393
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,12 @@
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
+ - 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.2
2
+
3
+ * Relax gem dependency: `hashie` >= 3.0.
4
+
5
+ # 0.3.1
6
+
7
+ * Even though this is a patch version, but it contains major changes.
8
+ * `to_hash` speed improvement - Same-ish as `Hash#to_hash`.
9
+ * Maintains replica for faster access and copy actions.
10
+ * Support all other `Hash` features (find, dig, collect etc) on `ctx` object.
11
+ * Namespace context related options within `flow_options`. (`{ flow_options: { context_options: { aliases: {}, ** } } }`).
12
+ * Add `Trailblazer::Context()` API with standard default container & replica class.
13
+
14
+ # 0.3.0
15
+ * Add support for ruby 2.7
16
+ * Drop support for ruby 2.0
17
+
18
+ # 0.2.0
19
+
20
+ * Added `Context::IndifferentAccess`.
21
+ * Added `Context::Aliasing`.
22
+ * `Context.for_circuit` is not the authorative builder for creating a context.
23
+
24
+ # 0.1.5
25
+
26
+ * `Context.build` allows quickly building a Context without requiring the circuit interface.
27
+
28
+ # 0.1.4
29
+
30
+ * 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.
31
+
1
32
  # 0.1.3
2
33
 
3
34
  * 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`.
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
- task default: %i[test rubocop]
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,92 +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 options we pass in here to be forward-compatible.
19
- def self.for(wrapped_options, (ctx, flow_options), circuit_options)
20
- implementation.new(wrapped_options, {})
21
- end
22
-
23
- # I hate globals, but currently this is the only easy way for setting the implementation.
24
- def self.implementation
25
- IndifferentAccess
26
- end
27
-
28
- def initialize(wrapped_options, mutable_options)
29
- @wrapped_options = wrapped_options
30
- @mutable_options = mutable_options
31
- # TODO: wrapped_options should be optimized for lookups here since
32
- # it could also be a Context instance, but should be a ContainerChain.
33
- end
34
-
35
- def [](name)
36
- # ContainerChain.find( [@mutable_options, @wrapped_options], name )
37
-
38
- # in 99.9% or cases @mutable_options will be a Hash, and these are already optimized for lookups.
39
- # it's up to the ContainerChain to optimize itself.
40
- return @mutable_options[name] if @mutable_options.key?(name)
41
- @wrapped_options[name]
42
- end
43
-
44
- # TODO: use ContainerChain.find here for a generic optimization
45
- #
46
- # the version here is about 4x faster for now.
47
- def key?(name)
48
- # ContainerChain.find( [@mutable_options, @wrapped_options], name )
49
- @mutable_options.key?(name) || @wrapped_options.key?(name)
50
- end
51
-
52
- def []=(name, value)
53
- @mutable_options[name] = value
54
- end
55
-
56
- # @private
57
- def merge(hash)
58
- original, mutable_options = decompose
11
+ module Context
12
+ autoload :Container, "trailblazer/context/container"
59
13
 
60
- self.class.new(original, mutable_options.merge(hash))
14
+ module Store
15
+ autoload :IndifferentAccess, "trailblazer/context/store/indifferent_access"
61
16
  end
62
17
 
63
- # Return the Context's two components. Used when computing the new output for
64
- # the next activity.
65
- def decompose
66
- [@wrapped_options, @mutable_options]
67
- end
18
+ module_function
68
19
 
69
- def keys
70
- @mutable_options.keys + @wrapped_options.keys # FIXME.
20
+ def for_circuit(wrapped_options, mutable_options, (_, flow_options), **)
21
+ build(wrapped_options, mutable_options, flow_options.fetch(:context_options))
71
22
  end
72
23
 
73
- # TODO: maybe we shouldn't allow to_hash from context?
74
- # TODO: massive performance bottleneck. also, we could already "know" here what keys the
75
- # transformation wants.
76
- # FIXME: ToKeywordArguments()
77
- def to_hash
78
- {}.tap do |hash|
79
- # the "key" here is to call to_hash on all containers.
80
- [@wrapped_options.to_hash, @mutable_options.to_hash].each do |options|
81
- options.each { |k, v| hash[k.to_sym] = v }
82
- end
83
- end
24
+ def build(wrapped_options, mutable_options, container_class:, **context_options)
25
+ container_class.new(wrapped_options, mutable_options, context_options)
84
26
  end
85
27
  end
86
28
 
87
- def self.Context(wrapped_options, mutable_options = {})
88
- 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) ))
89
32
  end
90
33
  end
91
-
92
- 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