qo 0.1.10 → 0.2.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
2
  SHA1:
3
- metadata.gz: 1af9159c107b7761e1b30c7062b108e692403067
4
- data.tar.gz: 483b4e35c8c9bba6a7e84c7516ea3e8a1de6e5b2
3
+ metadata.gz: dcbbe9e009a9ccf04769b8ccb7eca4343ea972a5
4
+ data.tar.gz: 53dae10eca53508dacfb75ad940207f0f047d288
5
5
  SHA512:
6
- metadata.gz: 3db647525ff5685025108dacbe62b7945f135399d56c8a68eb9f39eb63dda3704614da143608a97d66251754c7d57c97d69d5e1a0d73f0c56e5d256cf813acdf
7
- data.tar.gz: 72582ba2f1d0f81d149e2f0b9e3d35d10dcb929418c2cde4f88d3773a734f93c7e78594dc59689058f8de0d2cf3edfbc9dc7bb448ec7cfe9bda237fe101a07d0
6
+ metadata.gz: 62bceca4a54804a1e18c86a26802e3903693a61d213fa27b959e0e6a362b54470aeb22f45101ebd8b73cb5dfa90a7c1214c7d78574d3a366a679734744005007
7
+ data.tar.gz: 40a0319fa994a9e82e1f55001deab434e95187819efc1ef5eaa6bdd9be015987c481b7b7097b00820974cdc43eeed815fc4b9f7d442f5388b1881549ad00eb62
data/README.md CHANGED
@@ -10,11 +10,19 @@ Short for Query Object, my play at Ruby pattern matching and fluent querying
10
10
 
11
11
  ## How does it work?
12
12
 
13
- Triple equals black magic, mostly.
13
+ Mostly by using Ruby language features like `to_proc` and `===`.
14
14
 
15
- Want to understand more of how that works? Check out this post: https://medium.com/rubyinside/triple-equals-black-magic-d934936a6379
15
+ There's an article explaining most of the base mechanics behind Qo:
16
16
 
17
- The original inspiration was from a chat I'd had with a few other Rubyists about pattern matching, which led to this experiment: https://gist.github.com/baweaver/611389c41c9005d025fb8e55448bf5f5
17
+ [For Want of Pattern Matching in Ruby - The Creation of Qo](https://medium.com/@baweaver/for-want-of-pattern-matching-in-ruby-the-creation-of-qo-c3b267109b25)
18
+
19
+ Most of it, though, utilizes Triple Equals. If you're not familiar with what all you can do with it in Ruby, I would encourage you to read this article as well:
20
+
21
+ [Triple Equals Black Magic](https://medium.com/rubyinside/triple-equals-black-magic-d934936a6379)
22
+
23
+ The original inspiration was from a chat I'd had with a few other Rubyists about pattern matching, which led to this experiment:
24
+
25
+ [Having fun with M and Q](https://gist.github.com/baweaver/611389c41c9005d025fb8e55448bf5f5)
18
26
 
19
27
  Fast forward a few months and I kind of wanted to make it real, so here it is. Introducing Qo!
20
28
 
@@ -36,7 +44,7 @@ people.select(&Qo[age: 18..30])
36
44
 
37
45
  # How about some "right-hand assignment" pattern matching
38
46
  name_longer_than_three = -> person { person.name.size > 3 }
39
- people_with_truncated_names = people.map(&Qo.match_fn(
47
+ people_with_truncated_names = people.map(&Qo.match(
40
48
  Qo.m(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) },
41
49
  Qo.m(:*) # Identity function, catch-all
42
50
  ))
@@ -63,7 +71,7 @@ Qo[/Rob/, 22]
63
71
  Qo.and(/Rob/, 22)
64
72
 
65
73
  # This is shorthand for
66
- Qo::Matcher.new('and', /Rob/, 22)
74
+ Qo::Matchers::BaseMatcher.new('and', /Rob/, 22)
67
75
 
68
76
  # An `or` matcher uses the same shorthand as `and` but uses `any?` behind the scenes instead:
69
77
  Qo.or(/Rob/, 22)
@@ -111,7 +119,7 @@ Qo[:*] === :literally_anything_here
111
119
  The first way a Qo matcher can be defined is by using `*varargs`:
112
120
 
113
121
  ```ruby
114
- # Qo::Matcher(type, *varargs, **kwargs)
122
+ Qo::Matchers::BaseMatcher(type, *varargs, **kwargs)
115
123
  ```
116
124
 
117
125
  This gives us the `and` matcher shorthand for array matchers.
@@ -277,7 +285,7 @@ Checks to see if the key is even present on the other object, false if not.
277
285
 
278
286
  ##### 3.1.2 - Match value and target are hashes
279
287
 
280
- If both the match value (`match_key: match_value`) and the match target are hashes, Qo will begin a recursive descent starting at the match key until it finds a matcher to try out:
288
+ If both the match value (`match_key: matcher`) and the match target are hashes, Qo will begin a recursive descent starting at the match key until it finds a matcher to try out:
281
289
 
282
290
  ```ruby
283
291
  Qo[a: {b: {c: 5..15}}] === {a: {b: {c: 10}}}
@@ -444,8 +452,6 @@ people_hashes.select(&Qo[age: :nil?])
444
452
 
445
453
  ### 4 - Right Hand Pattern Matching
446
454
 
447
- > ALPHA - This feature is alpha, currently testing. Considering whether or not to add `or` and `not` as `m_or` and `m_not`.
448
-
449
455
  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.
450
456
 
451
457
  ```ruby
@@ -470,12 +476,12 @@ In this case it's trying to do a few things:
470
476
 
471
477
  If no block function is provided, it assumes an identity function (`-> v { v }`) instead. If no match is found, `nil` will be returned.
472
478
 
473
- Now you _can_ also use a reversed version, `match_fn` (name pending better ideas), to run with map:
479
+ 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:
474
480
 
475
481
  ```ruby
476
482
  name_longer_than_three = -> person { person.name.size > 3 }
477
483
 
478
- people_objects.map(&Qo.match_fn(
484
+ people_objects.map(&Qo.match(
479
485
  Qo.m(name_longer_than_three) { |person|
480
486
  person.name = person.name[0..2]
481
487
  person
@@ -596,7 +602,7 @@ The nice thing about Unix style commands is that they use headers, which means C
596
602
  ```ruby
597
603
  rows = CSV.new(`df -h`, col_sep: " ", headers: true).read.map(&:to_h)
598
604
 
599
- rows.map(&Qo.match_fn(
605
+ rows.map(&Qo.match(
600
606
  Qo.m(Avail: /Gi$/) { |row|
601
607
  "#{row['Filesystem']} mounted on #{row['Mounted']} [#{row['Avail']} / #{row['Size']}]"
602
608
  }
data/Rakefile CHANGED
@@ -105,3 +105,75 @@ task :perf do
105
105
  }
106
106
  )
107
107
  end
108
+
109
+ # Below this mark are mostly my experiments to see what features perform a bit better
110
+ # than others, and are mostly left to check different versions of Ruby against eachother.
111
+ #
112
+ # Feel free to use them in development, but the general consensus of them is that
113
+ # `send` type methods are barely slower. One _could_ write an IIFE to get around
114
+ # that and maintain the flexibility but it's a net loss of clarity.
115
+ #
116
+ # Proc wise, they're all within margin of error. We just need to be really careful
117
+ # of the 2.4+ bug of lambdas not destructuring automatically, which will wreak
118
+ # havoc on hash matchers.
119
+
120
+ task :perf_predicates do
121
+ array = (1..1000).to_a
122
+
123
+ run_benchmark('Predicates any?',
124
+ 'block_any?': -> { array.any? { |v| v.even? } },
125
+ 'proc_any?': -> { array.any?(&:even?) },
126
+ 'send_proc_any?': -> { array.public_send(:any?, &:even?) }
127
+ )
128
+
129
+ run_benchmark('Predicates all?',
130
+ 'block_all?': -> { array.all? { |v| v.even? } },
131
+ 'proc_all?': -> { array.all?(&:even?) },
132
+ 'send_proc_all?': -> { array.public_send(:all?, &:even?) }
133
+ )
134
+
135
+ run_benchmark('Predicates none?',
136
+ 'block_none?': -> { array.none? { |v| v.even? } },
137
+ 'proc_none?': -> { array.none?(&:even?) },
138
+ 'send_proc_none?': -> { array.public_send(:none?, &:even?) },
139
+ )
140
+
141
+ even_stabby_lambda = -> n { n % 2 == 0 }
142
+ even_lambda = lambda { |n| n % 2 == 0 }
143
+ even_proc_new = Proc.new { |n| n % 2 == 0 }
144
+ even_proc_short = proc { |n| n % 2 == 0 }
145
+ even_to_proc = :even?.to_proc
146
+
147
+ run_benchmark('Types of Functions in Ruby',
148
+ even_stabby_lambda: -> { array.all?(&even_stabby_lambda) },
149
+ even_lambda: -> { array.all?(&even_lambda) },
150
+ even_proc_new: -> { array.all?(&even_proc_new) },
151
+ even_proc_short: -> { array.all?(&even_proc_short) },
152
+ even_to_proc: -> { array.all?(&even_to_proc) },
153
+ )
154
+ end
155
+
156
+ task :perf_random do
157
+ # run_benchmark('Empty on blank array',
158
+ # 'empty?': -> { [].empty? },
159
+ # 'size == 0': -> { [].size == 0 },
160
+ # 'size.zero?': -> { [].size.zero? },
161
+ # )
162
+
163
+ array = (1..1000).to_a
164
+ # run_benchmark('Empty on several elements array',
165
+ # 'empty?': -> { array.empty? },
166
+ # 'size == 0': -> { array.size == 0 },
167
+ # 'size.zero?': -> { array.size.zero? },
168
+ # )
169
+
170
+ hash = array.map { |v| [v, v] }.to_h
171
+
172
+ run_benchmark('Empty on blank hash vs array',
173
+ 'hash empty?': -> { {}.empty? },
174
+ 'array empty?': -> { [].empty? },
175
+
176
+ 'full hash empty?': -> { hash.empty? },
177
+ 'full array empty?': -> { array.empty? },
178
+ )
179
+ end
data/lib/qo.rb CHANGED
@@ -1,92 +1,25 @@
1
1
  require "qo/version"
2
- require 'qo/matcher'
3
- require 'qo/guard_block_matcher'
4
2
 
5
- module Qo
6
- WILDCARD_MATCH = :*
7
-
8
- class << self
9
-
10
- # Creates a Guard Block matcher.
11
- #
12
- # A guard block matcher is used to guard a function from running unless
13
- # the left-hand matcher passes. Once called with a value, it will either
14
- # return `[false, false]` or `[true, Any]`.
15
- #
16
- # This wrapping is done to preserve intended false or nil responses,
17
- # and is unwrapped with match below.
18
- #
19
- # @param *array_matchers [Array] varargs matchers
20
- # @param **keyword_matchers [Hash] kwargs matchers
21
- # @param &fn [Proc] Guarded function
22
- #
23
- # @return [Proc[Any]]
24
- # Any -> Proc[Any]
25
- def matcher(*array_matchers, **keyword_matchers, &fn)
26
- Qo::GuardBlockMatcher.new(*array_matchers, **keyword_matchers, &fn)
27
- end
28
-
29
- alias_method :m, :matcher
30
-
31
-
32
- # Takes a set of Guard Block matchers, runs each in sequence, then
33
- # unfolds the response from the first passing block.
34
- #
35
- # @param target [Any] Target object to run against
36
- # @param *qo_matchers [Array[GuardBlockMatcher]] Collection of matchers to run
37
- #
38
- # @return [type] [description]
39
- def match(target, *qo_matchers)
40
- all_are_guards = qo_matchers.all? { |q| q.is_a?(Qo::GuardBlockMatcher) }
41
- raise 'Must patch Qo GuardBlockMatchers!' unless all_are_guards
42
-
43
- qo_matchers.reduce(nil) { |_, matcher|
44
- did_match, match_result = matcher.call(target)
45
- break match_result if did_match
46
- }
47
- end
3
+ # Matchers
4
+ require 'qo/matchers/base_matcher'
5
+ require 'qo/matchers/array_matcher'
6
+ require 'qo/matchers/hash_matcher'
7
+ require 'qo/matchers/guard_block_matcher'
48
8
 
49
- # Wraps match to allow it to be used in a points-free style like regular matchers.
50
- #
51
- # @param *qo_matchers [Array[GuardBlockMatcher]] Collection of matchers to run
52
- #
53
- # @return [Proc[Any]]
54
- # Any -> Any
55
- def match_fn(*qo_matchers)
56
- -> target { match(target, *qo_matchers) }
57
- end
9
+ # Meta Matchers
10
+ require 'qo/matchers/pattern_match'
58
11
 
59
- def and(*array_matchers, **keyword_matchers)
60
- Qo::Matcher.new('and', *array_matchers, **keyword_matchers)
61
- end
12
+ # Helpers
13
+ require 'qo/helpers'
62
14
 
63
- alias_method :[], :and
15
+ # Public API
16
+ require 'qo/exceptions'
17
+ require 'qo/public_api'
64
18
 
65
- def or(*array_matchers, **keyword_matchers)
66
- Qo::Matcher.new('or', *array_matchers, **keyword_matchers)
67
- end
68
-
69
- def not(*array_matchers, **keyword_matchers)
70
- Qo::Matcher.new('not', *array_matchers, **keyword_matchers)
71
- end
72
-
73
- # Utility functions. Consider placing these elsewhere.
74
-
75
- def dig(path_map, expected_value)
76
- -> hash {
77
- segments = path_map.split('.')
78
-
79
- expected_value === hash.dig(*segments) ||
80
- expected_value === hash.dig(*segments.map(&:to_sym))
81
- }
82
- end
83
-
84
- def count_by(targets, &fn)
85
- fn ||= -> v { v }
19
+ module Qo
20
+ WILDCARD_MATCH = :*
86
21
 
87
- targets.each_with_object(Hash.new(0)) { |target, counts|
88
- counts[fn[target]] += 1
89
- }
90
- end
91
- end
22
+ extend Qo::Exceptions
23
+ extend Qo::Helpers
24
+ extend Qo::PublicApi
92
25
  end
@@ -0,0 +1,39 @@
1
+ module Qo
2
+ # Defines common exception classes for use throughout the library
3
+ #
4
+ # @author [baweaver]
5
+ #
6
+ module Exceptions
7
+ # If no matchers in either Array or Hash style are provided.
8
+ #
9
+ # @author [lemur]
10
+ #
11
+ class NoMatchersProvided < ArgumentError
12
+ def to_s
13
+ "No Qo matchers were provided!"
14
+ end
15
+ end
16
+
17
+ # If both Array and Hash style matchers are provided.
18
+ #
19
+ # @author [lemur]
20
+ #
21
+ class MultipleMatchersProvided < ArgumentError
22
+ def to_s
23
+ "Cannot provide both array and keyword matchers!"
24
+ end
25
+ end
26
+
27
+ # In the case of a Pattern Match, we need to ensure all arguments are
28
+ # GuardBlockMatchers.
29
+ #
30
+ # @author [lemur]
31
+ #
32
+ class NotAllGuardMatchersProvided < ArgumentError
33
+ def to_s
34
+ "All provided matchers must be of type Qo::Matchers::GuardBlockMatcher " +
35
+ "defined with `Qo.matcher` or `Qo.m` instead of regular matchers."
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ module Qo
2
+ module Helpers
3
+ # A curried variant of Hash#dig meant to be passed as a matcher util.
4
+ #
5
+ # @note This method will attempt to coerce path segments to Symbols
6
+ # if unsuccessful in first dig.
7
+ #
8
+ # @param path_map [String] Dot-delimited path
9
+ # @param expected_value [Any] Matcher
10
+ #
11
+ # @return [Proc]
12
+ # Hash -> Bool # Status of digging against the hash
13
+ def dig(path_map, expected_value)
14
+ Proc.new { |hash|
15
+ segments = path_map.split('.')
16
+
17
+ expected_value === hash.dig(*segments) ||
18
+ expected_value === hash.dig(*segments.map(&:to_sym))
19
+ }
20
+ end
21
+
22
+ # Counts by a function. This is entirely because I hackney this everywhere in
23
+ # pry anyways, so I want a function to do it for me already.
24
+ #
25
+ # @param targets [Array[Any]] Targets to count
26
+ # @param &fn [Proc] Function to define count key
27
+ #
28
+ # @return [Hash[Any, Integer]] Counts
29
+ def count_by(targets, &fn)
30
+ fn ||= -> v { v }
31
+
32
+ targets.each_with_object(Hash.new(0)) { |target, counts|
33
+ counts[fn[target]] += 1
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ module Qo
2
+ module Matchers
3
+ # An Array Matcher is a matcher that uses only varargs to define a sequence
4
+ # of matches to perform against either an object or another Array.
5
+ #
6
+ # In the case of an Array matching against an Array it will compare via index.
7
+ #
8
+ # In the case of an Array matching against an Object, it will match each provided
9
+ # matcher against the object.
10
+ #
11
+ # All variants present in the BaseMatcher are present here, including 'and',
12
+ # 'not', and 'or'.
13
+ #
14
+ # @author [baweaver]
15
+ #
16
+ class ArrayMatcher < BaseMatcher
17
+ # Used to match against a matcher made from an Array, like:
18
+ #
19
+ # Qo['Foo', 'Bar']
20
+ #
21
+ # @param matchers [Array[respond_to?(===)]] indexed tuple to match the target object against
22
+ #
23
+ # @return [Proc[Any]]
24
+ # Array -> Bool # Tuple match against targets index
25
+ # Object -> Bool # Boolean public send
26
+ def to_proc
27
+ Proc.new { |target| self.call(target) }
28
+ end
29
+
30
+ # Invocation for the match sequence. Will determine the target and applicable
31
+ # matchers to run against it.
32
+ #
33
+ # @param target [Any]
34
+ #
35
+ # @return [Boolean] Match status
36
+ def call(target)
37
+ return true if @array_matchers == target
38
+
39
+ if target.is_a?(::Array)
40
+ match_with(@array_matchers.each_with_index) { |matcher, i|
41
+ match_value?(target[i], matcher)
42
+ }
43
+ else
44
+ match_with(@array_matchers) { |matcher|
45
+ match_value?(target, matcher)
46
+ }
47
+ end
48
+ end
49
+
50
+ # Defines what it means for a value to match a matcher
51
+ #
52
+ # @param target [Any] Target to match against
53
+ # @param matcher [Any] Any matcher to run against, most frequently responds to ===
54
+ #
55
+ # @return [Boolean] Match status
56
+ private def match_value?(target, matcher)
57
+ wildcard_match?(matcher) ||
58
+ case_match?(target, matcher) ||
59
+ method_matches?(target, matcher)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,104 @@
1
+ module Qo
2
+ module Matchers
3
+ # Base instance of matcher which is meant to take in either Array style or
4
+ # Keyword style arguments to run a match against various datatypes.
5
+ #
6
+ # Will delegate responsibilities to either Array or Hash style matchers if
7
+ # invoked directly.
8
+ #
9
+ # @author [baweaver]
10
+ #
11
+ class BaseMatcher
12
+ def initialize(type, *array_matchers, **keyword_matchers)
13
+ @array_matchers = array_matchers
14
+ @keyword_matchers = keyword_matchers
15
+ @type = type
16
+ end
17
+
18
+ # Converts a Matcher to a proc for use in querying, such as:
19
+ #
20
+ # data.select(&Qo[...])
21
+ #
22
+ # @return [Proc]
23
+ def to_proc
24
+ @array_matchers.empty? ?
25
+ Qo::Matchers::HashMatcher.new(@type, **@keyword_matchers).to_proc :
26
+ Qo::Matchers::ArrayMatcher.new(@type, *@array_matchers).to_proc
27
+ end
28
+
29
+ # You can directly call a matcher as well, much like a Proc,
30
+ # using one of call, ===, or []
31
+ #
32
+ # @param target [Any] Object to match against
33
+ #
34
+ # @return [type] [description]
35
+ def call(target)
36
+ self.to_proc.call(target)
37
+ end
38
+
39
+ alias_method :===, :call
40
+ alias_method :[], :call
41
+
42
+ # Wrapper around public send to encapsulate the matching method (any, all, none)
43
+ #
44
+ # @param collection [Enumerable] Any collection that can be enumerated over
45
+ # @param fn [Proc] Function to match with
46
+ #
47
+ # @return [Enumerable] Resulting collection
48
+ private def match_with(collection, &fn)
49
+ return collection.any?(&fn) if @type == 'or'
50
+ return collection.none?(&fn) if @type == 'not'
51
+
52
+ collection.all?(&fn)
53
+ end
54
+
55
+ # Wraps wildcard in case we want to do anything fun with it later
56
+ #
57
+ # @param value [Any] Value to test against the wild card
58
+ #
59
+ # @note The rescue is because some classes override `==` to do silly things,
60
+ # like IPAddr, and I kinda want to use that.
61
+ #
62
+ # @return [Boolean]
63
+ private def wildcard_match?(value)
64
+ value == WILDCARD_MATCH rescue false
65
+ end
66
+
67
+ # Wraps a case equality statement to make it a bit easier to read. The
68
+ # typical left bias of `===` can be confusing reading down a page, so
69
+ # more of a clarity thing than anything. Also makes for nicer stack traces.
70
+ #
71
+ # @param target [Any] Target to match against
72
+ # @param matcher [respond_to?(:===)]
73
+ # Anything that responds to ===, preferably in a unique and entertaining way.
74
+ #
75
+ # @return [Boolean]
76
+ private def case_match?(target, matcher)
77
+ matcher === target
78
+ end
79
+
80
+ # Guarded version of `public_send` meant to stamp out more
81
+ # obscure errors when running against non-matching types.
82
+ #
83
+ # @param target [Any] Object to send to
84
+ # @param matcher [respond_to?(:to_sym)] Anything that can be coerced into a method name
85
+ #
86
+ # @return [Any] Response of sending to the method, or false if failed
87
+ private def method_send(target, matcher)
88
+ matcher.respond_to?(:to_sym) &&
89
+ target.respond_to?(matcher.to_sym) &&
90
+ target.public_send(matcher)
91
+ end
92
+
93
+ # Predicate variant of `method_send` with the same guard concerns
94
+ #
95
+ # @param target [Any] Object to send to
96
+ # @param matcher [respond_to?(:to_sym)] Anything that can be coerced into a method name
97
+ #
98
+ # @return [Boolean] Success status of predicate
99
+ private def method_matches?(target, matcher)
100
+ !!method_send(target, matcher)
101
+ end
102
+ end
103
+ end
104
+ end