check_please 0.2.3 → 0.5.0
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 +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)
|