check_please 0.2.3 → 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 +318 -47
- data/Rakefile +46 -3
- data/bin/gh-md-toc +350 -0
- data/{bin → exe}/check_please +1 -1
- data/lib/check_please.rb +116 -14
- data/lib/check_please/cli.rb +4 -37
- data/lib/check_please/cli/parser.rb +24 -19
- data/lib/check_please/cli/runner.rb +3 -3
- data/lib/check_please/comparison.rb +108 -38
- data/lib/check_please/diff.rb +5 -13
- data/lib/check_please/diffs.rb +13 -8
- data/lib/check_please/error.rb +24 -0
- data/lib/check_please/flag.rb +88 -0
- data/lib/check_please/flags.rb +46 -0
- data/lib/check_please/path.rb +151 -6
- data/lib/check_please/path_segment.rb +88 -0
- data/lib/check_please/printers.rb +8 -7
- data/lib/check_please/printers/table_print.rb +12 -4
- data/lib/check_please/refinements.rb +16 -0
- data/lib/check_please/reification.rb +50 -0
- data/lib/check_please/version.rb +1 -1
- data/usage_examples.rb +24 -2
- metadata +11 -5
- data/lib/check_please/cli/flag.rb +0 -40
@@ -0,0 +1,46 @@
|
|
1
|
+
module CheckPlease
|
2
|
+
|
3
|
+
# NOTE: this gets all of its attributes defined (via .define) in ../check_please.rb
|
4
|
+
|
5
|
+
class Flags
|
6
|
+
BY_NAME = {} ; private_constant :BY_NAME
|
7
|
+
|
8
|
+
def self.[](name)
|
9
|
+
BY_NAME[name.to_sym]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.define(name, &block)
|
13
|
+
flag = Flag.new(name: name.to_sym, &block)
|
14
|
+
BY_NAME[flag.name] = flag
|
15
|
+
define_accessors flag
|
16
|
+
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.each_flag
|
21
|
+
BY_NAME.each do |_, flag|
|
22
|
+
yield flag
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.define_accessors(flag)
|
27
|
+
getter = flag.name
|
28
|
+
define_method(getter) {
|
29
|
+
@attributes.fetch(flag.name) { flag.default }
|
30
|
+
}
|
31
|
+
|
32
|
+
setter = :"#{flag.name}="
|
33
|
+
define_method(setter) { |value|
|
34
|
+
flag.send :__set__, value, on: @attributes, flags: self
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(attrs = {})
|
39
|
+
@attributes = {}
|
40
|
+
attrs.each do |name, value|
|
41
|
+
send "#{name}=", value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/check_please/path.rb
CHANGED
@@ -1,27 +1,172 @@
|
|
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
|
-
def
|
19
|
-
|
70
|
+
def excluded?(flags)
|
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
|
20
78
|
end
|
21
79
|
|
22
80
|
def inspect
|
23
|
-
to_s
|
81
|
+
"<#{self.class.name} '#{to_s}'>"
|
24
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])
|
114
|
+
end
|
115
|
+
|
116
|
+
def root?
|
117
|
+
@segments.empty?
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
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
|
+
|
130
|
+
def explicitly_excluded?(flags)
|
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
|
135
|
+
end
|
136
|
+
|
137
|
+
def implicitly_excluded?(flags)
|
138
|
+
return false if flags.select_paths.empty?
|
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? }
|
151
|
+
end
|
152
|
+
|
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 }
|
156
|
+
end
|
157
|
+
|
158
|
+
def too_deep?(flags)
|
159
|
+
return false if flags.max_depth.nil?
|
160
|
+
depth > flags.max_depth
|
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
|
+
|
25
170
|
end
|
26
171
|
|
27
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
|
@@ -1,10 +1,11 @@
|
|
1
|
-
require_relative 'printers/base'
|
2
|
-
require_relative 'printers/json'
|
3
|
-
require_relative 'printers/table_print'
|
4
|
-
|
5
1
|
module CheckPlease
|
2
|
+
using Refinements
|
6
3
|
|
7
4
|
module Printers
|
5
|
+
autoload :Base, "check_please/printers/base"
|
6
|
+
autoload :JSON, "check_please/printers/json"
|
7
|
+
autoload :TablePrint, "check_please/printers/table_print"
|
8
|
+
|
8
9
|
PRINTERS_BY_FORMAT = {
|
9
10
|
table: Printers::TablePrint,
|
10
11
|
json: Printers::JSON,
|
@@ -12,9 +13,9 @@ module CheckPlease
|
|
12
13
|
FORMATS = PRINTERS_BY_FORMAT.keys.sort
|
13
14
|
DEFAULT_FORMAT = :table
|
14
15
|
|
15
|
-
def self.render(diffs,
|
16
|
-
|
17
|
-
printer = PRINTERS_BY_FORMAT[format
|
16
|
+
def self.render(diffs, flags = {})
|
17
|
+
flags = Flags(flags)
|
18
|
+
printer = PRINTERS_BY_FORMAT[flags.format]
|
18
19
|
printer.render(diffs)
|
19
20
|
end
|
20
21
|
end
|
@@ -4,11 +4,19 @@ module CheckPlease
|
|
4
4
|
module Printers
|
5
5
|
|
6
6
|
class TablePrint < Base
|
7
|
+
InspectStrings = Object.new.tap do |obj|
|
8
|
+
def obj.format(value)
|
9
|
+
value.is_a?(String) ? value.inspect : value
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
PATH_MAX_WIDTH = 250 # if you hit this limit, you have other problems
|
14
|
+
|
7
15
|
TP_OPTS = [
|
8
|
-
:
|
9
|
-
{ :
|
10
|
-
:
|
11
|
-
:
|
16
|
+
{ type: { display_name: "Type" } },
|
17
|
+
{ path: { display_name: "Path", width: PATH_MAX_WIDTH } },
|
18
|
+
{ reference: { display_name: "Reference", formatters: [ InspectStrings ] } },
|
19
|
+
{ candidate: { display_name: "Candidate", formatters: [ InspectStrings ] } },
|
12
20
|
]
|
13
21
|
|
14
22
|
def to_s
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CheckPlease
|
2
|
+
|
3
|
+
module Refinements
|
4
|
+
refine Kernel do
|
5
|
+
def Flags(flags_or_hash)
|
6
|
+
case flags_or_hash
|
7
|
+
when Flags ; return flags_or_hash
|
8
|
+
when Hash ; return Flags.new(flags_or_hash)
|
9
|
+
else
|
10
|
+
raise ArgumentError, "Expected either a CheckPlease::Flags or a Hash; got #{flags_or_hash.inspect}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
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
|
data/lib/check_please/version.rb
CHANGED
data/usage_examples.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
require 'check_please'
|
2
2
|
|
3
|
-
reference = { foo
|
4
|
-
candidate = { bar
|
3
|
+
reference = { "foo" => "wibble" }
|
4
|
+
candidate = { "bar" => "wibble" }
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
##### Printing diffs #####
|
5
9
|
|
6
10
|
puts CheckPlease.render_diff(reference, candidate)
|
7
11
|
|
@@ -12,3 +16,21 @@ _ = <<EOF
|
|
12
16
|
missing | /foo | wibble |
|
13
17
|
extra | /bar | | wibble
|
14
18
|
EOF
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
##### Doing your own thing with diffs #####
|
23
|
+
|
24
|
+
diffs = CheckPlease.diff(reference, candidate)
|
25
|
+
|
26
|
+
# `diffs` is a custom collection (type: CheckPlease::Diffs) that contains
|
27
|
+
# individual Diff objects for you to inspect as you see fit.
|
28
|
+
#
|
29
|
+
# If you come up with a useful way to present these, feel free to submit a PR
|
30
|
+
# with a new class in `lib/check_please/printers` !
|
31
|
+
|
32
|
+
# To print these in the console, you can just do:
|
33
|
+
puts diffs
|
34
|
+
|
35
|
+
# If for some reason you want to print the JSON version, it gets a little more verbose:
|
36
|
+
puts diffs.to_s(format: :json)
|