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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/Gemfile.lock +1 -1
- data/README.md +301 -29
- data/Rakefile +50 -3
- data/bin/gh-md-toc +350 -0
- data/lib/check_please.rb +77 -25
- data/lib/check_please/cli.rb +1 -1
- data/lib/check_please/cli/parser.rb +1 -1
- data/lib/check_please/comparison.rb +176 -40
- data/lib/check_please/diffs.rb +4 -5
- data/lib/check_please/error.rb +24 -0
- data/lib/check_please/flag.rb +13 -3
- data/lib/check_please/path.rb +148 -13
- data/lib/check_please/path_segment.rb +88 -0
- data/lib/check_please/reification.rb +50 -0
- data/lib/check_please/version.rb +1 -1
- data/usage_examples.rb +24 -2
- metadata +5 -2
data/lib/check_please/cli.rb
CHANGED
@@ -1,34 +1,37 @@
|
|
1
1
|
module CheckPlease
|
2
|
+
using Refinements
|
2
3
|
|
3
|
-
|
4
|
-
|
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
|
-
|
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
|
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
|
18
|
-
if (
|
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
|
23
|
-
when [ :array, :array ] ; compare_arrays ref, can, path
|
24
|
-
when [ :hash, :hash ] ; compare_hashes ref, can, path
|
25
|
-
when [ :other, :other ] ; compare_others ref, can, path
|
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
|
-
|
30
|
+
record_diff ref, can, path, :type_mismatch
|
28
31
|
end
|
29
32
|
end
|
30
33
|
|
31
|
-
def
|
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
|
42
|
-
|
43
|
-
(
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
data/lib/check_please/diffs.rb
CHANGED
@@ -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
|
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,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
|
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.
|
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 +
|
67
|
+
1 + segments.length
|
16
68
|
end
|
17
69
|
|
18
70
|
def excluded?(flags)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
if flags
|
24
|
-
|
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
|
30
|
-
|
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
|
-
|
34
|
-
|
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
|