check_please 0.4.1 → 0.5.0
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 +46 -3
- data/bin/gh-md-toc +350 -0
- data/lib/check_please.rb +41 -25
- data/lib/check_please/comparison.rb +75 -14
- data/lib/check_please/diffs.rb +4 -0
- data/lib/check_please/error.rb +20 -0
- data/lib/check_please/flag.rb +13 -3
- data/lib/check_please/path.rb +121 -14
- 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 +8 -2
- metadata +5 -2
@@ -42,23 +42,84 @@ module CheckPlease
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def compare_arrays(ref_array, can_array, path)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
ref = ref_array[i]
|
51
|
-
can = can_array[i]
|
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
|
+
if ( key = path.key_for_compare(flags) )
|
46
|
+
compare_arrays_by_key ref_array, can_array, path, key
|
47
|
+
else
|
48
|
+
compare_arrays_by_index ref_array, can_array, path
|
59
49
|
end
|
60
50
|
end
|
61
51
|
|
52
|
+
def compare_arrays_by_key(ref_array, can_array, path, key_name)
|
53
|
+
refs_by_key = index_array!(ref_array, path, key_name, "reference")
|
54
|
+
cans_by_key = index_array!(can_array, path, key_name, "candidate")
|
55
|
+
key_values = (refs_by_key.keys | cans_by_key.keys)
|
56
|
+
|
57
|
+
key_values.compact! # NOTE: will break if nil is ever used as a key (but WHO WOULD DO THAT?!)
|
58
|
+
key_values.sort!
|
59
|
+
|
60
|
+
key_values.each do |key_value|
|
61
|
+
new_path = path + "#{key_name}=#{key_value}"
|
62
|
+
ref = refs_by_key[key_value]
|
63
|
+
can = cans_by_key[key_value]
|
64
|
+
case
|
65
|
+
when ref.nil? ; record_diff ref, can, new_path, :extra
|
66
|
+
when can.nil? ; record_diff ref, can, new_path, :missing
|
67
|
+
else ; compare ref, can, new_path
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def index_array!(array_of_hashes, path, key_name, ref_or_can)
|
73
|
+
elements_by_key = {}
|
74
|
+
|
75
|
+
array_of_hashes.each.with_index do |h, i|
|
76
|
+
# make sure we have a hash
|
77
|
+
unless h.is_a?(Hash)
|
78
|
+
raise CheckPlease::TypeMismatchError, \
|
79
|
+
"The element at position #{i} in the #{ref_or_can} array is not a hash."
|
80
|
+
end
|
81
|
+
|
82
|
+
# try to get the value of the attribute identified by key_name
|
83
|
+
key_value = h.fetch(key_name) {
|
84
|
+
raise CheckPlease::NoSuchKeyError, \
|
85
|
+
<<~EOF
|
86
|
+
The #{ref_or_can} hash at position #{i} has no #{key_name.inspect} key.
|
87
|
+
Keys it does have: #{h.keys.inspect}
|
88
|
+
EOF
|
89
|
+
}
|
90
|
+
|
91
|
+
# complain about dupes
|
92
|
+
if elements_by_key.has_key?(key_value)
|
93
|
+
key_val_expr = "#{key_name}=#{key_value}"
|
94
|
+
raise CheckPlease::DuplicateKeyError, \
|
95
|
+
"Duplicate #{ref_or_can} element found at path '#{path + key_val_expr}'."
|
96
|
+
end
|
97
|
+
|
98
|
+
# ok, now we can proceed
|
99
|
+
elements_by_key[key_value] = h
|
100
|
+
end
|
101
|
+
|
102
|
+
elements_by_key
|
103
|
+
end
|
104
|
+
|
105
|
+
def compare_arrays_by_index(ref_array, can_array, path)
|
106
|
+
max_len = [ ref_array, can_array ].map(&:length).max
|
107
|
+
(0...max_len).each do |i|
|
108
|
+
n = i + 1 # count in human pls
|
109
|
+
new_path = path + n
|
110
|
+
|
111
|
+
ref = ref_array[i]
|
112
|
+
can = can_array[i]
|
113
|
+
|
114
|
+
case
|
115
|
+
when ref_array.length < n ; record_diff ref, can, new_path, :extra
|
116
|
+
when can_array.length < n ; record_diff ref, can, new_path, :missing
|
117
|
+
else
|
118
|
+
compare ref, can, new_path
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
62
123
|
def compare_hashes(ref_hash, can_hash, path)
|
63
124
|
record_missing_keys ref_hash, can_hash, path
|
64
125
|
compare_common_keys ref_hash, can_hash, path
|
data/lib/check_please/diffs.rb
CHANGED
data/lib/check_please/error.rb
CHANGED
@@ -6,8 +6,28 @@ module CheckPlease
|
|
6
6
|
# instead....
|
7
7
|
end
|
8
8
|
|
9
|
+
class DuplicateKeyError < ::IndexError
|
10
|
+
include CheckPlease::Error
|
11
|
+
end
|
12
|
+
|
9
13
|
class InvalidFlag < ArgumentError
|
10
14
|
include CheckPlease::Error
|
11
15
|
end
|
12
16
|
|
17
|
+
class InvalidPath < ArgumentError
|
18
|
+
include CheckPlease::Error
|
19
|
+
end
|
20
|
+
|
21
|
+
class InvalidPathSegment < ArgumentError
|
22
|
+
include CheckPlease::Error
|
23
|
+
end
|
24
|
+
|
25
|
+
class NoSuchKeyError < ::KeyError
|
26
|
+
include CheckPlease::Error
|
27
|
+
end
|
28
|
+
|
29
|
+
class TypeMismatchError < ::TypeError
|
30
|
+
include CheckPlease::Error
|
31
|
+
end
|
32
|
+
|
13
33
|
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,74 @@
|
|
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
|
+
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
|
+
|
12
33
|
@segments = Array(segments)
|
34
|
+
|
13
35
|
@to_s = SEPARATOR + @segments.join(SEPARATOR)
|
14
36
|
freeze
|
37
|
+
rescue InvalidPathSegment => e
|
38
|
+
raise InvalidPath, e.message
|
15
39
|
end
|
16
40
|
|
17
41
|
def +(new_basename)
|
18
|
-
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
|
19
64
|
end
|
20
65
|
|
21
66
|
def depth
|
22
|
-
1 +
|
67
|
+
1 + segments.length
|
23
68
|
end
|
24
69
|
|
25
70
|
def excluded?(flags)
|
26
|
-
return false if root?
|
71
|
+
return false if root? # that would just be silly
|
27
72
|
|
28
73
|
return true if too_deep?(flags)
|
29
74
|
return true if explicitly_excluded?(flags)
|
@@ -33,33 +78,95 @@ module CheckPlease
|
|
33
78
|
end
|
34
79
|
|
35
80
|
def inspect
|
36
|
-
"
|
81
|
+
"<#{self.class.name} '#{to_s}'>"
|
82
|
+
end
|
83
|
+
|
84
|
+
# TODO: Naming Things
|
85
|
+
def key_for_compare(flags)
|
86
|
+
mbk_exprs = unpack_mbk_exprs(flags)
|
87
|
+
matches = mbk_exprs.select { |mbk_expr|
|
88
|
+
# NOTE: matching on parent because MBK '/foo/:id' should return 'id' for path '/foo'
|
89
|
+
mbk_expr.parent.match?(self)
|
90
|
+
}
|
91
|
+
|
92
|
+
case matches.length
|
93
|
+
when 0 ; nil
|
94
|
+
when 1 ; matches.first.segments.last.key
|
95
|
+
else ; raise "More than one match_by_key expression for path '#{self}': #{matches.map(&:to_s).inspect}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def match?(path_or_string)
|
100
|
+
# If the strings are literally equal, we're good..
|
101
|
+
return true if self == path_or_string
|
102
|
+
|
103
|
+
# Otherwise, compare segments: do we have the same number, and do they all #match?
|
104
|
+
other = reify(path_or_string)
|
105
|
+
return false if other.depth != self.depth
|
106
|
+
|
107
|
+
seg_pairs = self.segments.zip(other.segments)
|
108
|
+
seg_pairs.all? { |a, b| a.match?(b) }
|
109
|
+
end
|
110
|
+
|
111
|
+
def parent
|
112
|
+
return nil if root? # TODO: consider the Null Object pattern
|
113
|
+
self.class.new(segments[0..-2])
|
37
114
|
end
|
38
115
|
|
39
116
|
def root?
|
40
|
-
|
117
|
+
@segments.empty?
|
41
118
|
end
|
42
119
|
|
43
120
|
private
|
44
121
|
|
122
|
+
# O(n^2) check to see if any of the path's ancestors are on a list
|
123
|
+
# (as of this writing, this should never actually happen, but I'm being thorough)
|
124
|
+
def ancestor_on_list?(paths)
|
125
|
+
paths.any? { |path|
|
126
|
+
ancestors.any? { |ancestor| ancestor == path }
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
45
130
|
def explicitly_excluded?(flags)
|
46
|
-
flags.reject_paths.
|
131
|
+
return false if flags.reject_paths.empty?
|
132
|
+
return true if self_on_list?(flags.reject_paths)
|
133
|
+
return true if ancestor_on_list?(flags.reject_paths)
|
134
|
+
false
|
47
135
|
end
|
48
136
|
|
49
137
|
def implicitly_excluded?(flags)
|
50
138
|
return false if flags.select_paths.empty?
|
51
|
-
flags.select_paths
|
139
|
+
return false if self_on_list?(flags.select_paths)
|
140
|
+
return false if ancestor_on_list?(flags.select_paths)
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
# A path of "/foo/:id/bar/:name" has two key expressions:
|
145
|
+
# - "/foo/:id"
|
146
|
+
# - "/foo/:id/bar/:name"
|
147
|
+
def key_exprs
|
148
|
+
( [self] + ancestors )
|
149
|
+
.reject { |path| path.root? }
|
150
|
+
.select { |path| path.segments.last&.key_expr? }
|
52
151
|
end
|
53
152
|
|
54
|
-
#
|
55
|
-
def
|
56
|
-
|
153
|
+
# O(n) check to see if the path itself is on a list
|
154
|
+
def self_on_list?(paths)
|
155
|
+
paths.any? { |path| self == path }
|
57
156
|
end
|
58
157
|
|
59
158
|
def too_deep?(flags)
|
60
159
|
return false if flags.max_depth.nil?
|
61
|
-
flags.max_depth
|
160
|
+
depth > flags.max_depth
|
62
161
|
end
|
162
|
+
|
163
|
+
def unpack_mbk_exprs(flags)
|
164
|
+
flags.match_by_key
|
165
|
+
.map { |path| path.send(:key_exprs) }
|
166
|
+
.flatten
|
167
|
+
.uniq { |e| e.to_s } # use the block form so we don't have to implement #hash and #eql? in horrible ways
|
168
|
+
end
|
169
|
+
|
63
170
|
end
|
64
171
|
|
65
172
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module CheckPlease
|
2
|
+
|
3
|
+
class PathSegment
|
4
|
+
include CheckPlease::Reification
|
5
|
+
can_reify String, Symbol, Numeric, nil
|
6
|
+
|
7
|
+
KEY_EXPR = %r{
|
8
|
+
^
|
9
|
+
\: # a literal colon
|
10
|
+
( # capture key
|
11
|
+
[^\:]+ # followed by one or more things that aren't colons
|
12
|
+
) # end capture key
|
13
|
+
$
|
14
|
+
}x
|
15
|
+
|
16
|
+
KEY_VAL_EXPR = %r{
|
17
|
+
^
|
18
|
+
( # capture key
|
19
|
+
[^=]+ # stuff (just not an equal sign)
|
20
|
+
) # end capture key
|
21
|
+
\= # an equal sign
|
22
|
+
( # capture key value
|
23
|
+
[^=]+ # stuff (just not an equal sign)
|
24
|
+
) # end capture key value
|
25
|
+
$
|
26
|
+
}x
|
27
|
+
|
28
|
+
attr_reader :name, :key, :key_value
|
29
|
+
alias_method :to_s, :name
|
30
|
+
|
31
|
+
def initialize(name = nil)
|
32
|
+
@name = name.to_s.strip
|
33
|
+
if @name =~ %r(\s) # has any whitespace
|
34
|
+
raise InvalidPathSegment, <<~EOF
|
35
|
+
#{name.inspect} is not a valid #{self.class} name
|
36
|
+
EOF
|
37
|
+
end
|
38
|
+
parse_key_and_value
|
39
|
+
freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
def empty?
|
43
|
+
name.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
def key_expr?
|
47
|
+
name.match?(KEY_EXPR)
|
48
|
+
end
|
49
|
+
|
50
|
+
def key_val_expr?
|
51
|
+
name.match?(KEY_VAL_EXPR)
|
52
|
+
end
|
53
|
+
|
54
|
+
def match?(other_segment_or_string)
|
55
|
+
other = self.class.reify(other_segment_or_string)
|
56
|
+
|
57
|
+
match_types = [ self.match_type, other.match_type ]
|
58
|
+
case match_types
|
59
|
+
when [ :plain, :plain ] ; self.name == other.name
|
60
|
+
when [ :key, :key_value ] ; self.key == other.key
|
61
|
+
when [ :key_value, :key ] ; self.key == other.key
|
62
|
+
else ; false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def match_type
|
69
|
+
return :key if key_expr?
|
70
|
+
return :key_value if key_val_expr?
|
71
|
+
:plain
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def parse_key_and_value
|
77
|
+
case name
|
78
|
+
when KEY_EXPR
|
79
|
+
@key = $1
|
80
|
+
when KEY_VAL_EXPR
|
81
|
+
@key, @key_value = $1, $2
|
82
|
+
else
|
83
|
+
# :nothingtodohere:
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module CheckPlease
|
2
|
+
|
3
|
+
module Reification
|
4
|
+
def self.included(receiver)
|
5
|
+
receiver.extend ClassMethods
|
6
|
+
receiver.send :include, InstanceMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def reifiable
|
11
|
+
@_reifiable ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_reify(*klasses)
|
15
|
+
klasses.flatten!
|
16
|
+
|
17
|
+
unless ( klasses - [nil] ).all? { |e| e.is_a?(Class) }
|
18
|
+
raise ArgumentError, "classes (or nil) only, please"
|
19
|
+
end
|
20
|
+
|
21
|
+
reifiable.concat klasses
|
22
|
+
reifiable.uniq!
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def reify(primitive_or_object)
|
27
|
+
case primitive_or_object
|
28
|
+
when self ; return primitive_or_object
|
29
|
+
when Array ; return primitive_or_object.map { |e| reify(e) }
|
30
|
+
when *reifiable ; return new(primitive_or_object)
|
31
|
+
end
|
32
|
+
# note early return ^^^
|
33
|
+
|
34
|
+
# that didn't work? complain!
|
35
|
+
acceptable = reifiable.map { |e| Class === e ? e.name : e.inspect }
|
36
|
+
raise ArgumentError, <<~EOF
|
37
|
+
#{self}.reify was given: #{primitive_or_object.inspect}
|
38
|
+
but only accepts: #{acceptable.join(", ")}
|
39
|
+
EOF
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module InstanceMethods
|
44
|
+
def reify(x)
|
45
|
+
self.class.reify(x)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|