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 +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +1 -1
- data/README.md +160 -2
- data/lib/dio.rb +1 -1
- data/lib/dio/errors.rb +12 -0
- data/lib/dio/forwarders.rb +8 -0
- data/lib/dio/forwarders/attribute_forwarder.rb +76 -0
- data/lib/dio/forwarders/base_forwarder.rb +131 -0
- data/lib/dio/forwarders/string_hash_forwarder.rb +63 -0
- data/lib/dio/public_api.rb +30 -1
- data/lib/dio/version.rb +1 -1
- metadata +7 -3
- data/lib/dio/dive_forwarder.rb +0 -129
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b059e97a8c1f5e6d1d68b445e51c553c14657ec837de6b61bbe1ce6bd8ccc106
|
4
|
+
data.tar.gz: 327197a40cc4cc1a3e0f814c60bb0f78bab24bff8b8012d69430a93454712ad2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9fd3543e1c9fca221b21db3629467b1f481c41e0a8191057e1d72bcfe1d190e859e902453e7292b45bb11a48ba56328582ff80d2dc10926aeca3d9a8b0dac11
|
7
|
+
data.tar.gz: '0194555bdc1bc59719e7c09167c26a7159ceff987dbb200754623694e3d49b2e1b87cf0ded4bfb4ac4af2eee05a1df9e2cb6f160bb95e889b6be3705caf55cdf'
|
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
@@ -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
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -15,8 +15,166 @@ match against it.
|
|
15
15
|
|
16
16
|
## Usage
|
17
17
|
|
18
|
-
|
19
|
-
|
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
data/lib/dio/errors.rb
CHANGED
@@ -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,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
|
data/lib/dio/public_api.rb
CHANGED
@@ -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::
|
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
|
data/lib/dio/version.rb
CHANGED
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.
|
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-
|
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
|
data/lib/dio/dive_forwarder.rb
DELETED
@@ -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
|