qo 0.4.0 → 0.5.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
- 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