dio 0.0.2 → 0.0.3

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: 2bdc81fcaa60149f840667355881ac6a53875ef032ca860a1bd8c695ecb5557e
4
- data.tar.gz: 7916ffd72b1a56a65425e1e43a91c48d788d44e3866c265826425f43c84f53e1
3
+ metadata.gz: b059e97a8c1f5e6d1d68b445e51c553c14657ec837de6b61bbe1ce6bd8ccc106
4
+ data.tar.gz: 327197a40cc4cc1a3e0f814c60bb0f78bab24bff8b8012d69430a93454712ad2
5
5
  SHA512:
6
- metadata.gz: abc3478a6776c6a37ce181437fe168ca54eae42f5919f48b17e8a331641fd1038fe12a465ad70a52ad4e5f69cef239c1ffa11f72ef05875fe908e0d2b0b532ba
7
- data.tar.gz: a8c94fcda04a3d2e9cd88f89107cf63f860f56c0d16b80a3c2fbf0a8788455d93fff5a35425378669e5c948287d9201da375c75f536ad32a43181cfa86e9ad06
6
+ metadata.gz: c9fd3543e1c9fca221b21db3629467b1f481c41e0a8191057e1d72bcfe1d190e859e902453e7292b45bb11a48ba56328582ff80d2dc10926aeca3d9a8b0dac11
7
+ data.tar.gz: '0194555bdc1bc59719e7c09167c26a7159ceff987dbb200754623694e3d49b2e1b87cf0ded4bfb4ac4af2eee05a1df9e2cb6f160bb95e889b6be3705caf55cdf'
data/.gitignore CHANGED
@@ -7,5 +7,8 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
 
10
+ # Gem builds
11
+ *.gem
12
+
10
13
  # rspec failure tracking
11
14
  .rspec_status
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ * None
6
+
7
+ ## 0.0.3 - 01/17/2021
8
+
9
+ * Forwarders introduced
10
+ * Base concept of Forwarder and two new Forwarders introduced
11
+ * Attribute Forwarder to only work on `attr_*` methods, narrowing scope
12
+ * String Hash Forwarder to work on String keyed Hashes
13
+
14
+ ## 0.0.2 - 01/17/2021
15
+
16
+ * [Patch 1](https://github.com/baweaver/dio/issues/1) - Lock Ruby version to 3.x
17
+
18
+ ## 0.0.1 - 01/17/2021
19
+
20
+ * Initial release
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dio (0.0.1)
4
+ dio (0.0.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -15,8 +15,166 @@ match against it.
15
15
 
16
16
  ## Usage
17
17
 
18
- Further usage instructions will be documented at a later date, see
19
- `spec/dio_spec.rb` for ideas in the mean time.
18
+ There are three core types of Forwarders, the center of how Dio works:
19
+
20
+ 1. **Dynamic** - Uses `public_send` for `Hash` matches, and `Array` method coercion for `Array` matches
21
+ 2. **Attribute** - Uses `attr_*` methods as source of match data
22
+ 3. **String Hash** - Treats `String` Hashes like `Symbol` ones for the purpose of matching.
23
+
24
+ Let's take a look at each of them.
25
+
26
+ ### Dynamic Forwarder
27
+
28
+ Used with `Dio.dynamic` or `Dio[]`, the default forwarder. This uses `public_send` to extract attributes for pattern matching.
29
+
30
+ #### With Hash Matching
31
+
32
+ With an `Integer` this might look like this:
33
+
34
+ ```ruby
35
+ Dio[1] in { succ: { succ: { succ: 4 } } }
36
+ # => true
37
+ ```
38
+
39
+ This has the same result as calling `1.succ.succ.succ` with each nested `Hash` in the pattern match working on the next value. That means it can also be used to do this:
40
+
41
+ ```ruby
42
+ Dio[1] in {
43
+ succ: { chr: "\x02", to_s: '2' },
44
+ to_s: '1'
45
+ }
46
+ ```
47
+
48
+ #### With Array Matching
49
+
50
+ If the object under the wrapper provides a method that can be used to coerce the value into an `Array` it can be used for an `Array` match.
51
+
52
+ Those methods are: `to_a`, `to_ary`, and `map`.
53
+
54
+ Given a `Node` class with a value and a set of children:
55
+
56
+ ```ruby
57
+ Node = Struct.new(:value, :children)
58
+ ```
59
+
60
+ We can match against it as if it were capable of natively pattern matching:
61
+
62
+ ```ruby
63
+ tree = Node[1,
64
+ Node[2, Node[3, Node[4]]],
65
+ Node[5],
66
+ Node[6, Node[7], Node[8]]
67
+ ]
68
+
69
+ case Dio.dynamic(tree)
70
+ in [1, [*, [5, _], *]]
71
+ true
72
+ else
73
+ false
74
+ end
75
+ ```
76
+
77
+ ### Attribute Forwarder
78
+
79
+ Attribute Forwarders are more conservative than Dynamic ones as they only work on public attributes, or those that are defined with `attr_*`. In the case of this class:
80
+
81
+ ```ruby
82
+ class Person
83
+ attr_reader :name, :age, :children
84
+
85
+ def initialize(name:, age:, children: [])
86
+ @name = name
87
+ @age = age
88
+ @children = children
89
+ end
90
+ end
91
+ ```
92
+
93
+ ...the attributes available would be `name`, `age`, and `children`. This also means that you can dive into `children` as well.
94
+
95
+ #### With Hash Matching
96
+
97
+ Let's say we had Alice here:
98
+
99
+ ```ruby
100
+ Person.new(
101
+ name: 'Alice',
102
+ age: 40,
103
+ children: [
104
+ Person.new(name: 'Jim', age: 10),
105
+ Person.new(name: 'Jill', age: 10)
106
+ ]
107
+ )
108
+ ```
109
+
110
+ With Hash style matching we can do this:
111
+
112
+ ```ruby
113
+ case Dio.attribute(alice)
114
+ in { name: /^A/, age: 30..50 }
115
+ true
116
+ else
117
+ false
118
+ end
119
+ ```
120
+
121
+ ...which, as pattern matches use `===` lets us use a lot of other fun things. We can even go deeper into searching through the `children` attribute:
122
+
123
+ ```ruby
124
+ case Dio.attribute(alice)
125
+ in { children: [*, { name: /^J/ }, *] }
126
+ true
127
+ else
128
+ false
129
+ end
130
+ ```
131
+
132
+ #### With Array Matching
133
+
134
+ This one is a bit more spurious, as it applies the attributes in the name it sees them. For something like our `Node` above with two attributes it will work the same as `dynamic`.
135
+
136
+ ### String Hash Forwarder
137
+
138
+ Pattern Matching cannot apply to `String` keys, which can be annoying when working with data and not wanting to deep transform it into `Symbol` keys. The String Hash Forwarder tries to address this.
139
+
140
+ #### With Hash Matching
141
+
142
+ Let's say we had the following `Hash`:
143
+
144
+ ```ruby
145
+ hash = {
146
+ 'a' => 1,
147
+ 'b' => 2,
148
+ 'c' => {
149
+ 'd' => 3,
150
+ 'e' => {
151
+ 'f' => 4
152
+ }
153
+ }
154
+ }
155
+ ```
156
+
157
+ We can match against it by using the `Symbol` equivalents of our `String` keys:
158
+
159
+ ```ruby
160
+ case Dio.string_hash(hash)
161
+ in { a: 1, b: 2 }
162
+ true
163
+ else
164
+ false
165
+ end
166
+ ```
167
+
168
+ ...and because of the nature of Dio you can continue to dive deeper:
169
+
170
+ ```ruby
171
+ case Dio.string_hash(hash)
172
+ in { a: 1, b: 2, c: { d: 1..10, e: { f: 3.. } } }
173
+ true
174
+ else
175
+ false
176
+ end
177
+ ```
20
178
 
21
179
  ## Installation
22
180
 
data/lib/dio.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'dio/version'
2
2
  require 'dio/public_api'
3
3
  require 'dio/errors'
4
- require 'dio/dive_forwarder'
4
+ require 'dio/forwarders'
5
5
 
6
6
  module Dio
7
7
  extend PublicApi
@@ -11,5 +11,17 @@ module Dio
11
11
 
12
12
  def initialize(msg=MSG) = super
13
13
  end
14
+
15
+ # Error raised when no deconstruction method is available on an object being
16
+ # treated like an Array deconstruction.
17
+ #
18
+ # @author [baweaver]
19
+ # @since 0.0.1
20
+ class UnknownAttributesProvided < ArgumentError
21
+ # Error message
22
+ MSG = 'Unknown attribute arguments provided to method'
23
+
24
+ def initialize(attributes) = super("#{MSG}: #{attributes.join(', ')}")
25
+ end
14
26
  end
15
27
  end
@@ -0,0 +1,8 @@
1
+ require 'dio/forwarders/base_forwarder'
2
+ require 'dio/forwarders/attribute_forwarder'
3
+ require 'dio/forwarders/string_hash_forwarder'
4
+
5
+ module Dio
6
+ module Forwarders
7
+ end
8
+ end
@@ -0,0 +1,76 @@
1
+ require 'delegate'
2
+
3
+ module Dio
4
+ module Forwarders
5
+ # A more controlled forwarder that targets only `attr_` type methods like
6
+ # `attr_reader` and `attr_accessor`. These attributes are found by diffing
7
+ # instance variables and method names to find generated accessor methods.
8
+ #
9
+ # It should be noted that this could be faster if there was an assumption
10
+ # of purity at the time of the match. I may make another variant for that.
11
+ #
12
+ # @author [baweaver]
13
+ # @since 0.0.3
14
+ #
15
+ class AttributeForwarder < BaseForwarder
16
+ # Wrapper for creating a new Forwarder
17
+ NEW_DIVE = -> v { new(v) }
18
+
19
+ def initialize(base_object)
20
+ ivars = Set.new base_object
21
+ .instance_variables
22
+ .map { _1.to_s.delete('@').to_sym }
23
+
24
+ all_methods = Set.new base_object.methods
25
+
26
+ @attributes = ivars.intersection(all_methods)
27
+
28
+ super
29
+ end
30
+
31
+ # Deconstructs from a list of attribute names, wrapping each value
32
+ # in a new dive in case the pattern match goes further than one level.
33
+ #
34
+ # @return [Array]
35
+ def deconstruct
36
+ return @base_object.map(&NEW_DIVE) if @base_object.is_a?(Array)
37
+
38
+ @attributes.map { NEW_DIVE[@base_object.public_send(_1)] }
39
+ end
40
+
41
+ # Deconstructs attributes from an object, wrapping each value in a new
42
+ # dive in case the pattern match goes further than one level.
43
+ #
44
+ # @param keys [Array]
45
+ # Keys to be extracted, diffed against possible attributes
46
+ #
47
+ # @raises [Dio::Errors::UnknownAttributesProvided]
48
+ # Guard against unknown attributes without an associated accessor
49
+ # method
50
+ #
51
+ # @return [Hash]
52
+ def deconstruct_keys(keys)
53
+ key_set = Set.new(keys)
54
+ unknown_keys = key_set - @attributes
55
+
56
+ if unknown_keys.any?
57
+ raise Dio::Errors::UnknownAttributesProvided.new(unknown_keys)
58
+ end
59
+
60
+ known_keys = @attributes.intersection(key_set)
61
+
62
+ known_keys.to_h { [_1, NEW_DIVE[@base_object.public_send(_1)]] }
63
+ end
64
+
65
+ # Unwrapped context, aliased afterwards to use Ruby's delegator interface
66
+ #
67
+ # @return [Any]
68
+ # Originally wrapped object
69
+ def value
70
+ @base_object
71
+ end
72
+
73
+ alias_method :__getobj__, :value
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,131 @@
1
+ require 'delegate'
2
+
3
+ module Dio
4
+ module Forwarders
5
+ # Allows for Pattern Matching against arbitrary objects by wrapping them
6
+ # in an interface that understands methods of deconstructing objects.
7
+ #
8
+ # **Approximating Deconstruction**
9
+ #
10
+ # As Ruby does not, by default, define `deconstruct` and `deconstruct_keys` on
11
+ # objects this class attempts to approximate them.
12
+ #
13
+ # This class does, however, do something unique in treating `deconstruct_keys`
14
+ # as a series of calls to sent to the class and its "nested" values.
15
+ #
16
+ # **Demonstrating Nested Values**
17
+ #
18
+ # Consider an integer:
19
+ #
20
+ # ```ruby
21
+ # Dio[1] in { succ: { succ: { succ: 4 } } }
22
+ # # => true
23
+ # ```
24
+ #
25
+ # It has no concept of deconstruction, except in that its `succ` method returns
26
+ # a "nested" value we can match against, allowing us to "dive into" the
27
+ # object, diving us our namesake Dio, or "Dive Into Object"
28
+ #
29
+ # **Delegation**
30
+ #
31
+ # As with most monadic-like design patterns that add additional behavior by
32
+ # wrapping objects we need to extract the value at the end to do anything
33
+ # particularly useful.
34
+ #
35
+ # By adding delegation to this class we have a cheat around this in that
36
+ # any method called on the nested DiveForwarder instances will call through
37
+ # to the associated base object instead.
38
+ #
39
+ # I am not 100% sold on this approach, and will consider it more in the
40
+ # future.
41
+ #
42
+ # @author [baweaver]
43
+ #
44
+ class BaseForwarder < ::Delegator
45
+ # Wrapper for creating a new Forwarder
46
+ NEW_DIVE = -> v { new(v) }
47
+
48
+ # Creates a new delegator that understands the pattern matching interface
49
+ #
50
+ # @param base_object [Any]
51
+ # Any object that does not necessarily understand pattern matching
52
+ #
53
+ # @return [DiveForwarder]
54
+ def initialize(base_object)
55
+ @base_object = base_object
56
+ end
57
+
58
+ # Approximation of an Array deconstruction:
59
+ #
60
+ # ```ruby
61
+ # [1, 2, 3] in [*, 2, *]
62
+ # ```
63
+ #
64
+ # Attempts to find a reasonable interface by which to extract values
65
+ # to be matched. If an object that knows how to match already is sent
66
+ # through wrap its child values for deeper matching.
67
+ #
68
+ # Current interface will work with `to_a`, `to_ary`, `map`, and values
69
+ # that can already `deconstruct`. If others are desired please submit a PR
70
+ # to add them
71
+ #
72
+ # @raises [Dio::Errors::NoDeconstructionMethod]
73
+ # If no method of deconstruction exists, an exception is raised to
74
+ # communicate the proper interface and note the abscense of a current one.
75
+ #
76
+ # @return [Array[DiveForwarder]]
77
+ # Values lifted into a Dio context for further matching
78
+ def deconstruct
79
+ return @base_object.deconstruct.map!(&NEW_DIVE) if @base_object.respond_to?(:deconstruct)
80
+
81
+ return @base_object.to_a.map!(&NEW_DIVE) if @base_object.respond_to?(:to_a)
82
+ return @base_object.to_ary.map!(&NEW_DIVE) if @base_object.respond_to?(:to_ary)
83
+ return @base_object.map(&NEW_DIVE) if @base_object.respond_to?(:map)
84
+
85
+ raise Dio::Errors::NoDeconstructionMethod
86
+ end
87
+
88
+ # Approximates `deconstruct_keys` for Hashes, except in adding `Qo`-like
89
+ # behavior that allows to treat objects as "nested values" through their
90
+ # method calls.
91
+ #
92
+ # **Deconstructing an Object**
93
+ #
94
+ # In `Qo` one could match against an object by calling to its methods using
95
+ # `public_send`. This allowed one to "dive into" an object through a series
96
+ # of method calls, approximating a Hash pattern match.
97
+ #
98
+ # **Native Behavior**
99
+ #
100
+ # If the object already responds to `deconstruct_keys` this method will
101
+ # behave similarly to `deconstruct` and wrap its values as new
102
+ # `DiveForwarder` contexts.
103
+ #
104
+ # @param keys [Array]
105
+ # Keys to be extracted from the object
106
+ #
107
+ # @return [Hash]
108
+ # Deconstructed keys pointing to associated values extracted from a Hash
109
+ # or an Object. Note that these values are matched against using `===`.
110
+ def deconstruct_keys(keys)
111
+ if @base_object.respond_to?(:deconstruct_keys)
112
+ @base_object
113
+ .deconstruct_keys(keys)
114
+ .transform_values!(&NEW_DIVE)
115
+ else
116
+ keys.to_h { |k| @base_object.public_send(k).then { |v| [k, NEW_DIVE[v]] } }
117
+ end
118
+ end
119
+
120
+ # Unwrapped context, aliased afterwards to use Ruby's delegator interface
121
+ #
122
+ # @return [Any]
123
+ # Originally wrapped object
124
+ def value
125
+ @base_object
126
+ end
127
+
128
+ alias_method :__getobj__, :value
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,63 @@
1
+ require 'delegate'
2
+
3
+ module Dio
4
+ module Forwarders
5
+ # Pattern Matching relies on Symbol keys for Hash matching, but a significant
6
+ # number of Ruby Hashes are keyed with Strings. This forwarder addresses
7
+ # that issue by transforming String keys into Symbols for the sake of matching
8
+ # against them:
9
+ #
10
+ # ```ruby
11
+ # Dio.string_hash({ 'a' => 1 }) in { a: 1 }
12
+ # # => true
13
+ # ```
14
+ #
15
+ # @author [baweaver]
16
+ # @since 0.0.3
17
+ #
18
+ class StringHashForwarder < BaseForwarder
19
+ # Wrapper for creating a new Forwarder
20
+ NEW_DIVE = -> v { new(v) }
21
+
22
+ # If an Array is provided from nested values we may want to match against
23
+ # it as well. Wrap the values in new forwarders to accomodate this.
24
+ #
25
+ # @raises [Dio::Errors::NoDeconstructionMethod]
26
+ # Debating on this one. If the base is not an Array it complicates how
27
+ # to Array match against a Hash. Raise an error for now, consider
28
+ # revisiting later.
29
+ #
30
+ # @return [Array]
31
+ def deconstruct
32
+ return @base_object.map(&NEW_DIVE) if @base_object.is_a?(Array)
33
+
34
+ # Debating on this one
35
+ raise Dio::Errors::NoDeconstructionMethod
36
+ end
37
+
38
+ # Extracts keys from a Hash by treating the provided keys like Strings.
39
+ # Return the base object with keys transformed to Symbols to accomodate
40
+ # pattern matching features.
41
+ #
42
+ # @param keys [Array]
43
+ # Keys to extract
44
+ #
45
+ # @return [Hash]
46
+ def deconstruct_keys(keys)
47
+ @base_object
48
+ .slice(*keys.map(&:to_s))
49
+ .to_h { |k, v| [k.to_sym, NEW_DIVE[v]] }
50
+ end
51
+
52
+ # Unwrapped context, aliased afterwards to use Ruby's delegator interface
53
+ #
54
+ # @return [Any]
55
+ # Originally wrapped object
56
+ def value
57
+ @base_object
58
+ end
59
+
60
+ alias_method :__getobj__, :value
61
+ end
62
+ end
63
+ end
@@ -1,4 +1,8 @@
1
1
  module Dio
2
+ # Public API for Dio
3
+ #
4
+ # @author [baweaver]
5
+ # @since 0.0.1
2
6
  module PublicApi
3
7
  # Treats `[]` like an alternative constructor and forwards to `DiveForwarder`
4
8
  #
@@ -7,6 +11,31 @@ module Dio
7
11
  #
8
12
  # @return [Dio::DiveForwarder]
9
13
  # Dio pattern matching interface
10
- def [](...) = Dio::DiveForwarder.new(...)
14
+ def [](...) = Dio::Forwarders::BaseForwarder.new(...)
15
+
16
+ # Dynamic Forwarder, uses `public_send` for Hash forwarding
17
+ #
18
+ # @param ... [Any]
19
+ # Arguments to match against
20
+ #
21
+ # @return [Dio::Forwarders::BaseForwarder]
22
+ def dynamic(...) = Dio::Forwarders::BaseForwarder.new(...)
23
+
24
+ # Attribute Forwarder, extracts `attr_*` methods to match against
25
+ #
26
+ # @param ... [Any]
27
+ # Arguments to match against
28
+ #
29
+ # @return [Dio::Forwarders::AttributeForwarder]
30
+ def attribute(...) = Dio::Forwarders::AttributeForwarder.new(...)
31
+
32
+ # String Hash Forwarder, treats a String Hash like a Symbol Hash for
33
+ # matching against.
34
+ #
35
+ # @param ... [Any]
36
+ # Arguments to match against
37
+ #
38
+ # @return [Dio::Forwarders::StringHashForwarder]
39
+ def string_hash(...) = Dio::Forwarders::StringHashForwarder.new(...)
11
40
  end
12
41
  end
@@ -1,3 +1,3 @@
1
1
  module Dio
2
- VERSION = '0.0.2'.freeze
2
+ VERSION = '0.0.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Weaver
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-17 00:00:00.000000000 Z
11
+ date: 2021-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -75,6 +75,7 @@ extra_rdoc_files: []
75
75
  files:
76
76
  - ".gitignore"
77
77
  - ".rspec"
78
+ - CHANGELOG.md
78
79
  - CODE_OF_CONDUCT.md
79
80
  - Gemfile
80
81
  - Gemfile.lock
@@ -86,8 +87,11 @@ files:
86
87
  - bin/setup
87
88
  - dio.gemspec
88
89
  - lib/dio.rb
89
- - lib/dio/dive_forwarder.rb
90
90
  - lib/dio/errors.rb
91
+ - lib/dio/forwarders.rb
92
+ - lib/dio/forwarders/attribute_forwarder.rb
93
+ - lib/dio/forwarders/base_forwarder.rb
94
+ - lib/dio/forwarders/string_hash_forwarder.rb
91
95
  - lib/dio/public_api.rb
92
96
  - lib/dio/version.rb
93
97
  homepage: https://www.github.com/baweaver/dio
@@ -1,129 +0,0 @@
1
- require 'delegate'
2
-
3
- module Dio
4
- # Allows for Pattern Matching against arbitrary objects by wrapping them
5
- # in an interface that understands methods of deconstructing objects.
6
- #
7
- # **Approximating Deconstruction**
8
- #
9
- # As Ruby does not, by default, define `deconstruct` and `deconstruct_keys` on
10
- # objects this class attempts to approximate them.
11
- #
12
- # This class does, however, do something unique in treating `deconstruct_keys`
13
- # as a series of calls to sent to the class and its "nested" values.
14
- #
15
- # **Demonstrating Nested Values**
16
- #
17
- # Consider an integer:
18
- #
19
- # ```ruby
20
- # Dio[1] in { succ: { succ: { succ: 4 } } }
21
- # # => true
22
- # ```
23
- #
24
- # It has no concept of deconstruction, except in that its `succ` method returns
25
- # a "nested" value we can match against, allowing us to "dive into" the
26
- # object, diving us our namesake Dio, or "Dive Into Object"
27
- #
28
- # **Delegation**
29
- #
30
- # As with most monadic-like design patterns that add additional behavior by
31
- # wrapping objects we need to extract the value at the end to do anything
32
- # particularly useful.
33
- #
34
- # By adding delegation to this class we have a cheat around this in that
35
- # any method called on the nested DiveForwarder instances will call through
36
- # to the associated base object instead.
37
- #
38
- # I am not 100% sold on this approach, and will consider it more in the
39
- # future.
40
- #
41
- # @author [baweaver]
42
- #
43
- class DiveForwarder < ::Delegator
44
- # Wrapper for creating a new DiveForwarder
45
- NEW_DIVE = -> v { DiveForwarder.new(v) }
46
-
47
- # Creates a new delegator that understands the pattern matching interface
48
- #
49
- # @param base_object [Any]
50
- # Any object that does not necessarily understand pattern matching
51
- #
52
- # @return [DiveForwarder]
53
- def initialize(base_object)
54
- @base_object = base_object
55
- end
56
-
57
- # Approximation of an Array deconstruction:
58
- #
59
- # ```ruby
60
- # [1, 2, 3] in [*, 2, *]
61
- # ```
62
- #
63
- # Attempts to find a reasonable interface by which to extract values
64
- # to be matched. If an object that knows how to match already is sent
65
- # through wrap its child values for deeper matching.
66
- #
67
- # Current interface will work with `to_a`, `to_ary`, `map`, and values
68
- # that can already `deconstruct`. If others are desired please submit a PR
69
- # to add them
70
- #
71
- # @raises [Dio::Errors::NoDeconstructionMethod]
72
- # If no method of deconstruction exists, an exception is raised to
73
- # communicate the proper interface and note the abscense of a current one.
74
- #
75
- # @return [Array[DiveForwarder]]
76
- # Values lifted into a Dio context for further matching
77
- def deconstruct
78
- return @base_object.deconstruct.map!(&NEW_DIVE) if @base_object.respond_to?(:deconstruct)
79
-
80
- return @base_object.to_a.map!(&NEW_DIVE) if @base_object.respond_to?(:to_a)
81
- return @base_object.to_ary.map!(&NEW_DIVE) if @base_object.respond_to?(:to_ary)
82
- return @base_object.map(&NEW_DIVE) if @base_object.respond_to?(:map)
83
-
84
- raise Dio::Errors::NoDeconstructionMethod
85
- end
86
-
87
- # Approximates `deconstruct_keys` for Hashes, except in adding `Qo`-like
88
- # behavior that allows to treat objects as "nested values" through their
89
- # method calls.
90
- #
91
- # **Deconstructing an Object**
92
- #
93
- # In `Qo` one could match against an object by calling to its methods using
94
- # `public_send`. This allowed one to "dive into" an object through a series
95
- # of method calls, approximating a Hash pattern match.
96
- #
97
- # **Native Behavior**
98
- #
99
- # If the object already responds to `deconstruct_keys` this method will
100
- # behave similarly to `deconstruct` and wrap its values as new
101
- # `DiveForwarder` contexts.
102
- #
103
- # @param keys [Array]
104
- # Keys to be extracted from the object
105
- #
106
- # @return [Hash]
107
- # Deconstructed keys pointing to associated values extracted from a Hash
108
- # or an Object. Note that these values are matched against using `===`.
109
- def deconstruct_keys(keys)
110
- if @base_object.respond_to?(:deconstruct_keys)
111
- @base_object
112
- .deconstruct_keys(*keys)
113
- .transform_values!(&NEW_DIVE)
114
- else
115
- keys.to_h { |k| @base_object.public_send(k).then { |v| [k, NEW_DIVE[v]] } }
116
- end
117
- end
118
-
119
- # Unwrapped context, aliased afterwards to use Ruby's delegator interface
120
- #
121
- # @return [Any]
122
- # Originally wrapped object
123
- def value
124
- @base_object
125
- end
126
-
127
- alias_method :__getobj__, :value
128
- end
129
- end