trailblazer-context 0.1.3 → 0.3.2

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