qo 0.4.0 → 0.5.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
- SHA256:
3
- metadata.gz: 2dcdd32a9c1743ae07fee6699551ca57393c751ce8172950edea505013e5a628
4
- data.tar.gz: 4b39a86b816b039c3282a5b0d307afb796f5dabed249cf3b658462a5bd791ece
2
+ SHA1:
3
+ metadata.gz: 02dea6ad061112d957032940212ab369e09b0ec1
4
+ data.tar.gz: '037591f1119e010e7faba94c51f1ed4678cafe45'
5
5
  SHA512:
6
- metadata.gz: bb43a7e3a7e2134d477149548c1de8de6961fa62a8da1c606ffa743287d95cb05bd5d400174e343297cf9c21a205f32aa60500abbb32c094c1cac4699558f18f
7
- data.tar.gz: 481c3331b0e1328b8e6eae0fd8c34ca54df0f58204cf8a0dc0cfff568343af618450a5978c1132e3a250da474c7dfd90fc4df3132ecc99f80095a1fc485b7faa
6
+ metadata.gz: b01330722400077045a769eaaccc565ad2132f0413624b510f370754bfda109dac6ffcf1ea6cd2ce6987182d19cceb773bd04e67f49c990eef84f7a8774d53ea
7
+ data.tar.gz: df76eed79e30353032930972ef916a7788500753b4e0fd684cf42be4c8f2f2dded8493c9e43ed52e71c2023291952f1b2b6c319dfca0de9b9b64b951ed6dc1d7
data/.gitignore CHANGED
@@ -14,3 +14,6 @@
14
14
  # built gems
15
15
  *.gem
16
16
  repro_cases/*.rb
17
+
18
+ # Junk files
19
+ .DS_Store
data/README.md CHANGED
@@ -52,43 +52,51 @@ How about some pattern matching? There are two styles:
52
52
 
53
53
  #### Pattern Match
54
54
 
55
- The original style
55
+ ##### Case Statements
56
+
57
+ Qo case statements work much like a Ruby case statement, except in that they leverage
58
+ the full power of Qo matchers behind the scenes.
56
59
 
57
60
  ```ruby
58
61
  # How about some "right-hand assignment" pattern matching
59
- name_longer_than_three = -> person { person.name.size > 3 }
60
- people_with_truncated_names = people.map(&Qo.match(
61
- Qo.m(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) },
62
- Qo.m(Any) # Identity function, catch-all
63
- ))
62
+ name_longer_than_three = -> person { person.name.size > 3 }
64
63
 
65
- # And standalone like a case:
66
- Qo.match(people.first,
67
- Qo.m(age: 10..19) { |person| "#{person.name} is a teen that's #{person.age} years old" },
68
- Qo.m(Any) { |person| "#{person.name} is #{person.age} years old" }
69
- )
64
+ person_with_truncated_name = Qo.case(people.first) { |m|
65
+ m.when(name_longer_than_three) { |person|
66
+ Person.new(person.name[0..2], person.age)
67
+ }
68
+
69
+ m.else
70
+ }
70
71
  ```
71
72
 
72
- #### Pattern Match Block
73
+ It takes in a value directly, and returns the result, much like a case statement.
74
+
75
+ Note that if `else` receives no block, it will default to an identity function
76
+ (`{ |v| v }`). If no else is provided and there's no match, you'll get back a nil.
77
+ You can write this out if you wish.
73
78
 
74
- The new style, likely to take over in `v1.0.0` after testing:
79
+ ##### Match Statements
80
+
81
+ Match statements are like case statements, except in that they don't directly take
82
+ a value to match against. They're waiting for a value to come in later from
83
+ something else.
75
84
 
76
85
  ```ruby
77
- name_longer_than_three = -> person { person.name.size > 3 }
86
+ name_longer_than_three = -> person { person.name.size > 3 }
87
+
78
88
  people_with_truncated_names = people.map(&Qo.match { |m|
79
89
  m.when(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) }
80
- m.else(&:itself)
90
+ m.else
81
91
  })
82
92
 
83
93
  # And standalone like a case:
84
- Qo.match(people.first) { |m|
94
+ Qo.match { |m|
85
95
  m.when(age: 10..19) { |person| "#{person.name} is a teen that's #{person.age} years old" }
86
96
  m.else { |person| "#{person.name} is #{person.age} years old" }
87
- }
97
+ }.call(people.first)
88
98
  ```
89
99
 
90
- (More details coming on the difference and planned 1.0.0 APIs)
91
-
92
100
  ### Qo'isms
93
101
 
94
102
  Qo supports three main types of queries: `and`, `or`, and `not`.
@@ -439,18 +447,18 @@ people_hashes.select(&Qo[age: :nil?])
439
447
  This is where I start going a bit off into the weeds. We're going to try and get RHA style pattern matching in Ruby.
440
448
 
441
449
  ```ruby
442
- Qo.match(['Robert', 22],
443
- Qo.m(Any, 20..99) { |n, a| "#{n} is an adult that is #{a} years old" },
444
- Qo.m(Any)
445
- )
450
+ Qo.case(['Robert', 22]) { |m|
451
+ m.when(Any, 20..99) { |n, a| "#{n} is an adult that is #{a} years old" }
452
+ m.else
453
+ }
446
454
  # => "Robert is an adult that is 22 years old"
447
455
  ```
448
456
 
449
457
  ```ruby
450
- Qo.match(people_objects.first,
451
- Qo.m(name: Any, age: 20..99) { |person| "#{person.name} is an adult that is #{person.age} years old" },
452
- Qo.m(Any)
453
- )
458
+ Qo.case(people_objects.first) { |m|
459
+ m.when(name: Any, age: 20..99) { |person| "#{person.name} is an adult that is #{person.age} years old" }
460
+ m.else
461
+ }
454
462
  ```
455
463
 
456
464
  In this case it's trying to do a few things:
@@ -460,18 +468,14 @@ In this case it's trying to do a few things:
460
468
 
461
469
  If no block function is provided, it assumes an identity function (`-> v { v }`) instead. If no match is found, `nil` will be returned.
462
470
 
463
- If an initial target is not furnished, the matcher will become a curried proc awaiting a target. In more simple terms it just wants a target to run against, so let's give it a few with map:
464
-
465
471
  ```ruby
466
472
  name_longer_than_three = -> person { person.name.size > 3 }
467
473
 
468
- people_objects.map(&Qo.match(
469
- Qo.m(name_longer_than_three) { |person|
470
- person.name = person.name[0..2]
471
- person
472
- },
473
- Qo.m(Any)
474
- ))
474
+ people_objects.map(&Qo.match { |m|
475
+ m.when(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) }
476
+
477
+ m.else
478
+ })
475
479
 
476
480
  # => [Person(age: 22, name: "Rob"), Person(age: 22, name: "Rob"), Person(age: 42, name: "Foo"), Person(age: 17, name: "Bar")]
477
481
  ```
@@ -517,6 +521,8 @@ Qo.count_by([1,2,3,2,2,2,1], &:even?)
517
521
  # }
518
522
  ```
519
523
 
524
+ This feature may be added to Ruby 2.6+: https://bugs.ruby-lang.org/issues/11076
525
+
520
526
  ### 5 - Hacky Fun Time
521
527
 
522
528
  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!
@@ -573,10 +579,10 @@ HTTP responses.
573
579
 
574
580
  ```ruby
575
581
  def get_url(url)
576
- Net::HTTP.get_response(URI(url)).yield_self(&Qo.match(
577
- Qo.m(Net::HTTPSuccess) { |response| response.body.size },
578
- Qo.m(Any) { |response| raise response.message }
579
- ))
582
+ Net::HTTP.get_response(URI(url)).yield_self(&Qo.match { |m|
583
+ m.when(Net::HTTPSuccess) { |response| response.body.size },
584
+ m.else { |response| raise response.message }
585
+ })
580
586
  end
581
587
 
582
588
  get_url('https://github.com/baweaver/qo')
@@ -590,7 +596,7 @@ The difference between this and case? Well, the first is you can pass this to
590
596
  be used in there, including predicate and content checks on the body:
591
597
 
592
598
  ```ruby
593
- Qo.m(Net::HTTPSuccess, body: /Qo/)
599
+ m.when(Net::HTTPSuccess, body: /Qo/)
594
600
  ```
595
601
 
596
602
  You could put as many checks as you want in there, or use different Qo matchers
@@ -617,11 +623,11 @@ The nice thing about Unix style commands is that they use headers, which means C
617
623
  ```ruby
618
624
  rows = CSV.new(`df -h`, col_sep: " ", headers: true).read.map(&:to_h)
619
625
 
620
- rows.map(&Qo.match(
621
- Qo.m(Avail: /Gi$/) { |row|
626
+ rows.map(&Qo.match { |m|
627
+ m.when(Avail: /Gi$/) { |row|
622
628
  "#{row['Filesystem']} mounted on #{row['Mounted']} [#{row['Avail']} / #{row['Size']}]"
623
629
  }
624
- )).compact
630
+ }).compact
625
631
  # => ["/dev/***** mounted on / [186Gi / 466Gi]"]
626
632
  ```
627
633
 
data/Rakefile CHANGED
@@ -19,13 +19,13 @@ task :default => :spec
19
19
  # "anything can be a key" bit of Ruby.
20
20
  #
21
21
  # @return [Unit] StdOut
22
- def run_benchmark(title, **benchmarks)
22
+ def run_benchmark(title, quiet = false, **benchmarks)
23
23
  puts '', title, '=' * title.size, ''
24
24
 
25
25
  # Validation
26
26
  benchmarks.each do |benchmark_name, benchmark_fn|
27
27
  puts "#{benchmark_name} result: #{benchmark_fn.call()}"
28
- end
28
+ end unless quiet
29
29
 
30
30
  puts
31
31
 
@@ -147,6 +147,110 @@ task :perf do
147
147
  )
148
148
  end
149
149
 
150
+ task :perf_pattern_match do
151
+ # Going to redefine the way that success and fail happen in here.
152
+ return false
153
+
154
+ require 'dry-matcher'
155
+
156
+ # Match `[:ok, some_value]` for success
157
+ success_case = Dry::Matcher::Case.new(
158
+ match: -> value { value.first == :ok },
159
+ resolve: -> value { value.last }
160
+ )
161
+
162
+ # Match `[:err, some_error_code, some_value]` for failure
163
+ failure_case = Dry::Matcher::Case.new(
164
+ match: -> value, *pattern {
165
+ value[0] == :err && (pattern.any? ? pattern.include?(value[1]) : true)
166
+ },
167
+ resolve: -> value { value.last }
168
+ )
169
+
170
+ # Build the matcher
171
+ matcher = Dry::Matcher.new(success: success_case, failure: failure_case)
172
+
173
+ qo_m = Qo.match { |m|
174
+ m.success(Any) { |v| v }
175
+ m.failure(Any) { "ERR!" }
176
+ }
177
+
178
+ qo_m_case = proc { |target|
179
+ Qo.case(target) { |m|
180
+ m.success(Any) { |v| v }
181
+ m.failure(Any) { "ERR!" }
182
+ }
183
+ }
184
+
185
+ dm_m = proc { |target|
186
+ matcher.(target) do |m|
187
+ m.success { |(v)| v }
188
+ m.failure { 'ERR!' }
189
+ end
190
+ }
191
+
192
+ # v_m = proc { |target|
193
+ # next target[1] if target[0] == :ok
194
+ # 'ERR!'
195
+ # }
196
+
197
+ ok_target = [:ok, 12345]
198
+ err_target = [:err, "OH NO!"]
199
+
200
+ run_benchmark('Single Item Tuple',
201
+ 'Qo': -> {
202
+ "OK: #{qo_m[ok_target]}, ERR: #{qo_m[err_target]}"
203
+ },
204
+
205
+ 'Qo Case': -> {
206
+ "OK: #{qo_m_case[ok_target]}, ERR: #{qo_m_case[err_target]}"
207
+ },
208
+
209
+ 'DryRB': -> {
210
+ "OK: #{dm_m[ok_target]}, ERR: #{dm_m[err_target]}"
211
+ },
212
+
213
+ # 'Vanilla': -> {
214
+ # "OK: #{v_m[ok_target]}, ERR: #{v_m[err_target]}"
215
+ # },
216
+ )
217
+
218
+ collection = [ok_target, err_target] * 2_000
219
+
220
+ run_benchmark('Large Tuple Collection', true,
221
+ 'Qo': -> { collection.map(&qo_m) },
222
+ 'Qo Case': -> { collection.map(&qo_m_case) },
223
+ 'DryRB': -> { collection.map(&dm_m) },
224
+ # 'Vanilla': -> { collection.map(&v_m) }
225
+ )
226
+
227
+ # Person = Struct.new(:name, :age)
228
+ # people = [
229
+ # Person.new('Robert', 22),
230
+ # Person.new('Roberta', 22),
231
+ # Person.new('Foo', 42),
232
+ # Person.new('Bar', 17)
233
+ # ] * 1_000
234
+
235
+ # v_om = proc { |target|
236
+ # if /^F/.match?(target.name) && (30..50).include?(target.age)
237
+ # "It's foo!"
238
+ # else
239
+ # "Not foo"
240
+ # end
241
+ # }
242
+
243
+ # qo_om = Qo.match { |m|
244
+ # m.when(name: /^F/, age: 30..50) { "It's foo!" }
245
+ # m.else { "Not foo" }
246
+ # }
247
+
248
+ # run_benchmark('Large Object Collection', true,
249
+ # 'Qo': -> { people.map(&qo_om) },
250
+ # 'Vanilla': -> { people.map(&v_om) }
251
+ # )
252
+ end
253
+
150
254
  # Below this mark are mostly my experiments to see what features perform a bit better
151
255
  # than others, and are mostly left to check different versions of Ruby against eachother.
152
256
  #
data/lib/qo.rb CHANGED
@@ -11,7 +11,6 @@ require 'qo/matchers/guard_block_matcher'
11
11
 
12
12
  # Meta Matchers
13
13
  require 'qo/matchers/pattern_match'
14
- require 'qo/matchers/pattern_match_block'
15
14
 
16
15
  # Helpers
17
16
  require 'qo/helpers'
@@ -21,6 +20,9 @@ require 'qo/exceptions'
21
20
  require 'qo/public_api'
22
21
 
23
22
  module Qo
23
+ # Identity function that returns its argument directly
24
+ IDENTITY = -> v { v }
25
+
24
26
  extend Qo::Exceptions
25
27
  extend Qo::Helpers
26
28
  extend Qo::PublicApi
@@ -27,19 +27,6 @@ module Qo
27
27
  end
28
28
  end
29
29
 
30
- # In the case of a Pattern Match, we need to ensure all arguments are
31
- # GuardBlockMatchers.
32
- #
33
- # @author baweaver
34
- # @since 0.2.0
35
- #
36
- class NotAllGuardMatchersProvided < ArgumentError
37
- def to_s
38
- "All provided matchers must be of type Qo::Matchers::GuardBlockMatcher " +
39
- "defined with `Qo.matcher` or `Qo.m` instead of regular matchers."
40
- end
41
- end
42
-
43
30
  # In the case of a Pattern Match, we should only have one "else" clause
44
31
  #
45
32
  # @author baweaver
@@ -5,8 +5,11 @@ module Qo
5
5
  # @note This method will attempt to coerce path segments to Symbols
6
6
  # if unsuccessful in first dig.
7
7
  #
8
- # @param path_map [String] Dot-delimited path
9
- # @param expected_value [Any] Matcher
8
+ # @param path_map [String]
9
+ # Dot-delimited path
10
+ #
11
+ # @param expected_value [Any]
12
+ # Matcher
10
13
  #
11
14
  # @return [Proc]
12
15
  # Hash -> Bool # Status of digging against the hash
@@ -22,10 +25,14 @@ module Qo
22
25
  # Counts by a function. This is entirely because I hackney this everywhere in
23
26
  # pry anyways, so I want a function to do it for me already.
24
27
  #
25
- # @param targets [Array[Any]] Targets to count
26
- # @param &fn [Proc] Function to define count key
28
+ # @param targets [Array[Any]]
29
+ # Targets to count
30
+ #
31
+ # @param &fn [Proc]
32
+ # Function to define count key
27
33
  #
28
- # @return [Hash[Any, Integer]] Counts
34
+ # @return [Hash[Any, Integer]]
35
+ # Counts
29
36
  def count_by(targets, &fn)
30
37
  fn ||= -> v { v }
31
38
 
@@ -14,6 +14,14 @@ module Qo
14
14
  # # => true
15
15
  # ```
16
16
  #
17
+ # It should be noted that arrays of dissimilar size will result in an instant
18
+ # false return value. If you wish to do a single value match, simply use the
19
+ # provided `Any` type as such:
20
+ #
21
+ # ```ruby
22
+ # array.select(&Any)
23
+ # ```
24
+ #
17
25
  # In the case of an Array matching against an Object, it will match each provided
18
26
  # matcher against the object.
19
27
  #
@@ -33,6 +41,11 @@ module Qo
33
41
  # @since 0.2.0
34
42
  #
35
43
  class ArrayMatcher < BaseMatcher
44
+ def initialize(type, array_matchers)
45
+ @array_matchers = array_matchers
46
+ @type = type
47
+ end
48
+
36
49
  # Wrapper around call to allow for invocation in an Enumerable function,
37
50
  # such as:
38
51
  #
@@ -59,6 +72,8 @@ module Qo
59
72
  return true if @array_matchers == target
60
73
 
61
74
  if target.is_a?(::Array)
75
+ return false unless target.size == @array_matchers.size
76
+
62
77
  match_with(@array_matchers.each_with_index) { |matcher, i|
63
78
  match_value?(target[i], matcher)
64
79
  }
@@ -22,10 +22,12 @@ module Qo
22
22
  # @since 0.2.0
23
23
  #
24
24
  class BaseMatcher
25
- def initialize(type, *array_matchers, **keyword_matchers)
26
- @array_matchers = array_matchers
27
- @keyword_matchers = keyword_matchers
28
- @type = type
25
+ def initialize(type, array_matchers, keyword_matchers)
26
+ @type = type
27
+
28
+ @full_matcher = array_matchers.empty? ?
29
+ Qo::Matchers::HashMatcher.new(type, keyword_matchers) :
30
+ Qo::Matchers::ArrayMatcher.new(type, array_matchers)
29
31
  end
30
32
 
31
33
  # Converts a Matcher to a proc for use in querying, such as:
@@ -34,9 +36,7 @@ module Qo
34
36
  #
35
37
  # @return [Proc[Any]]
36
38
  def to_proc
37
- @array_matchers.empty? ?
38
- Qo::Matchers::HashMatcher.new(@type, **@keyword_matchers).to_proc :
39
- Qo::Matchers::ArrayMatcher.new(@type, *@array_matchers).to_proc
39
+ @full_matcher.to_proc
40
40
  end
41
41
 
42
42
  # You can directly call a matcher as well, much like a Proc,
@@ -46,11 +46,12 @@ module Qo
46
46
  #
47
47
  # @return [Boolean] Result of the match
48
48
  def call(target)
49
- self.to_proc.call(target)
49
+ @full_matcher.call(target)
50
50
  end
51
51
 
52
52
  alias_method :===, :call
53
53
  alias_method :[], :call
54
+ alias_method :match?, :call
54
55
 
55
56
  # Runs the relevant match method against the given collection with the
56
57
  # given matcher function.
@@ -60,10 +61,12 @@ module Qo
60
61
  #
61
62
  # @return [Boolean] Result of the match
62
63
  private def match_with(collection, &fn)
63
- return collection.any?(&fn) if @type == 'or'
64
- return collection.none?(&fn) if @type == 'not'
65
-
66
- collection.all?(&fn)
64
+ case @type
65
+ when 'and' then collection.all?(&fn)
66
+ when 'or' then collection.any?(&fn)
67
+ when 'not' then collection.none?(&fn)
68
+ else false
69
+ end
67
70
  end
68
71
 
69
72
  # Wraps a case equality statement to make it a bit easier to read. The
@@ -10,9 +10,9 @@ module Qo
10
10
  # you're going to want to read that documentation first.
11
11
  #
12
12
  # @example
13
- # Qo::Matchers::GuardBlockMatcher == Qo.matcher == Qo.m
13
+ # Qo::Matchers::GuardBlockMatcher
14
14
  #
15
- # guard_matcher = Qo.m(Integer) { |v| v * 2 }
15
+ # guard_matcher = Qo::Matchers::GuardBlockMatcher.new(Integer) { |v| v * 2 }
16
16
  # guard_matcher.call(1) # => [true, 2]
17
17
  # guard_matcher.call(:x) # => [false, false]
18
18
  #
@@ -23,28 +23,68 @@ module Qo
23
23
  # @since 0.1.5
24
24
  #
25
25
  class GuardBlockMatcher < BaseMatcher
26
- # Identity function that returns its argument directly
27
- IDENTITY = -> v { v }
28
-
29
26
  # Definition of a non-match
30
27
  NON_MATCH = [false, false]
31
28
 
32
- def initialize(*array_matchers, **keyword_matchers, &fn)
33
- @fn = fn || IDENTITY
29
+ def initialize(array_matchers, keyword_matchers, &fn)
30
+ @fn = fn || Qo::IDENTITY
31
+
32
+ super('and', array_matchers, keyword_matchers)
33
+ end
34
+
35
+ # Direct test of whether or not a target matches the GuardBlock's
36
+ # condition
37
+ #
38
+ # @param target [Any]
39
+ # Target value to match against
40
+ #
41
+ # @return [Boolean]
42
+ # Whether or not the target matched
43
+ def match?(target)
44
+ super(target)
45
+ end
34
46
 
35
- super('and', *array_matchers, **keyword_matchers)
47
+ # Forces a resolution of a match. Note that this method should
48
+ # not be used outside of pattern matches, as only a pattern
49
+ # match will have the necessary additional context to call
50
+ # it correctly.
51
+ #
52
+ # @param target [Any]
53
+ # Target value to match against
54
+ #
55
+ # @return [Any]
56
+ # Result of the function being called on the target
57
+ def match(target)
58
+ @fn.call(target)
36
59
  end
37
60
 
38
61
  # Overrides the base matcher's #to_proc to wrap the value in a status
39
62
  # and potentially call through to the associated block if a base
40
63
  # matcher would have passed
41
64
  #
42
- # @return [Proc[Any] - (Bool, Any)]
43
- # (status, result) tuple
65
+ # @see Qo::Matchers::GuardBlockMatcher#call
66
+ #
67
+ # @return [Proc[Any]]
68
+ # Function awaiting target value
44
69
  def to_proc
45
- Proc.new { |target|
46
- super[target] ? [true, @fn.call(target)] : NON_MATCH
47
- }
70
+ Proc.new { |target| self.call(target) }
71
+ end
72
+
73
+
74
+ # Overrides the call method to wrap values in a return tuple to represent
75
+ # a positive match to guard against valid falsy returns
76
+ #
77
+ # @param target [Any]
78
+ # Target value to match against
79
+ #
80
+ # @return [Array[false, false]]
81
+ # The guard block did not match
82
+ #
83
+ # @return [Array[true, Any]]
84
+ # The guard block matched, and the provided function called through
85
+ # providing a return value.
86
+ def call(target)
87
+ super(target) ? [true, @fn.call(target)] : NON_MATCH
48
88
  end
49
89
  end
50
90
  end
@@ -26,6 +26,11 @@ module Qo
26
26
  # @since 0.2.0
27
27
  #
28
28
  class HashMatcher < BaseMatcher
29
+ def initialize(type, keyword_matchers)
30
+ @keyword_matchers = keyword_matchers
31
+ @type = type
32
+ end
33
+
29
34
  # Wrapper around call to allow for invocation in an Enumerable function,
30
35
  # such as:
31
36
  #
@@ -95,6 +100,7 @@ module Qo
95
100
  # @return [Boolean]
96
101
  private def hash_case_match?(target, match_key, matcher)
97
102
  return true if case_match?(target[match_key], matcher)
103
+ return false unless target.keys.first.is_a?(String)
98
104
 
99
105
  match_key.respond_to?(:to_s) &&
100
106
  target.key?(match_key.to_s) &&
@@ -2,71 +2,94 @@ require 'qo/exceptions'
2
2
 
3
3
  module Qo
4
4
  module Matchers
5
- # Creates a PatternMatch, which will evaluate once given a match target.
5
+ # Creates a PatternMatch in a succinct block format:
6
6
  #
7
- # Each GuardBlockMatcher given will be run in sequence until a "match" is
8
- # located. Once that condition is met, it will call the associated block
9
- # function of that GuardBlockMatcher with the match target.
10
- #
11
- # This is done as an effort to emulate Right Hand Assignment seen in other
12
- # functionally oriented languages pattern matching systems. Most notably this
13
- # is done in Scala: (https://docs.scala-lang.org/tour/pattern-matching.html)
14
- #
15
- # ```scala
16
- # notification match {
17
- # case Email(email, title, _) =>
18
- # s"You got an email from $email with title: $title"
19
- #
20
- # case SMS(number, message) =>
21
- # s"You got an SMS from $number! Message: $message"
22
- #
23
- # case VoiceRecording(name, link) =>
24
- # s"You received a Voice Recording from $name! Click the link to hear it: $link"
25
- #
26
- # case other => other
7
+ # ```ruby
8
+ # Qo.match(target) { |m|
9
+ # m.when(/^F/, 42) { |(name, age)| "#{name} is #{age}" }
10
+ # m.else { "We need a default, right?" }
27
11
  # }
28
12
  # ```
29
13
  #
30
- # Qo will instead pipe the entire matched object, and might look something like this:
14
+ # The Public API obscures the fact that the matcher is only called when it
15
+ # is explicitly given an argument to match against. If it is not, it will
16
+ # just return a class waiting for a target, as such:
31
17
  #
32
18
  # ```ruby
33
- # # Assuming notification is in the form of a tuple
34
- #
35
- # Qo.match(notification,
36
- # Qo.m(EMAIL_REGEX, String, :*) { |email, title, _|
37
- # "You got an email from #{email} with title: #{title}"
38
- # },
39
- #
40
- # Qo.m(PHONE_REGEX, String) { |number, message, _|
41
- # "You got an SMS from #{number}! Message: #{message}"
42
- # },
43
- #
44
- # Qo.m(String, LINK_REGEX) { |name, link, _|
45
- # "You received a Voice Recording from #{name}! Click the link to hear it: #{link}"
46
- # },
47
- #
48
- # Qo.m(:*)
49
- # )
19
+ # def get_url(url)
20
+ # Net::HTTP.get_response(URI(url)).yield_self(&Qo.match { |m|
21
+ # m.when(Net::HTTPSuccess) { |response| response.body.size }
22
+ # m.else { |response| raise response.message }
23
+ # })
24
+ # end
25
+ #
26
+ # get_url('https://github.com/baweaver/qo')
27
+ # # => 142387
28
+ # get_url('https://github.com/baweaver/qo/does_not_exist')
29
+ # # => RuntimeError: Not Found
50
30
  # ```
51
31
  #
52
- # Efforts to emulate the case class mechanic may be present in later versions,
53
- # but for now the associated performance penalty may be too steep to consider.
32
+ # This is intended for flexibility between singular calls and calls as a
33
+ # paramater to higher order functions like `map` and `yield_self`.
54
34
  #
55
- # We'll evaluate those options in a few experiments later.
35
+ # This variant was inspired by ideas from Scala, Haskell, and various Ruby
36
+ # libraries dealing with Async and self-yielding blocks. Especially notable
37
+ # were websocket handlers and dry-ruby implementations.
56
38
  #
57
39
  # @author baweaver
58
- # @since 0.2.0
40
+ # @since 0.3.0
59
41
  #
60
42
  class PatternMatch
61
- def initialize(*matchers)
62
- raise Qo::Exceptions::NotAllGuardMatchersProvided unless matchers.all? { |q|
63
- q.is_a?(Qo::Matchers::GuardBlockMatcher)
64
- }
43
+ def initialize
44
+ @matchers = []
45
+
46
+ yield(self)
47
+ end
65
48
 
66
- @matchers = matchers
49
+ # Creates a match case. This is the exact same as any other `and` style
50
+ # match reflected in the public API, except that it's a Guard Block
51
+ # match being performed. That means if the left side matches, the right
52
+ # side function is invoked and that value is returned.
53
+ #
54
+ # @param *array_matchers [Array[Any]]
55
+ # Array style matchers
56
+ #
57
+ # @param **keyword_matchers [Hash[Any, Any]]
58
+ # Hash style matchers
59
+ #
60
+ # @param &fn [Proc]
61
+ # If matched, this function will be called. If no function is provided
62
+ # Qo will default to identity
63
+ #
64
+ # @return [Array[GuardBlockMatcher]]
65
+ # The return of this method should not be directly depended on, but will
66
+ # provide all matchers currently present. This will likely be left for
67
+ # ease of debugging later.
68
+ def when(*array_matchers, **keyword_matchers, &fn)
69
+ @matchers << Qo::Matchers::GuardBlockMatcher.new(
70
+ array_matchers,
71
+ keyword_matchers,
72
+ &(fn || Qo::IDENTITY)
73
+ )
67
74
  end
68
75
 
69
- # Function return of a PatternMatch waiting for a target to run
76
+ # Else is the last statement that will be evaluated if all other parts
77
+ # fail. It should be noted that it won't magically appear, you have to
78
+ # explicitly put an `else` case in for it to catch on no match unless
79
+ # you want a `nil` return
80
+ #
81
+ # @param &fn [Proc]
82
+ # Function to call when all other matches have failed. If no value is
83
+ # provided, it assumes `Qo::IDENTITY` which will return the value given.
84
+ #
85
+ # @return [Proc]
86
+ def else(&fn)
87
+ raise Qo::Exceptions::MultipleElseClauses if @else
88
+
89
+ @else = fn || Qo::IDENTITY
90
+ end
91
+
92
+ # Proc version of a PatternMatch
70
93
  #
71
94
  # @return [Proc]
72
95
  # Any -> Any | nil
@@ -80,15 +103,24 @@ module Qo
80
103
  # Target to run against and pipe to the associated block if it
81
104
  # "matches" any of the GuardBlocks
82
105
  #
83
- # @return [Any | nil] Result of the piped block, or nil on a miss
106
+ # @return [Any]
107
+ # Result of the piped block
108
+ #
109
+ # @return [nil]
110
+ # No matches were found, so nothing is returned
84
111
  def call(target)
85
112
  @matchers.each { |guard_block_matcher|
86
- did_match, match_result = guard_block_matcher.call(target)
87
- return match_result if did_match
113
+ next unless guard_block_matcher.match?(target)
114
+ return guard_block_matcher.match(target)
88
115
  }
89
116
 
117
+ return @else.call(target) if @else
118
+
90
119
  nil
91
120
  end
121
+
122
+ alias_method :===, :call
123
+ alias_method :[], :call
92
124
  end
93
125
  end
94
126
  end
@@ -15,8 +15,11 @@ module Qo
15
15
  # must pass to be considered a "match". It will short-circuit in the case of
16
16
  # a false match.
17
17
  #
18
- # @param *array_matchers [Array] Array-like conditionals
19
- # @param **keyword_matchers [Hash] Keyword style conditionals
18
+ # @param *array_matchers [Array]
19
+ # Array-like conditionals
20
+ #
21
+ # @param **keyword_matchers [Hash]
22
+ # Keyword style conditionals
20
23
  #
21
24
  # @return [Proc[Any]]
22
25
  # Any -> Bool # Given a target, will return if it "matches"
@@ -31,8 +34,11 @@ module Qo
31
34
  # must pass to be considered a "match". It will short-circuit in the case of
32
35
  # a true match.
33
36
  #
34
- # @param *array_matchers [Array] Array-like conditionals
35
- # @param **keyword_matchers [Hash] Keyword style conditionals
37
+ # @param *array_matchers [Array]
38
+ # Array-like conditionals
39
+ #
40
+ # @param **keyword_matchers [Hash]
41
+ # Keyword style conditionals
36
42
  #
37
43
  # @return [Proc[Any]]
38
44
  # Any -> Bool # Given a target, will return if it "matches"
@@ -44,8 +50,11 @@ module Qo
44
50
  # should pass to be considered a "match". It will short-circuit in the case of
45
51
  # a true match.
46
52
  #
47
- # @param *array_matchers [Array] Array-like conditionals
48
- # @param **keyword_matchers [Hash] Keyword style conditionals
53
+ # @param *array_matchers [Array]
54
+ # Array-like conditionals
55
+ #
56
+ # @param **keyword_matchers [Hash]
57
+ # Keyword style conditionals
49
58
  #
50
59
  # @return [Proc[Any]]
51
60
  # Any -> Bool # Given a target, will return if it "matches"
@@ -53,90 +62,74 @@ module Qo
53
62
  create_matcher('not', array_matchers, keyword_matchers)
54
63
  end
55
64
 
56
- # Creates a Guard Block matcher.
57
- #
58
- # A guard block matcher is used to guard a function from running unless
59
- # the left-hand matcher passes. Once called with a value, it will either
60
- # return `[false, false]` or `[true, Any]`.
65
+ # A pattern match will try and run all guard block style matchers in sequence
66
+ # until it finds one that "matches". Once found, it will pass the target
67
+ # into the associated matcher's block function.
61
68
  #
62
- # This wrapping is done to preserve intended false or nil responses,
63
- # and is unwrapped with match below.
69
+ # @example
70
+ # [1,2,3].map(&Qo.match { |m|
71
+ # m.when(:even?) { |v| v * 3 }
72
+ # m.else { |v| v - 1 }
73
+ # })
74
+ # => [0, 6, 2]
64
75
  #
65
- # @param *array_matchers [Array] varargs matchers
66
- # @param **keyword_matchers [Hash] kwargs matchers
67
- # @param &fn [Proc] Guarded function
76
+ # @param fn [Proc]
77
+ # Body of the matcher, as shown in examples
68
78
  #
69
- # @return [Proc[Any]]
70
- # Any -> Proc[Any]
71
- def matcher(*array_matchers, **keyword_matchers, &fn)
72
- Qo::Matchers::GuardBlockMatcher.new(*array_matchers, **keyword_matchers, &fn)
73
- end
79
+ # @return [Qo::PatternMatch]
80
+ # A function awaiting a value to match against
81
+ def match(&fn)
82
+ return proc { false } unless block_given?
74
83
 
75
- # Might be a tinge fond of shorthand
76
- alias_method :m, :matcher
84
+ Qo::Matchers::PatternMatch.new(&fn)
85
+ end
77
86
 
78
- # "Curried" function that waits for a target, or evaluates immediately if given
79
- # one.
87
+ # Similar to the common case statement of Ruby, except in that it behaves
88
+ # as if `Array#===` and `Hash#===` exist in the form of Qo matchers.
80
89
  #
81
- # A PatternMatch will try and run all GuardBlock matchers in sequence until
82
- # it finds one that "matches". Once found, it will pass the target into the
83
- # associated matcher's block function.
90
+ # @note
91
+ # I refer to the potential 2.6+ features currently being discussed here:
84
92
  #
85
- # @param fn [Proc]
86
- # If provided, the pattern match will become block-style, utilizing
87
- # PatternMatchBlock instead. If any args are provided, the first
88
- # will be treated as the target.
93
+ # * `Hash#===` - https://bugs.ruby-lang.org/issues/14869
94
+ # * `Array#===` - https://bugs.ruby-lang.org/issues/14916
89
95
  #
90
- # @param *args [Array[Any, *GuardBlockMatcher]]
91
- # Collection of matchers to run, potentially prefixed by a target object
96
+ # @see Qo#match
92
97
  #
93
- # @return [Qo::PatternMatchBlock]
94
- # If a value is not provided, a block style pattern match will be returned
95
- # that responds to proc coercion. It can be used for functions like `map`.
98
+ # @example
99
+ # Qo.case([1, 1]) { |m|
100
+ # m.when(Any, Any) { |a, b| a + b }
101
+ # m.else { |v| v }
102
+ # }
103
+ # => 2
96
104
  #
97
- # @return [Qo::PatternMatch]
98
- # If a value is not provided and no function is present, a PatternMatch
99
- # will be returned, awaiting a value to match against.
105
+ # @param value [Any]
106
+ # Value to match against
107
+ #
108
+ # @param &fn [Proc]
109
+ # Body of the matcher, as shown above
100
110
  #
101
111
  # @return [Any]
102
- # If a value is provided, matchers will attempt to call through on it,
103
- # returning the result of the function.
104
- def match(*args, &fn)
105
- if block_given?
106
- return args.empty? ?
107
- Qo::Matchers::PatternMatchBlock.new(&fn) :
108
- Qo::Matchers::PatternMatchBlock.new(&fn).call(args.first)
109
- end
110
-
111
- if args.first.is_a?(Qo::Matchers::GuardBlockMatcher)
112
- Qo::Matchers::PatternMatch.new(*args)
113
- else
114
- match_target, *qo_matchers = args
115
- Qo::Matchers::PatternMatch.new(*qo_matchers).call(match_target)
116
- end
112
+ # The result of calling a pattern match with a provided value
113
+ def case(value, &fn)
114
+ Qo::Matchers::PatternMatch.new(&fn).call(value)
117
115
  end
118
116
 
119
117
  # Abstraction for creating a matcher, allowing for common error handling scenarios.
120
118
  #
121
- # @param type [String] Type of matcher
122
- # @param *array_matchers [Array] Array-like conditionals
123
- # @param **keyword_matchers [Hash] Keyword style conditionals
119
+ # @param type [String]
120
+ # Type of matcher
121
+ #
122
+ # @param array_matchers [Array[Any]]
123
+ # Array-like conditionals
124
124
  #
125
- # @raises Qo::Exceptions::NoMatchersProvided
126
- # @raises Qo::Exceptions::MultipleMatchersProvided
125
+ # @param keyword_matchers [Hash[Any, Any]]
126
+ # Keyword style conditionals
127
127
  #
128
128
  # @return [Qo::Matcher]
129
129
  private def create_matcher(type, array_matchers, keyword_matchers)
130
- array_empty, hash_empty = array_matchers.empty?, keyword_matchers.empty?
131
-
132
- raise Qo::NoMatchersProvided if array_empty && hash_empty
133
- raise Qo::MultipleMatchersProvided if !(array_empty || hash_empty)
130
+ raise MultipleMatchersProvided if !array_matchers.empty? && !keyword_matchers.empty?
134
131
 
135
- if hash_empty
136
- Qo::Matchers::ArrayMatcher.new(type, *array_matchers)
137
- else
138
- Qo::Matchers::HashMatcher.new(type, **keyword_matchers)
139
- end
132
+ Qo::Matchers::BaseMatcher.new(type, array_matchers, keyword_matchers)
140
133
  end
141
134
  end
142
135
  end
@@ -1,3 +1,3 @@
1
1
  module Qo
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -1,4 +1,4 @@
1
- Running on Qo v0.4.0 at rev 96607b76bba3fea60d46e118a67fa5b4c76d5c72
1
+ Running on Qo v0.5.0 at rev 7b49c20d29630d9f56328d4663bb1b1ce1add2b0
2
2
  - Ruby ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
3
3
 
4
4
  Array * Array - Literal
@@ -8,15 +8,15 @@ Vanilla result: true
8
8
  Qo.and result: true
9
9
 
10
10
  Warming up --------------------------------------
11
- Vanilla 275.772k i/100ms
12
- Qo.and 77.013k i/100ms
11
+ Vanilla 245.363k i/100ms
12
+ Qo.and 67.365k i/100ms
13
13
  Calculating -------------------------------------
14
- Vanilla 8.911M2.0%) i/s - 44.675M in 5.015740s
15
- Qo.and 992.994k1.7%) i/s - 5.006M in 5.042624s
14
+ Vanilla 7.823M4.0%) i/s - 39.258M in 5.027372s
15
+ Qo.and 859.133k2.3%) i/s - 4.311M in 5.020923s
16
16
 
17
17
  Comparison:
18
- Vanilla: 8910560.1 i/s
19
- Qo.and: 992994.3 i/s - 8.97x slower
18
+ Vanilla: 7822633.5 i/s
19
+ Qo.and: 859133.0 i/s - 9.11x slower
20
20
 
21
21
 
22
22
  Array * Array - Index pattern match
@@ -26,15 +26,15 @@ Vanilla result: true
26
26
  Qo.and result: true
27
27
 
28
28
  Warming up --------------------------------------
29
- Vanilla 48.662k i/100ms
30
- Qo.and 22.868k i/100ms
29
+ Vanilla 43.805k i/100ms
30
+ Qo.and 21.434k i/100ms
31
31
  Calculating -------------------------------------
32
- Vanilla 569.942k1.6%) i/s - 2.871M in 5.038819s
33
- Qo.and 247.240k (± 2.8%) i/s - 1.258M in 5.091282s
32
+ Vanilla 511.690k2.0%) i/s - 2.584M in 5.053034s
33
+ Qo.and 241.516k (± 2.7%) i/s - 1.222M in 5.062575s
34
34
 
35
35
  Comparison:
36
- Vanilla: 569942.4 i/s
37
- Qo.and: 247239.9 i/s - 2.31x slower
36
+ Vanilla: 511689.7 i/s
37
+ Qo.and: 241515.5 i/s - 2.12x slower
38
38
 
39
39
 
40
40
  Array * Object - Predicate match
@@ -44,15 +44,15 @@ Vanilla result: false
44
44
  Qo.and result: false
45
45
 
46
46
  Warming up --------------------------------------
47
- Vanilla 142.790k i/100ms
48
- Qo.and 28.458k i/100ms
47
+ Vanilla 129.649k i/100ms
48
+ Qo.and 25.903k i/100ms
49
49
  Calculating -------------------------------------
50
- Vanilla 2.251M1.8%) i/s - 11.280M in 5.012252s
51
- Qo.and 315.757k1.8%) i/s - 1.594M in 5.048759s
50
+ Vanilla 2.049M2.7%) i/s - 10.242M in 5.002097s
51
+ Qo.and 287.416k3.8%) i/s - 1.451M in 5.054898s
52
52
 
53
53
  Comparison:
54
- Vanilla: 2251336.4 i/s
55
- Qo.and: 315757.2 i/s - 7.13x slower
54
+ Vanilla: 2049180.0 i/s
55
+ Qo.and: 287416.4 i/s - 7.13x slower
56
56
 
57
57
 
58
58
  Array * Array - Select index pattern match
@@ -62,15 +62,15 @@ Vanilla result: [["Robert", 22], ["Roberta", 22]]
62
62
  Qo.and result: [["Robert", 22], ["Roberta", 22]]
63
63
 
64
64
  Warming up --------------------------------------
65
- Vanilla 14.162k i/100ms
66
- Qo.and 7.559k i/100ms
65
+ Vanilla 12.729k i/100ms
66
+ Qo.and 6.911k i/100ms
67
67
  Calculating -------------------------------------
68
- Vanilla 150.748k (± 1.7%) i/s - 764.748k in 5.074423s
69
- Qo.and 78.082k1.6%) i/s - 393.068k in 5.035396s
68
+ Vanilla 135.430k (± 1.8%) i/s - 687.366k in 5.077139s
69
+ Qo.and 71.615k2.8%) i/s - 359.372k in 5.022246s
70
70
 
71
71
  Comparison:
72
- Vanilla: 150748.3 i/s
73
- Qo.and: 78082.2 i/s - 1.93x slower
72
+ Vanilla: 135430.5 i/s
73
+ Qo.and: 71615.2 i/s - 1.89x slower
74
74
 
75
75
 
76
76
  Hash * Hash - Hash intersection
@@ -80,15 +80,15 @@ Vanilla result: [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}]
80
80
  Qo.and result: [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}]
81
81
 
82
82
  Warming up --------------------------------------
83
- Vanilla 35.977k i/100ms
84
- Qo.and 5.112k i/100ms
83
+ Vanilla 33.461k i/100ms
84
+ Qo.and 5.366k i/100ms
85
85
  Calculating -------------------------------------
86
- Vanilla 410.483k (± 1.3%) i/s - 2.087M in 5.084305s
87
- Qo.and 51.860k1.6%) i/s - 260.712k in 5.028458s
86
+ Vanilla 366.234k3.1%) i/s - 1.840M in 5.030236s
87
+ Qo.and 54.974k4.4%) i/s - 279.032k in 5.087315s
88
88
 
89
89
  Comparison:
90
- Vanilla: 410483.2 i/s
91
- Qo.and: 51860.4 i/s - 7.92x slower
90
+ Vanilla: 366233.9 i/s
91
+ Qo.and: 54973.5 i/s - 6.66x slower
92
92
 
93
93
 
94
94
  Hash * Object - Property match
@@ -98,13 +98,13 @@ Vanilla result: [#<struct Person name="Robert", age=22>, #<struct Person name="R
98
98
  Qo.and result: [#<struct Person name="Robert", age=22>, #<struct Person name="Roberta", age=22>]
99
99
 
100
100
  Warming up --------------------------------------
101
- Vanilla 36.219k i/100ms
102
- Qo.and 5.415k i/100ms
101
+ Vanilla 33.166k i/100ms
102
+ Qo.and 5.659k i/100ms
103
103
  Calculating -------------------------------------
104
- Vanilla 412.314k1.3%) i/s - 2.064M in 5.007920s
105
- Qo.and 55.712k1.6%) i/s - 281.580k in 5.055626s
104
+ Vanilla 371.150k3.4%) i/s - 1.857M in 5.010508s
105
+ Qo.and 58.451k3.4%) i/s - 294.268k in 5.040637s
106
106
 
107
107
  Comparison:
108
- Vanilla: 412313.9 i/s
109
- Qo.and: 55711.7 i/s - 7.40x slower
108
+ Vanilla: 371149.8 i/s
109
+ Qo.and: 58450.6 i/s - 6.35x slower
110
110
 
data/qo.gemspec CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency "benchmark-ips"
29
29
  spec.add_development_dependency "yard"
30
30
  spec.add_development_dependency "redcarpet"
31
+ spec.add_development_dependency "dry-matcher"
31
32
 
32
33
  spec.add_runtime_dependency "any", '0.1.0'
33
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Weaver
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-08-06 00:00:00.000000000 Z
11
+ date: 2018-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: dry-matcher
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: any
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -199,7 +213,6 @@ files:
199
213
  - lib/qo/matchers/guard_block_matcher.rb
200
214
  - lib/qo/matchers/hash_matcher.rb
201
215
  - lib/qo/matchers/pattern_match.rb
202
- - lib/qo/matchers/pattern_match_block.rb
203
216
  - lib/qo/public_api.rb
204
217
  - lib/qo/version.rb
205
218
  - performance_report.txt
@@ -224,7 +237,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
224
237
  version: '0'
225
238
  requirements: []
226
239
  rubyforge_project:
227
- rubygems_version: 2.7.6
240
+ rubygems_version: 2.6.14.1
228
241
  signing_key:
229
242
  specification_version: 4
230
243
  summary: Qo is a querying library for Ruby pattern matching
@@ -1,115 +0,0 @@
1
- require 'qo/exceptions'
2
-
3
- module Qo
4
- module Matchers
5
- # Creates a PatternMatch in the style of a block.
6
- #
7
- # This varies from the regular PatternMatch in that all matchers are
8
- # provided in a more succinct block format:
9
- #
10
- # ```ruby
11
- # Qo.match(target) { |m|
12
- # m.when(/^F/, 42) { |(name, age)| "#{name} is #{age}" }
13
- # m.else { "We need a default, right?" }
14
- # }
15
- # ```
16
- #
17
- # The Public API obscures the fact that the matcher is only called when it
18
- # is explicitly given an argument to match against. If it is not, it will
19
- # just return a class waiting for a target, as such:
20
- #
21
- # ```ruby
22
- # def get_url(url)
23
- # Net::HTTP.get_response(URI(url)).yield_self(&Qo.match { |m|
24
- # m.when(Net::HTTPSuccess) { |response| response.body.size }
25
- # m.else { |response| raise response.message }
26
- # })
27
- # end
28
- #
29
- # get_url('https://github.com/baweaver/qo')
30
- # # => 142387
31
- # get_url('https://github.com/baweaver/qo/does_not_exist')
32
- # # => RuntimeError: Not Found
33
- # ```
34
- #
35
- # This is intended for flexibility between singular calls and calls as a
36
- # paramater to higher order functions like `map` and `yield_self`.
37
- #
38
- # This variant was inspired by ideas from Scala, Haskell, and various Ruby
39
- # libraries dealing with Async and self-yielding blocks. Especially notable
40
- # were websocket handlers and dry-ruby implementations.
41
- #
42
- # @author baweaver
43
- # @since 0.3.0
44
- #
45
- class PatternMatchBlock
46
- def initialize
47
- @matchers = []
48
-
49
- yield(self)
50
- end
51
-
52
- # Creates a match case. This is the exact same as any other `and` style
53
- # match reflected in the public API, except that it's a Guard Block
54
- # match being performed. That means if the left side matches, the right
55
- # side function is invoked and that value is returned.
56
- #
57
- # @param *array_matchers [Array[Any]]
58
- # Array style matchers
59
- #
60
- # @param **keyword_matchers [Hash[Any, Any]]
61
- # Hash style matchers
62
- #
63
- # @param &fn [Proc]
64
- # If matched, this function will be called
65
- #
66
- # @return [Array[GuardBlockMatcher]]
67
- # The return of this method should not be directly depended on, but will
68
- # provide all matchers currently present. This will likely be left for
69
- # ease of debugging later.
70
- def when(*array_matchers, **keyword_matchers, &fn)
71
- @matchers << Qo::Matchers::GuardBlockMatcher.new(*array_matchers, **keyword_matchers, &fn)
72
- end
73
-
74
- # Else is the last statement that will be evaluated if all other parts
75
- # fail. It should be noted that it won't magically appear, you have to
76
- # explicitly put an `else` case in for it to catch on no match unless
77
- # you want a `nil` return
78
- #
79
- # @param &fn [Proc]
80
- # Function to call when all other matches have failed
81
- #
82
- # @return [Proc]
83
- def else(&fn)
84
- raise Qo::Exceptions::MultipleElseClauses if @else
85
- @else = fn
86
- end
87
-
88
- # Proc version of a PatternMatchBlock
89
- #
90
- # @return [Proc]
91
- # Any -> Any | nil
92
- def to_proc
93
- Proc.new { |target| self.call(target) }
94
- end
95
-
96
- # Immediately invokes a PatternMatch
97
- #
98
- # @param target [Any]
99
- # Target to run against and pipe to the associated block if it
100
- # "matches" any of the GuardBlocks
101
- #
102
- # @return [Any | nil] Result of the piped block, or nil on a miss
103
- def call(target)
104
- @matchers.each { |guard_block_matcher|
105
- did_match, match_result = guard_block_matcher.call(target)
106
- return match_result if did_match
107
- }
108
-
109
- return @else.call(target) if @else
110
-
111
- nil
112
- end
113
- end
114
- end
115
- end