qo 0.5.0 → 0.99.0

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
- SHA1:
3
- metadata.gz: 02dea6ad061112d957032940212ab369e09b0ec1
4
- data.tar.gz: '037591f1119e010e7faba94c51f1ed4678cafe45'
2
+ SHA256:
3
+ metadata.gz: 58ef9c82490e44396774612fd230c2056cb45588536800816ed69033e700b8c6
4
+ data.tar.gz: 250b1a8d1e619a6f2910dc40637e5c06e5b51ac05eeb294a2285315232d7cdff
5
5
  SHA512:
6
- metadata.gz: b01330722400077045a769eaaccc565ad2132f0413624b510f370754bfda109dac6ffcf1ea6cd2ce6987182d19cceb773bd04e67f49c990eef84f7a8774d53ea
7
- data.tar.gz: df76eed79e30353032930972ef916a7788500753b4e0fd684cf42be4c8f2f2dded8493c9e43ed52e71c2023291952f1b2b6c319dfca0de9b9b64b951ed6dc1d7
6
+ metadata.gz: ea272d67050323df29d83d8a7c7f6f7528f1a58e39aeec146d6902c0abdf1db42bf1fd4577f255487efb0ce927b6d139d7d3b9d0e386b30c5c4f3486369abf6f
7
+ data.tar.gz: 05cff280a0d79b3cbeb5a7114d8c05d2c3ea1d317ea4b346ea9aa17785af7937333da79c75ebb839d1e047287a804e94d2112cec78b04d01c67689c9aab7ebe1
@@ -1,7 +1,7 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.7
5
- - 2.4.4
6
- - 2.5.1
4
+ - 2.4.5
5
+ - 2.5.3
6
+ - 2.6.1
7
7
  before_install: gem install bundler -v 1.15.4
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
- ### 6 - Helper functions
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
- #### 6.1 - Dig
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
- #### 6.2 - Count By
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
- ### 5 - Hacky Fun Time
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
- #### 5.1 - JSON and HTTP
610
+ #### 7.1 - JSON and HTTP
531
611
 
532
612
  > Note that Qo does not support deep querying of hashes (yet)
533
613
 
534
- ##### 5.1.1 - JSON Placeholder
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
- #### 5.2 - Opsy Stuff
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
- ##### 5.2.1 - NMap
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
- ##### 5.2.2 - `df`
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.match { |m|
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.case(target) { |m|
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
- IDENTITY = -> v { v }
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'