qo 0.5.0 → 0.99.0
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 +5 -5
- data/.travis.yml +3 -3
- data/README.md +106 -9
- data/Rakefile +3 -3
- data/lib/qo.rb +17 -15
- data/lib/qo/branches/branch.rb +191 -0
- data/lib/qo/branches/branches.rb +37 -0
- data/lib/qo/branches/else_branch.rb +20 -0
- data/lib/qo/branches/error_branch.rb +26 -0
- data/lib/qo/branches/failure_branch.rb +26 -0
- data/lib/qo/branches/monadic_else_branch.rb +26 -0
- data/lib/qo/branches/monadic_when_branch.rb +26 -0
- data/lib/qo/branches/success_branch.rb +26 -0
- data/lib/qo/branches/when_branch.rb +21 -0
- data/lib/qo/destructurers/destructurer.rb +85 -0
- data/lib/qo/destructurers/destructurers.rb +15 -0
- data/lib/qo/exceptions.rb +3 -32
- data/lib/qo/matchers/matcher.rb +298 -0
- data/lib/qo/pattern_matchers/branching.rb +52 -0
- data/lib/qo/pattern_matchers/pattern_match.rb +170 -0
- data/lib/qo/pattern_matchers/pattern_matchers.rb +13 -0
- data/lib/qo/pattern_matchers/result_pattern_match.rb +45 -0
- data/lib/qo/public_api.rb +130 -8
- data/lib/qo/version.rb +1 -1
- data/qo.gemspec +11 -0
- metadata +28 -11
- data/lib/qo/helpers.rb +0 -44
- data/lib/qo/matchers/array_matcher.rb +0 -99
- data/lib/qo/matchers/base_matcher.rb +0 -110
- data/lib/qo/matchers/guard_block_matcher.rb +0 -91
- data/lib/qo/matchers/hash_matcher.rb +0 -144
- data/lib/qo/matchers/pattern_match.rb +0 -126
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 58ef9c82490e44396774612fd230c2056cb45588536800816ed69033e700b8c6
|
4
|
+
data.tar.gz: 250b1a8d1e619a6f2910dc40637e5c06e5b51ac05eeb294a2285315232d7cdff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea272d67050323df29d83d8a7c7f6f7528f1a58e39aeec146d6902c0abdf1db42bf1fd4577f255487efb0ce927b6d139d7d3b9d0e386b30c5c4f3486369abf6f
|
7
|
+
data.tar.gz: 05cff280a0d79b3cbeb5a7114d8c05d2c3ea1d317ea4b346ea9aa17785af7937333da79c75ebb839d1e047287a804e94d2112cec78b04d01c67689c9aab7ebe1
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -482,11 +482,11 @@ people_objects.map(&Qo.match { |m|
|
|
482
482
|
|
483
483
|
So we just truncated everyone's name that was longer than three characters.
|
484
484
|
|
485
|
-
###
|
485
|
+
### 5 - Helper functions
|
486
486
|
|
487
487
|
There are a few functions added for convenience, and it should be noted that because all Qo matchers respond to `===` that they can be used as helpers as well.
|
488
488
|
|
489
|
-
####
|
489
|
+
#### 5.1 - Dig
|
490
490
|
|
491
491
|
Dig is used to get in deep at a nested hash value. It takes a dot-path and a `===` respondent matcher:
|
492
492
|
|
@@ -500,7 +500,7 @@ Qo.dig('a.b.c', Qo.or(1..5, 15..25)) === {a: {b: {c: 20}}}
|
|
500
500
|
|
501
501
|
To be fair that means anything that can respond to `===`, including classes and other such things.
|
502
502
|
|
503
|
-
####
|
503
|
+
#### 5.2 - Count By
|
504
504
|
|
505
505
|
This ends up coming up a lot, especially around querying, so let's get a way to count by!
|
506
506
|
|
@@ -523,15 +523,95 @@ Qo.count_by([1,2,3,2,2,2,1], &:even?)
|
|
523
523
|
|
524
524
|
This feature may be added to Ruby 2.6+: https://bugs.ruby-lang.org/issues/11076
|
525
525
|
|
526
|
-
###
|
526
|
+
### 6 - Custom Pattern Matchers
|
527
|
+
|
528
|
+
With the release of Qo 1.0.0 we introduced the idea of custom branches and pattern matchers for more advanced
|
529
|
+
users of the library.
|
530
|
+
|
531
|
+
Consider a Monadic type like `Some` and `None`:
|
532
|
+
|
533
|
+
```ruby
|
534
|
+
# Technically Some and None don't exist yet, so we have to "cheat" instead
|
535
|
+
# of just saying `Some` for the precondition
|
536
|
+
#
|
537
|
+
# We start by defining two branches that match against a Some type and a None
|
538
|
+
# type, extracting the value on match before yielding to their associated
|
539
|
+
# functions
|
540
|
+
SomeBranch = Qo.create_branch(
|
541
|
+
name: 'some',
|
542
|
+
precondition: -> v { v.is_a?(Some) },
|
543
|
+
extractor: :value
|
544
|
+
)
|
545
|
+
|
546
|
+
NoneBranch = Qo.create_branch(
|
547
|
+
name: 'none',
|
548
|
+
precondition: -> v { v.is_a?(None) },
|
549
|
+
extractor: :value
|
550
|
+
)
|
551
|
+
|
552
|
+
# Now we create a new pattern matching class with those branches. Note that
|
553
|
+
# there's nothing stopping you from making as many branches as you want,
|
554
|
+
# except that it may get confusing after a while.
|
555
|
+
SomePatternMatch = Qo.create_pattern_match(branches: [
|
556
|
+
SomeBranch,
|
557
|
+
NoneBranch
|
558
|
+
])
|
559
|
+
|
560
|
+
class Some
|
561
|
+
# There's also a provided mixin that gives an `match` method that
|
562
|
+
# works exactly like a pattern match without having to use it explicitly
|
563
|
+
include SomePatternMatch.mixin
|
564
|
+
|
565
|
+
attr_reader :value
|
566
|
+
|
567
|
+
def initialize(value) @value = value end
|
568
|
+
def self.[](value) new(value) end
|
569
|
+
|
570
|
+
def fmap(&fn)
|
571
|
+
new_value = fn.call(value)
|
572
|
+
new_value ? Some[new_value] : None[value]
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
class None
|
577
|
+
include SomePatternMatch.mixin
|
578
|
+
|
579
|
+
attr_reader :value
|
580
|
+
|
581
|
+
def initialize(value) @value = value end
|
582
|
+
def self.[](value) new(value) end
|
583
|
+
|
584
|
+
def fmap(&fn) None[value] end
|
585
|
+
end
|
586
|
+
|
587
|
+
# So now we can pattern match with `some` and `none` branches using the `match`
|
588
|
+
# method that was mixed into both types.
|
589
|
+
Some[1]
|
590
|
+
.fmap { |v| v * 2 }
|
591
|
+
.match { |m|
|
592
|
+
m.some { |v| v + 100 }
|
593
|
+
m.none { "OHNO!" }
|
594
|
+
}
|
595
|
+
=> 102
|
596
|
+
|
597
|
+
Some[1]
|
598
|
+
.fmap { |v| nil }
|
599
|
+
.match { |m|
|
600
|
+
m.some { |v| v + 100 }
|
601
|
+
m.none { "OHNO!" }
|
602
|
+
}
|
603
|
+
=> "OHNO!"
|
604
|
+
```
|
605
|
+
|
606
|
+
### 7 - Hacky Fun Time
|
527
607
|
|
528
608
|
These examples will grow over the next few weeks as I think of more fun things to do with Qo. PRs welcome if you find fun uses!
|
529
609
|
|
530
|
-
####
|
610
|
+
#### 7.1 - JSON and HTTP
|
531
611
|
|
532
612
|
> Note that Qo does not support deep querying of hashes (yet)
|
533
613
|
|
534
|
-
#####
|
614
|
+
##### 7.1.1 - JSON Placeholder
|
535
615
|
|
536
616
|
Qo tries to be clever though, it assumes Symbol keys first and then String keys, so how about some JSON?:
|
537
617
|
|
@@ -602,9 +682,26 @@ m.when(Net::HTTPSuccess, body: /Qo/)
|
|
602
682
|
You could put as many checks as you want in there, or use different Qo matchers
|
603
683
|
nested to get even further in.
|
604
684
|
|
605
|
-
|
685
|
+
Now if we wanted to add more power and create an HTTP matcher:
|
686
|
+
|
687
|
+
```ruby
|
688
|
+
HTTP_Matcher = Qo.create_pattern_match(branches: [
|
689
|
+
Qo.create_branch(name: 'success', precondition: Net::HTTPSuccess),
|
690
|
+
Qo.create_branch(name: 'error', precondition: Net::HTTPError),
|
691
|
+
Qo::Braches::ElseBranch
|
692
|
+
])
|
693
|
+
|
694
|
+
def get_url(url)
|
695
|
+
Net::HTTP.get_response(URI(url)).then(&HTTP_Matcher.match { |m|
|
696
|
+
m.success { |response| response.body.size },
|
697
|
+
m.else { |response| raise response.message }
|
698
|
+
})
|
699
|
+
end
|
700
|
+
```
|
701
|
+
|
702
|
+
#### 7.2 - Opsy Stuff
|
606
703
|
|
607
|
-
#####
|
704
|
+
##### 7.2.1 - NMap
|
608
705
|
|
609
706
|
What about NMap for our Opsy friends? Well, simulated, but still fun.
|
610
707
|
|
@@ -616,7 +713,7 @@ hosts.select(&Qo[IPAddr.new('192.168.1.1/8')])
|
|
616
713
|
=> [["192.168.1.1", "(Router)"], ["192.168.1.2", "(My Computer)"]]
|
617
714
|
```
|
618
715
|
|
619
|
-
#####
|
716
|
+
##### 7.2.2 - `df`
|
620
717
|
|
621
718
|
The nice thing about Unix style commands is that they use headers, which means CSV can get a hold of them for some good formatting. It's also smart enough to deal with space separators that may vary in length:
|
622
719
|
|
data/Rakefile
CHANGED
@@ -149,7 +149,7 @@ end
|
|
149
149
|
|
150
150
|
task :perf_pattern_match do
|
151
151
|
# Going to redefine the way that success and fail happen in here.
|
152
|
-
return false
|
152
|
+
# return false
|
153
153
|
|
154
154
|
require 'dry-matcher'
|
155
155
|
|
@@ -170,13 +170,13 @@ task :perf_pattern_match do
|
|
170
170
|
# Build the matcher
|
171
171
|
matcher = Dry::Matcher.new(success: success_case, failure: failure_case)
|
172
172
|
|
173
|
-
qo_m = Qo.
|
173
|
+
qo_m = Qo.result_match { |m|
|
174
174
|
m.success(Any) { |v| v }
|
175
175
|
m.failure(Any) { "ERR!" }
|
176
176
|
}
|
177
177
|
|
178
178
|
qo_m_case = proc { |target|
|
179
|
-
Qo.
|
179
|
+
Qo.result_case(target) { |m|
|
180
180
|
m.success(Any) { |v| v }
|
181
181
|
m.failure(Any) { "ERR!" }
|
182
182
|
}
|
data/lib/qo.rb
CHANGED
@@ -3,27 +3,29 @@ require 'any'
|
|
3
3
|
|
4
4
|
require "qo/version"
|
5
5
|
|
6
|
-
# Matchers
|
7
|
-
require 'qo/matchers/base_matcher'
|
8
|
-
require 'qo/matchers/array_matcher'
|
9
|
-
require 'qo/matchers/hash_matcher'
|
10
|
-
require 'qo/matchers/guard_block_matcher'
|
11
|
-
|
12
|
-
# Meta Matchers
|
13
|
-
require 'qo/matchers/pattern_match'
|
14
|
-
|
15
|
-
# Helpers
|
16
|
-
require 'qo/helpers'
|
17
|
-
|
18
6
|
# Public API
|
19
7
|
require 'qo/exceptions'
|
20
8
|
require 'qo/public_api'
|
21
9
|
|
22
10
|
module Qo
|
23
|
-
# Identity function that returns its argument directly
|
24
|
-
|
11
|
+
# Identity function that returns its argument directly. Argument name is
|
12
|
+
# important, as it will extract the literal identity of the object in
|
13
|
+
# the case of a non-destructured match, and the object itself in the
|
14
|
+
# case of a destructured one.
|
15
|
+
IDENTITY = -> itself { itself }
|
25
16
|
|
26
17
|
extend Qo::Exceptions
|
27
|
-
extend Qo::Helpers
|
28
18
|
extend Qo::PublicApi
|
29
19
|
end
|
20
|
+
|
21
|
+
# Destructurers
|
22
|
+
require 'qo/destructurers/destructurers'
|
23
|
+
|
24
|
+
# Matchers
|
25
|
+
require 'qo/matchers/matcher'
|
26
|
+
|
27
|
+
# Branches
|
28
|
+
require 'qo/branches/branches'
|
29
|
+
|
30
|
+
# Pattern Matchers
|
31
|
+
require 'qo/pattern_matchers/pattern_matchers'
|
@@ -0,0 +1,191 @@
|
|
1
|
+
module Qo
|
2
|
+
module Branches
|
3
|
+
# ### Branches
|
4
|
+
#
|
5
|
+
# A branch is a particular branch of a pattern match. The default branches
|
6
|
+
# emulate a `case` statement. Consider a `case` statement like this:
|
7
|
+
#
|
8
|
+
# ```ruby
|
9
|
+
# case value
|
10
|
+
# when condition then first_return
|
11
|
+
# else second_return
|
12
|
+
# end
|
13
|
+
# ```
|
14
|
+
#
|
15
|
+
# With a Qo branch you would see something like this:
|
16
|
+
#
|
17
|
+
# ```ruby
|
18
|
+
# Qo.match { |m|
|
19
|
+
# m.when(condition) { first_return }
|
20
|
+
# m.else { second_return }
|
21
|
+
# }
|
22
|
+
# ```
|
23
|
+
#
|
24
|
+
# The `when` and `else` are the names the branch was "registered" with in
|
25
|
+
# `Qo::PatternMatchers::Branching`. The name becomes the method name that
|
26
|
+
# the associated matcher uses.
|
27
|
+
#
|
28
|
+
# ### Order of Execution
|
29
|
+
#
|
30
|
+
# A branch will execute in the following order:
|
31
|
+
#
|
32
|
+
# ```
|
33
|
+
# value -> precondition ? -> condition ? -> extractor -> destructurer
|
34
|
+
# ```
|
35
|
+
#
|
36
|
+
# Preconditions allow for things like type checks or any static condition
|
37
|
+
# that will remain constant across all matches. Think of them as abstracting
|
38
|
+
# a single condition to guard before the branch continues.
|
39
|
+
#
|
40
|
+
# Conditions are typical Qo matchers, as documented in the README. Upon a
|
41
|
+
# match, the branch will be considered matched and continue on to calling
|
42
|
+
# the associated block function.
|
43
|
+
#
|
44
|
+
# Extractors are used to pull a value out of a container type, such as
|
45
|
+
# `value` for monadic types or `last` for response array tuples.
|
46
|
+
#
|
47
|
+
# Lastly, if given, Destructurers will destructure an object. That means
|
48
|
+
# that the associated function now places great significance on the
|
49
|
+
# names of the arguments as they'll be used to extract values from the
|
50
|
+
# object that would have normally been returned.
|
51
|
+
#
|
52
|
+
# Destructuring can be a complicated topic, see the following article to
|
53
|
+
# find out more on how this works or see the README for examples:
|
54
|
+
#
|
55
|
+
# https://medium.com/rubyinside/destructuring-in-ruby-9e9bd2be0360
|
56
|
+
#
|
57
|
+
# ### Match Tuples
|
58
|
+
#
|
59
|
+
# Branches will respond with a tuple of (status, value). A status of false
|
60
|
+
# indicates a non-match, and a status or true indicates a match. This is done
|
61
|
+
# to ensure that truly `false` or `nil` returns are not swallowed by a
|
62
|
+
# match.
|
63
|
+
#
|
64
|
+
# A Pattern Match will use these statuses to find the first matching branch.
|
65
|
+
#
|
66
|
+
# @author baweaver
|
67
|
+
# @since 1.0.0
|
68
|
+
class Branch
|
69
|
+
# Representation of an unmatched value. These values are wrapped in array
|
70
|
+
# tuples to preserve legitimate `false` and `nil` values by indicating
|
71
|
+
# the status of the match in the first position and the returned value in
|
72
|
+
# the second.
|
73
|
+
UNMATCHED = [false, nil]
|
74
|
+
|
75
|
+
# Name of the branch, see the initializer for more information
|
76
|
+
attr_reader :name
|
77
|
+
|
78
|
+
# Creates an instance of a Branch
|
79
|
+
#
|
80
|
+
# @param name: [String]
|
81
|
+
# Name of the branch. This is what binds to the pattern match as a method,
|
82
|
+
# meaning a name of `where` will result in calling it as `m.where`.
|
83
|
+
#
|
84
|
+
# @param precondition: Any [Symbol, #===]
|
85
|
+
# A precondition to the branch being considered true. This is done for
|
86
|
+
# static conditions like a certain type or perhaps checking a tuple type
|
87
|
+
# like `[:ok, value]`.
|
88
|
+
#
|
89
|
+
# If a `Symbol` is given, Qo will coerce it into a proc. This is done to
|
90
|
+
# make a nicer shorthand for creating a branch.
|
91
|
+
#
|
92
|
+
# @param extractor: IDENTITY [Proc, Symbol]
|
93
|
+
# How to pull the value out of a target object when a branch matches before
|
94
|
+
# calling the associated function. For a monadic type this might be something
|
95
|
+
# like extracting the value before yielding to the given block.
|
96
|
+
#
|
97
|
+
# If a `Symbol` is given, Qo will coerce it into a proc. This is done to
|
98
|
+
# make a nicer shorthand for creating a branch.
|
99
|
+
#
|
100
|
+
# @param destructure: false
|
101
|
+
# Whether or not to destructure the given object before yielding to the
|
102
|
+
# associated block. This means that the given block now places great
|
103
|
+
# importance on the argument names, as they'll be used to extract values
|
104
|
+
# from the associated object by that same method name, or key name in the
|
105
|
+
# case of hashes.
|
106
|
+
#
|
107
|
+
# @param default: false [Boolean]
|
108
|
+
# Whether this branch is considered to be a default condition. This is
|
109
|
+
# done to ensure that a branch runs last after all other conditions have
|
110
|
+
# failed. An example of this would be an `else` branch.
|
111
|
+
#
|
112
|
+
# @return [Qo::Branches::Branch]
|
113
|
+
def initialize(name:, precondition: Any, extractor: IDENTITY, destructure: false, default: false)
|
114
|
+
@name = name
|
115
|
+
@precondition = precondition.is_a?(Symbol) ? precondition.to_proc : precondition
|
116
|
+
@extractor = extractor.is_a?(Symbol) ? extractor.to_proc : extractor
|
117
|
+
@destructure = destructure
|
118
|
+
@default = default
|
119
|
+
end
|
120
|
+
|
121
|
+
# A dynamic creator for new branch types to be made on the fly in programs.
|
122
|
+
# This exists to make new types of pattern matches to suit your own needs.
|
123
|
+
#
|
124
|
+
# Prefer the public API to using this method directly, `Qo.create_branch`,
|
125
|
+
# mostly because it's less typing.
|
126
|
+
#
|
127
|
+
# @see `.initialize` for parameter documentation
|
128
|
+
#
|
129
|
+
# @return [Class]
|
130
|
+
# new Class to be bound to a constant name, or used anonymously
|
131
|
+
def self.create(name:, precondition: Any, extractor: IDENTITY, destructure: false, default: false)
|
132
|
+
attributes = {
|
133
|
+
name: name,
|
134
|
+
precondition: precondition,
|
135
|
+
extractor: extractor,
|
136
|
+
destructure: destructure,
|
137
|
+
default: default
|
138
|
+
}
|
139
|
+
|
140
|
+
Class.new(Qo::Branches::Branch) do
|
141
|
+
define_method(:initialize) { super(**attributes) }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Whether or not this is a default branch
|
146
|
+
#
|
147
|
+
# @return [Boolean]
|
148
|
+
def default?
|
149
|
+
@default
|
150
|
+
end
|
151
|
+
|
152
|
+
# Uses the current configuration of the branch to create a matcher to
|
153
|
+
# be used in a pattern match. The returned proc can be passed a value
|
154
|
+
# that will return back a tuple of `(status, value)` to indicate whether
|
155
|
+
# or not a match was made with this branch.
|
156
|
+
#
|
157
|
+
# @param conditions [#===]
|
158
|
+
# A set of conditions to run against, typically a `Qo.and` matcher but
|
159
|
+
# could be anything that happens to respond to `===`.
|
160
|
+
#
|
161
|
+
# @param destructure: false [Boolean]
|
162
|
+
# Whether or not to run the extracted value through a destructure before
|
163
|
+
# yielding it to the associated block.
|
164
|
+
#
|
165
|
+
# @param &function [Proc]
|
166
|
+
# Function to be called if a matcher matches.
|
167
|
+
#
|
168
|
+
# @return [Proc[Any]] [description]
|
169
|
+
def create_matcher(conditions, destructure: @destructure, &function)
|
170
|
+
function ||= IDENTITY
|
171
|
+
|
172
|
+
destructurer = Destructurers::Destructurer.new(
|
173
|
+
destructure: destructure, &function
|
174
|
+
)
|
175
|
+
|
176
|
+
Proc.new { |value|
|
177
|
+
extracted_value = @extractor.call(value)
|
178
|
+
|
179
|
+
# If it's a default branch, return true, as conditions are redundant
|
180
|
+
next [true, destructurer.call(extracted_value)] if @default
|
181
|
+
|
182
|
+
if @precondition === value && conditions === extracted_value
|
183
|
+
[true, destructurer.call(extracted_value)]
|
184
|
+
else
|
185
|
+
UNMATCHED
|
186
|
+
end
|
187
|
+
}
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Qo
|
2
|
+
# In Qo, a Branch is one of the callable branches in a pattern match. Most
|
3
|
+
# commonly you'll see this expressed in a `where` or `else` branch, named
|
4
|
+
# as such to emulate a `case` statement:
|
5
|
+
#
|
6
|
+
# ```ruby
|
7
|
+
# Qo.match { |m|
|
8
|
+
# m.when(conditions) { code executed when matched }
|
9
|
+
# m.else { code executed otherwise }
|
10
|
+
# }
|
11
|
+
# ```
|
12
|
+
#
|
13
|
+
# More documentation on the creation of branches is available in `Branch`
|
14
|
+
#
|
15
|
+
# @see Qo::Branches::Branch
|
16
|
+
#
|
17
|
+
# @author baweaver
|
18
|
+
# @since 1.0.0
|
19
|
+
module Branches
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Base branch type which defines the interface
|
24
|
+
require 'qo/branches/branch'
|
25
|
+
|
26
|
+
# The branches you're probably used to with `where` and `else`
|
27
|
+
require 'qo/branches/when_branch'
|
28
|
+
require 'qo/branches/else_branch'
|
29
|
+
|
30
|
+
# Result type matchers for tuples like `[:ok, value]`
|
31
|
+
require 'qo/branches/success_branch'
|
32
|
+
require 'qo/branches/error_branch'
|
33
|
+
require 'qo/branches/failure_branch'
|
34
|
+
|
35
|
+
# Monadic matchers to extract values from items
|
36
|
+
require 'qo/branches/monadic_when_branch'
|
37
|
+
require 'qo/branches/monadic_else_branch'
|