check_please 0.4.0 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,7 +5,7 @@ module CheckPlease
5
5
  autoload :Parser, "check_please/cli/runner"
6
6
 
7
7
  def self.run(exe_file_name)
8
- Runner.new(__FILE__).run(*ARGV.dup)
8
+ Runner.new(exe_file_name).run(*ARGV.dup)
9
9
  end
10
10
  end
11
11
 
@@ -5,7 +5,7 @@ module CLI
5
5
 
6
6
  class Parser
7
7
  def initialize(exe_file_name)
8
- @exe_file_name = exe_file_name
8
+ @exe_file_name = File.basename(exe_file_name)
9
9
  end
10
10
 
11
11
  # Unfortunately, OptionParser *really* wants to use closures. I haven't
@@ -1,34 +1,37 @@
1
1
  module CheckPlease
2
+ using Refinements
2
3
 
3
- module Comparison
4
- extend self
4
+ class Comparison
5
+ def self.perform(reference, candidate, flags = {})
6
+ new.perform(reference, candidate, flags)
7
+ end
5
8
 
6
9
  def perform(reference, candidate, flags = {})
7
- root = CheckPlease::Path.new
8
- diffs = Diffs.new(flags: flags)
10
+ @flags = Flags(flags) # whoa, it's almost like Java in here
11
+ @diffs = Diffs.new(flags: @flags)
12
+
9
13
  catch(:max_diffs_reached) do
10
- compare reference, candidate, root, diffs
14
+ compare reference, candidate, CheckPlease::Path.root
11
15
  end
12
16
  diffs
13
17
  end
14
18
 
15
19
  private
20
+ attr_reader :diffs, :flags
16
21
 
17
- def compare(ref, can, path, diffs)
18
- if (d = diffs.flags.max_depth)
19
- return if path.depth > d + 1
20
- end
22
+ def compare(ref, can, path)
23
+ return if path.excluded?(flags)
21
24
 
22
- case types(ref, can)
23
- when [ :array, :array ] ; compare_arrays ref, can, path, diffs
24
- when [ :hash, :hash ] ; compare_hashes ref, can, path, diffs
25
- when [ :other, :other ] ; compare_others ref, can, path, diffs
25
+ case types_for_compare(ref, can)
26
+ when [ :array, :array ] ; compare_arrays ref, can, path
27
+ when [ :hash, :hash ] ; compare_hashes ref, can, path
28
+ when [ :other, :other ] ; compare_others ref, can, path
26
29
  else
27
- diffs.record ref, can, path, :type_mismatch
30
+ record_diff ref, can, path, :type_mismatch
28
31
  end
29
32
  end
30
33
 
31
- def types(*list)
34
+ def types_for_compare(*list)
32
35
  list.map { |e|
33
36
  case e
34
37
  when Array ; :array
@@ -38,55 +41,188 @@ module CheckPlease
38
41
  }
39
42
  end
40
43
 
41
- def compare_arrays(ref_array, can_array, path, diffs)
42
- max_len = [ ref_array, can_array ].map(&:length).max
43
- (0...max_len).each do |i|
44
- n = i + 1 # count in human pls
45
- new_path = path + n
44
+ def compare_arrays(ref_array, can_array, path)
45
+ case
46
+ when ( key = path.key_to_match_by(flags) )
47
+ compare_arrays_by_key ref_array, can_array, path, key
48
+ when path.match_by_value?(flags)
49
+ compare_arrays_by_value ref_array, can_array, path
50
+ else
51
+ compare_arrays_by_index ref_array, can_array, path
52
+ end
53
+ end
54
+
55
+ def compare_arrays_by_key(ref_array, can_array, path, key_name)
56
+ refs_by_key = index_array!(ref_array, path, key_name, "reference")
57
+ cans_by_key = index_array!(can_array, path, key_name, "candidate")
58
+
59
+ key_values = (refs_by_key.keys | cans_by_key.keys)
46
60
 
47
- ref = ref_array[i]
48
- can = can_array[i]
61
+ key_values.compact! # NOTE: will break if nil is ever used as a key (but WHO WOULD DO THAT?!)
62
+ key_values.sort!
49
63
 
50
- case
51
- when ref_array.length < n ; diffs.record ref, can, new_path, :extra
52
- when can_array.length < n ; diffs.record ref, can, new_path, :missing
53
- else
54
- compare ref, can, new_path, diffs
64
+ key_values.each do |key_value|
65
+ new_path = path + "#{key_name}=#{key_value}"
66
+ ref = refs_by_key[key_value]
67
+ can = cans_by_key[key_value]
68
+ case
69
+ when ref.nil? ; record_diff ref, can, new_path, :extra
70
+ when can.nil? ; record_diff ref, can, new_path, :missing
71
+ else ; compare ref, can, new_path
72
+ end
55
73
  end
56
74
  end
57
- end
58
75
 
59
- def compare_hashes(ref_hash, can_hash, path, diffs)
60
- record_missing_keys ref_hash, can_hash, path, diffs
61
- compare_common_keys ref_hash, can_hash, path, diffs
62
- record_extra_keys ref_hash, can_hash, path, diffs
76
+ def index_array!(array_of_hashes, path, key_name, ref_or_can)
77
+ elements_by_key = {}
78
+
79
+ array_of_hashes.each.with_index do |h, i|
80
+ # make sure we have a hash
81
+ unless h.is_a?(Hash)
82
+ raise CheckPlease::TypeMismatchError, \
83
+ "The element at position #{i} in the #{ref_or_can} array is not a hash."
84
+ end
85
+
86
+ if flags.indifferent_keys
87
+ h = stringify_symbol_keys(h)
88
+ end
89
+
90
+ # try to get the value of the attribute identified by key_name
91
+ key_value = h.fetch(key_name) {
92
+ raise CheckPlease::NoSuchKeyError, \
93
+ <<~EOF
94
+ The #{ref_or_can} hash at position #{i} has no #{key_name.inspect} key.
95
+ Keys it does have: #{h.keys.inspect}
96
+ EOF
97
+ }
98
+
99
+ # complain about dupes
100
+ if elements_by_key.has_key?(key_value)
101
+ key_val_expr = "#{key_name}=#{key_value}"
102
+ raise CheckPlease::DuplicateKeyError, \
103
+ "Duplicate #{ref_or_can} element found at path '#{path + key_val_expr}'."
104
+ end
105
+
106
+ # ok, now we can proceed
107
+ elements_by_key[key_value] = h
108
+ end
109
+
110
+ elements_by_key
111
+ end
112
+
113
+ # FIXME: this can generate duplicate paths.
114
+ # Time to introduce lft_path, rgt_path ?
115
+ def compare_arrays_by_value(ref_array, can_array, path)
116
+ assert_can_match_by_value! ref_array
117
+ assert_can_match_by_value! can_array
118
+
119
+ matches = can_array.map { false }
120
+
121
+ # Look for missing values
122
+ ref_array.each.with_index do |ref, i|
123
+ new_path = path + (i+1) # count in human pls
124
+
125
+ # Weird, but necessary to handle duplicates properly
126
+ j = can_array.index.with_index { |can, j|
127
+ matches[j] == false && can == ref
128
+ }
129
+
130
+ if j
131
+ matches[j] = true
132
+ else
133
+ record_diff ref, nil, new_path, :missing
134
+ end
135
+ end
136
+
137
+ # Look for extra values
138
+ can_array.zip(matches).each.with_index do |(can, match), i|
139
+ next if match
140
+ new_path = path + (i+1) # count in human pls
141
+ record_diff nil, can, new_path, :extra
142
+ end
143
+ end
144
+
145
+ def assert_can_match_by_value!(array)
146
+ if array.any? { |e| Array === e || Hash === e }
147
+ raise CheckPlease::BehaviorUndefined,
148
+ "match_by_value behavior is not defined for collections!"
149
+ end
150
+ end
151
+
152
+ def compare_arrays_by_index(ref_array, can_array, path)
153
+ max_len = [ ref_array, can_array ].map(&:length).max
154
+ (0...max_len).each do |i|
155
+ n = i + 1 # count in human pls
156
+ new_path = path + n
157
+
158
+ ref = ref_array[i]
159
+ can = can_array[i]
160
+
161
+ case
162
+ when ref_array.length < n ; record_diff ref, can, new_path, :extra
163
+ when can_array.length < n ; record_diff ref, can, new_path, :missing
164
+ else
165
+ compare ref, can, new_path
166
+ end
167
+ end
168
+ end
169
+
170
+ def compare_hashes(ref_hash, can_hash, path)
171
+ if flags.indifferent_keys
172
+ ref_hash = stringify_symbol_keys(ref_hash)
173
+ can_hash = stringify_symbol_keys(can_hash)
174
+ end
175
+ record_missing_keys ref_hash, can_hash, path
176
+ compare_common_keys ref_hash, can_hash, path
177
+ record_extra_keys ref_hash, can_hash, path
63
178
  end
64
179
 
65
- def record_missing_keys(ref_hash, can_hash, path, diffs)
180
+ def stringify_symbol_keys(h)
181
+ Hash[
182
+ h.map { |k,v|
183
+ [ stringify_symbol(k), v ]
184
+ }
185
+ ]
186
+ end
187
+
188
+ def stringify_symbol(x)
189
+ Symbol === x ? x.to_s : x
190
+ end
191
+
192
+ def record_missing_keys(ref_hash, can_hash, path)
66
193
  keys = ref_hash.keys - can_hash.keys
67
194
  keys.each do |k|
68
- diffs.record ref_hash[k], nil, path + k, :missing
195
+ record_diff ref_hash[k], nil, path + k, :missing
69
196
  end
70
197
  end
71
198
 
72
- def compare_common_keys(ref_hash, can_hash, path, diffs)
199
+ def compare_common_keys(ref_hash, can_hash, path)
73
200
  keys = ref_hash.keys & can_hash.keys
74
201
  keys.each do |k|
75
- compare ref_hash[k], can_hash[k], path + k, diffs
202
+ compare ref_hash[k], can_hash[k], path + k
76
203
  end
77
204
  end
78
205
 
79
- def record_extra_keys(ref_hash, can_hash, path, diffs)
206
+ def record_extra_keys(ref_hash, can_hash, path)
80
207
  keys = can_hash.keys - ref_hash.keys
81
208
  keys.each do |k|
82
- diffs.record nil, can_hash[k], path + k, :extra
209
+ record_diff nil, can_hash[k], path + k, :extra
83
210
  end
84
211
  end
85
212
 
86
- def compare_others(ref, can, path, diffs)
213
+ def compare_others(ref, can, path)
214
+ if flags.indifferent_values
215
+ ref = stringify_symbol(ref)
216
+ can = stringify_symbol(can)
217
+ end
87
218
  return if ref == can
88
- diffs.record ref, can, path, :mismatch
219
+ record_diff ref, can, path, :mismatch
89
220
  end
221
+
222
+ def record_diff(ref, can, path, type)
223
+ diff = Diff.new(type, path, ref, can)
224
+ diffs << diff
225
+ end
90
226
  end
91
227
 
92
228
  end
@@ -43,15 +43,14 @@ module CheckPlease
43
43
  @hash[diff.path] = diff
44
44
  end
45
45
 
46
- def record(ref, can, path, type)
47
- return if path.excluded?(flags)
48
- self << Diff.new(type, path, ref, can)
49
- end
50
-
51
46
  def data
52
47
  @list.map(&:attributes)
53
48
  end
54
49
 
50
+ def to_s(flags = {})
51
+ CheckPlease::Printers.render(self, flags)
52
+ end
53
+
55
54
  extend Forwardable
56
55
  def_delegators :@list, *%i[
57
56
  each
@@ -6,8 +6,32 @@ module CheckPlease
6
6
  # instead....
7
7
  end
8
8
 
9
+ class BehaviorUndefined < ::StandardError
10
+ include CheckPlease::Error
11
+ end
12
+
13
+ class DuplicateKeyError < ::IndexError
14
+ include CheckPlease::Error
15
+ end
16
+
9
17
  class InvalidFlag < ArgumentError
10
18
  include CheckPlease::Error
11
19
  end
12
20
 
21
+ class InvalidPath < ArgumentError
22
+ include CheckPlease::Error
23
+ end
24
+
25
+ class InvalidPathSegment < ArgumentError
26
+ include CheckPlease::Error
27
+ end
28
+
29
+ class NoSuchKeyError < ::KeyError
30
+ include CheckPlease::Error
31
+ end
32
+
33
+ class TypeMismatchError < ::TypeError
34
+ include CheckPlease::Error
35
+ end
36
+
13
37
  end
@@ -29,12 +29,22 @@ module CheckPlease
29
29
  @coercer = block
30
30
  end
31
31
 
32
+ def description=(value)
33
+ if value.is_a?(String) && value =~ /\n/m
34
+ lines = value.lines
35
+ else
36
+ lines = Array(value).map(&:to_s)
37
+ end
38
+
39
+ @description = lines.map(&:rstrip)
40
+ end
41
+
32
42
  def mutually_exclusive_to(flag_name)
33
43
  @validators << ->(flags, _) { flags.send(flag_name).empty? }
34
44
  end
35
45
 
36
- def reentrant
37
- @reentrant = true
46
+ def repeatable
47
+ @repeatable = true
38
48
  self.default_proc = ->{ Array.new }
39
49
  end
40
50
 
@@ -47,7 +57,7 @@ module CheckPlease
47
57
  def __set__(value, on:, flags:)
48
58
  val = _coerce(value)
49
59
  _validate(flags, val)
50
- if @reentrant
60
+ if @repeatable
51
61
  on[name] ||= []
52
62
  on[name].concat(Array(val))
53
63
  else
@@ -1,38 +1,173 @@
1
1
  module CheckPlease
2
2
 
3
+ # TODO: this class is getting a bit large; maybe split out some of the stuff that uses flags?
3
4
  class Path
5
+ include CheckPlease::Reification
6
+ can_reify String, Symbol, Numeric, nil
7
+
4
8
  SEPARATOR = "/"
5
9
 
6
- def initialize(segments = [])
10
+ def self.root
11
+ new('/')
12
+ end
13
+
14
+
15
+
16
+ attr_reader :to_s, :segments
17
+ def initialize(name_or_segments = [])
18
+ case name_or_segments
19
+ when String, Symbol, Numeric, nil
20
+ names = name_or_segments.to_s.split(SEPARATOR)
21
+ names.shift until names.empty? || names.first =~ /\S/
22
+ segments = PathSegment.reify(names)
23
+ when Array
24
+ segments = PathSegment.reify(name_or_segments)
25
+ else
26
+ raise InvalidPath, "not sure what to do with #{name_or_segments.inspect}"
27
+ end
28
+
29
+ if segments.any?(&:empty?)
30
+ raise InvalidPath, "#{self.class.name} cannot contain empty segments"
31
+ end
32
+
7
33
  @segments = Array(segments)
34
+
35
+ @to_s = SEPARATOR + @segments.join(SEPARATOR)
36
+ freeze
37
+ rescue InvalidPathSegment => e
38
+ raise InvalidPath, e.message
8
39
  end
9
40
 
10
41
  def +(new_basename)
11
- self.class.new( Array(@segments) + Array(new_basename.to_s) )
42
+ new_segments = self.segments.dup
43
+ new_segments << new_basename # don't reify here; it'll get done on Path#initialize
44
+ self.class.new(new_segments)
45
+ end
46
+
47
+ def ==(other)
48
+ self.to_s == other.to_s
49
+ end
50
+
51
+ def ancestors
52
+ list = []
53
+ p = self
54
+ loop do
55
+ break if p.root?
56
+ p = p.parent
57
+ list.unshift p
58
+ end
59
+ list.reverse
60
+ end
61
+
62
+ def basename
63
+ segments.last.to_s
12
64
  end
13
65
 
14
66
  def depth
15
- 1 + @segments.length
67
+ 1 + segments.length
16
68
  end
17
69
 
18
70
  def excluded?(flags)
19
- s = to_s ; matches = ->(path_expr) { s.include?(path_expr) }
20
- if flags.select_paths.length > 0
21
- return flags.select_paths.none?(&matches)
22
- end
23
- if flags.reject_paths.length > 0
24
- return flags.reject_paths.any?(&matches)
71
+ return false if root? # that would just be silly
72
+
73
+ return true if too_deep?(flags)
74
+ return true if explicitly_excluded?(flags)
75
+ return true if implicitly_excluded?(flags)
76
+
77
+ false
78
+ end
79
+
80
+ def inspect
81
+ "<#{self.class.name} '#{to_s}'>"
82
+ end
83
+
84
+ def key_to_match_by(flags)
85
+ key_exprs = unpack_key_exprs(flags.match_by_key)
86
+ # NOTE: match on parent because if self.to_s == '/foo', MBK '/foo/:id' should return 'id'
87
+ matches = key_exprs.select { |e| e.parent.match?(self) }
88
+
89
+ case matches.length
90
+ when 0 ; nil
91
+ when 1 ; matches.first.segments.last.key
92
+ else ; raise "More than one match_by_key expression for path '#{self}': #{matches.map(&:to_s).inspect}"
25
93
  end
94
+ end
95
+
96
+ def match_by_value?(flags)
97
+ flags.match_by_value.any? { |e| e.match?(self) }
98
+ end
99
+
100
+ def match?(path_or_string)
101
+ # If the strings are literally equal, we're good..
102
+ return true if self == path_or_string
103
+
104
+ # Otherwise, compare segments: do we have the same number, and do they all #match?
105
+ other = reify(path_or_string)
106
+ return false if other.depth != self.depth
107
+
108
+ seg_pairs = self.segments.zip(other.segments)
109
+ seg_pairs.all? { |a, b| a.match?(b) }
110
+ end
111
+
112
+ def parent
113
+ return nil if root? # TODO: consider the Null Object pattern
114
+ self.class.new(segments[0..-2])
115
+ end
116
+
117
+ def root?
118
+ @segments.empty?
119
+ end
120
+
121
+ private
122
+
123
+ # O(n^2) check to see if any of the path's ancestors are on a list
124
+ # (as of this writing, this should never actually happen, but I'm being thorough)
125
+ def ancestor_on_list?(paths)
126
+ paths.any? { |path|
127
+ ancestors.any? { |ancestor| ancestor == path }
128
+ }
129
+ end
130
+
131
+ def explicitly_excluded?(flags)
132
+ return false if flags.reject_paths.empty?
133
+ return true if self_on_list?(flags.reject_paths)
134
+ return true if ancestor_on_list?(flags.reject_paths)
26
135
  false
27
136
  end
28
137
 
29
- def to_s
30
- SEPARATOR + @segments.join(SEPARATOR)
138
+ def implicitly_excluded?(flags)
139
+ return false if flags.select_paths.empty?
140
+ return false if self_on_list?(flags.select_paths)
141
+ return false if ancestor_on_list?(flags.select_paths)
142
+ true
31
143
  end
32
144
 
33
- def inspect
34
- to_s
145
+ # A path of "/foo/:id/bar/:name" has two key expressions:
146
+ # - "/foo/:id"
147
+ # - "/foo/:id/bar/:name"
148
+ def key_exprs
149
+ ( [self] + ancestors )
150
+ .reject { |path| path.root? }
151
+ .select { |path| path.segments.last&.key_expr? }
152
+ end
153
+
154
+ # O(n) check to see if the path itself is on a list
155
+ def self_on_list?(paths)
156
+ paths.any? { |path| self == path }
157
+ end
158
+
159
+ def too_deep?(flags)
160
+ return false if flags.max_depth.nil?
161
+ depth > flags.max_depth
35
162
  end
163
+
164
+ def unpack_key_exprs(path_list)
165
+ path_list
166
+ .map { |path| path.send(:key_exprs) }
167
+ .flatten
168
+ .uniq { |e| e.to_s } # use the block form so we don't have to implement #hash and #eql? in horrible ways
169
+ end
170
+
36
171
  end
37
172
 
38
173
  end