check_please 0.4.1 → 0.5.4
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 +4 -4
- data/.gitignore +3 -1
- data/Gemfile.lock +1 -1
- data/README.md +284 -24
- data/Rakefile +50 -3
- data/bin/gh-md-toc +350 -0
- data/lib/check_please.rb +85 -34
- data/lib/check_please/comparison.rb +145 -17
- data/lib/check_please/diffs.rb +9 -0
- data/lib/check_please/error.rb +24 -0
- data/lib/check_please/flag.rb +13 -3
- data/lib/check_please/path.rb +123 -14
- data/lib/check_please/path_segment.rb +74 -0
- data/lib/check_please/path_segment_matcher.rb +44 -0
- data/lib/check_please/reification.rb +50 -0
- data/lib/check_please/version.rb +1 -1
- data/usage_examples.rb +8 -2
- metadata +6 -2
@@ -13,7 +13,7 @@ module CheckPlease
|
|
13
13
|
catch(:max_diffs_reached) do
|
14
14
|
compare reference, candidate, CheckPlease::Path.root
|
15
15
|
end
|
16
|
-
diffs
|
16
|
+
diffs.filter_by_flags(@flags)
|
17
17
|
end
|
18
18
|
|
19
19
|
private
|
@@ -22,7 +22,7 @@ module CheckPlease
|
|
22
22
|
def compare(ref, can, path)
|
23
23
|
return if path.excluded?(flags)
|
24
24
|
|
25
|
-
case
|
25
|
+
case types_for_compare(ref, can)
|
26
26
|
when [ :array, :array ] ; compare_arrays ref, can, path
|
27
27
|
when [ :hash, :hash ] ; compare_hashes ref, can, path
|
28
28
|
when [ :other, :other ] ; compare_others ref, can, path
|
@@ -31,7 +31,7 @@ module CheckPlease
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
34
|
+
def types_for_compare(*list)
|
35
35
|
list.map { |e|
|
36
36
|
case e
|
37
37
|
when Array ; :array
|
@@ -42,29 +42,153 @@ module CheckPlease
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def compare_arrays(ref_array, can_array, path)
|
45
|
-
|
46
|
-
(
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
case
|
54
|
-
when ref_array.length < n ; record_diff ref, can, new_path, :extra
|
55
|
-
when can_array.length < n ; record_diff ref, can, new_path, :missing
|
56
|
-
else
|
57
|
-
compare ref, can, new_path
|
58
|
-
end
|
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
|
59
52
|
end
|
60
53
|
end
|
61
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)
|
60
|
+
|
61
|
+
key_values.compact! # NOTE: will break if nil is ever used as a key (but WHO WOULD DO THAT?!)
|
62
|
+
key_values.sort!
|
63
|
+
|
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
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
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
|
+
|
62
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
|
63
175
|
record_missing_keys ref_hash, can_hash, path
|
64
176
|
compare_common_keys ref_hash, can_hash, path
|
65
177
|
record_extra_keys ref_hash, can_hash, path
|
66
178
|
end
|
67
179
|
|
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
|
+
|
68
192
|
def record_missing_keys(ref_hash, can_hash, path)
|
69
193
|
keys = ref_hash.keys - can_hash.keys
|
70
194
|
keys.each do |k|
|
@@ -87,6 +211,10 @@ module CheckPlease
|
|
87
211
|
end
|
88
212
|
|
89
213
|
def compare_others(ref, can, path)
|
214
|
+
if flags.indifferent_values
|
215
|
+
ref = stringify_symbol(ref)
|
216
|
+
can = stringify_symbol(can)
|
217
|
+
end
|
90
218
|
return if ref == can
|
91
219
|
record_diff ref, can, path, :mismatch
|
92
220
|
end
|
data/lib/check_please/diffs.rb
CHANGED
@@ -47,6 +47,15 @@ module CheckPlease
|
|
47
47
|
@list.map(&:attributes)
|
48
48
|
end
|
49
49
|
|
50
|
+
def filter_by_flags(flags)
|
51
|
+
new_list = @list.reject { |diff| Path.new(diff.path).excluded?(flags) }
|
52
|
+
self.class.new(new_list, flags: flags)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s(flags = {})
|
56
|
+
CheckPlease::Printers.render(self, flags)
|
57
|
+
end
|
58
|
+
|
50
59
|
extend Forwardable
|
51
60
|
def_delegators :@list, *%i[
|
52
61
|
each
|
data/lib/check_please/error.rb
CHANGED
@@ -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
|
data/lib/check_please/flag.rb
CHANGED
@@ -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
|
37
|
-
@
|
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 @
|
60
|
+
if @repeatable
|
51
61
|
on[name] ||= []
|
52
62
|
on[name].concat(Array(val))
|
53
63
|
else
|
data/lib/check_please/path.rb
CHANGED
@@ -1,29 +1,75 @@
|
|
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
10
|
def self.root
|
7
|
-
new
|
11
|
+
new('/')
|
8
12
|
end
|
9
13
|
|
10
|
-
|
11
|
-
|
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
|
+
string = name_or_segments.to_s
|
21
|
+
if string =~ %r(//)
|
22
|
+
raise InvalidPath, "paths cannot have empty segments"
|
23
|
+
end
|
24
|
+
|
25
|
+
names = string.split(SEPARATOR)
|
26
|
+
names.shift until names.empty? || names.first =~ /\S/
|
27
|
+
segments = PathSegment.reify(names)
|
28
|
+
when Array
|
29
|
+
segments = PathSegment.reify(name_or_segments)
|
30
|
+
else
|
31
|
+
raise InvalidPath, "not sure what to do with #{name_or_segments.inspect}"
|
32
|
+
end
|
33
|
+
|
12
34
|
@segments = Array(segments)
|
35
|
+
|
13
36
|
@to_s = SEPARATOR + @segments.join(SEPARATOR)
|
14
37
|
freeze
|
38
|
+
rescue InvalidPathSegment => e
|
39
|
+
raise InvalidPath, e.message
|
15
40
|
end
|
16
41
|
|
17
42
|
def +(new_basename)
|
18
|
-
self.
|
43
|
+
new_segments = self.segments.dup
|
44
|
+
new_segments << new_basename # don't reify here; it'll get done on Path#initialize
|
45
|
+
self.class.new(new_segments)
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
self.to_s == other.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
def ancestors
|
53
|
+
list = []
|
54
|
+
p = self
|
55
|
+
loop do
|
56
|
+
break if p.root?
|
57
|
+
p = p.parent
|
58
|
+
list.unshift p
|
59
|
+
end
|
60
|
+
list.reverse
|
61
|
+
end
|
62
|
+
|
63
|
+
def basename
|
64
|
+
segments.last.to_s
|
19
65
|
end
|
20
66
|
|
21
67
|
def depth
|
22
|
-
1 +
|
68
|
+
1 + segments.length
|
23
69
|
end
|
24
70
|
|
25
71
|
def excluded?(flags)
|
26
|
-
return false if root?
|
72
|
+
return false if root? # that would just be silly
|
27
73
|
|
28
74
|
return true if too_deep?(flags)
|
29
75
|
return true if explicitly_excluded?(flags)
|
@@ -33,33 +79,96 @@ module CheckPlease
|
|
33
79
|
end
|
34
80
|
|
35
81
|
def inspect
|
36
|
-
"
|
82
|
+
"<#{self.class.name} '#{to_s}'>"
|
83
|
+
end
|
84
|
+
|
85
|
+
def key_to_match_by(flags)
|
86
|
+
key_exprs = unpack_key_exprs(flags.match_by_key)
|
87
|
+
# NOTE: match on parent because if self.to_s == '/foo', MBK '/foo/:id' should return 'id'
|
88
|
+
matches = key_exprs.select { |e| e.parent.match?(self) }
|
89
|
+
|
90
|
+
case matches.length
|
91
|
+
when 0 ; nil
|
92
|
+
when 1 ; matches.first.segments.last.key
|
93
|
+
else ; raise "More than one match_by_key expression for path '#{self}': #{matches.map(&:to_s).inspect}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def match_by_value?(flags)
|
98
|
+
flags.match_by_value.any? { |e| e.match?(self) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def match?(path_or_string)
|
102
|
+
# If the strings are literally equal, we're good..
|
103
|
+
return true if self == path_or_string
|
104
|
+
|
105
|
+
# Otherwise, compare segments: do we have the same number, and do they all #match?
|
106
|
+
other = reify(path_or_string)
|
107
|
+
return false if other.depth != self.depth
|
108
|
+
|
109
|
+
seg_pairs = self.segments.zip(other.segments)
|
110
|
+
seg_pairs.all? { |a, b| a.match?(b) }
|
111
|
+
end
|
112
|
+
|
113
|
+
def parent
|
114
|
+
return nil if root? # TODO: consider the Null Object pattern
|
115
|
+
self.class.new(segments[0..-2])
|
37
116
|
end
|
38
117
|
|
39
118
|
def root?
|
40
|
-
|
119
|
+
@segments.empty?
|
41
120
|
end
|
42
121
|
|
43
122
|
private
|
44
123
|
|
124
|
+
# O(n^2) check to see if any of the path's ancestors are on a list
|
125
|
+
# (as of this writing, this should never actually happen, but I'm being thorough)
|
126
|
+
def ancestor_on_list?(paths)
|
127
|
+
paths.any? { |path|
|
128
|
+
ancestors.any? { |ancestor| ancestor.match?(path) }
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
45
132
|
def explicitly_excluded?(flags)
|
46
|
-
flags.reject_paths.
|
133
|
+
return false if flags.reject_paths.empty?
|
134
|
+
return true if self_on_list?(flags.reject_paths)
|
135
|
+
return true if ancestor_on_list?(flags.reject_paths)
|
136
|
+
false
|
47
137
|
end
|
48
138
|
|
49
139
|
def implicitly_excluded?(flags)
|
50
140
|
return false if flags.select_paths.empty?
|
51
|
-
flags.select_paths
|
141
|
+
return false if self_on_list?(flags.select_paths)
|
142
|
+
return false if ancestor_on_list?(flags.select_paths)
|
143
|
+
true
|
144
|
+
end
|
145
|
+
|
146
|
+
# A path of "/foo/:id/bar/:name" has two key expressions:
|
147
|
+
# - "/foo/:id"
|
148
|
+
# - "/foo/:id/bar/:name"
|
149
|
+
def key_exprs
|
150
|
+
( [self] + ancestors )
|
151
|
+
.reject { |path| path.root? }
|
152
|
+
.select { |path| path.segments.last&.key_expr? }
|
52
153
|
end
|
53
154
|
|
54
|
-
#
|
55
|
-
def
|
56
|
-
|
155
|
+
# O(n) check to see if the path itself is on a list
|
156
|
+
def self_on_list?(paths)
|
157
|
+
paths.any? { |path| self.match?(path) }
|
57
158
|
end
|
58
159
|
|
59
160
|
def too_deep?(flags)
|
60
161
|
return false if flags.max_depth.nil?
|
61
|
-
flags.max_depth
|
162
|
+
depth > flags.max_depth
|
62
163
|
end
|
164
|
+
|
165
|
+
def unpack_key_exprs(path_list)
|
166
|
+
path_list
|
167
|
+
.map { |path| path.send(:key_exprs) }
|
168
|
+
.flatten
|
169
|
+
.uniq { |e| e.to_s } # use the block form so we don't have to implement #hash and #eql? in horrible ways
|
170
|
+
end
|
171
|
+
|
63
172
|
end
|
64
173
|
|
65
174
|
end
|