check_please 0.4.0 → 0.5.3

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.
@@ -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