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