check_please 0.3.0 → 0.5.2
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 +306 -29
- data/Rakefile +50 -3
- data/bin/gh-md-toc +350 -0
- data/lib/check_please.rb +136 -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 +135 -40
- data/lib/check_please/diff.rb +3 -3
- 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/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 +8 -3
- data/lib/check_please/cli/flag.rb +0 -40
@@ -0,0 +1,88 @@
|
|
1
|
+
module CheckPlease
|
2
|
+
|
3
|
+
class Flag
|
4
|
+
attr_accessor :name
|
5
|
+
attr_writer :default # reader is defined below
|
6
|
+
attr_accessor :default_proc
|
7
|
+
attr_accessor :description
|
8
|
+
attr_accessor :cli_long
|
9
|
+
attr_accessor :cli_short
|
10
|
+
|
11
|
+
def initialize(attrs = {})
|
12
|
+
@validators = []
|
13
|
+
attrs.each do |name, value|
|
14
|
+
set_attribute! name, value
|
15
|
+
end
|
16
|
+
yield self if block_given?
|
17
|
+
freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def default
|
21
|
+
if default_proc
|
22
|
+
default_proc.call
|
23
|
+
else
|
24
|
+
@default
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def coerce(&block)
|
29
|
+
@coercer = block
|
30
|
+
end
|
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
|
+
|
42
|
+
def mutually_exclusive_to(flag_name)
|
43
|
+
@validators << ->(flags, _) { flags.send(flag_name).empty? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def repeatable
|
47
|
+
@repeatable = true
|
48
|
+
self.default_proc = ->{ Array.new }
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate(&block)
|
52
|
+
@validators << block
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def __set__(value, on:, flags:)
|
58
|
+
val = _coerce(value)
|
59
|
+
_validate(flags, val)
|
60
|
+
if @repeatable
|
61
|
+
on[name] ||= []
|
62
|
+
on[name].concat(Array(val))
|
63
|
+
else
|
64
|
+
on[name] = val
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def _coerce(value)
|
71
|
+
return value if @coercer.nil?
|
72
|
+
@coercer.call(value)
|
73
|
+
end
|
74
|
+
|
75
|
+
def _validate(flags, value)
|
76
|
+
return if @validators.empty?
|
77
|
+
return if @validators.all? { |block| block.call(flags, value) }
|
78
|
+
raise InvalidFlag, "#{value.inspect} is not a legal value for #{name}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def set_attribute!(name, value)
|
82
|
+
self.send "#{name}=", value
|
83
|
+
rescue NoMethodError
|
84
|
+
raise ArgumentError, "unrecognized attribute: #{name}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -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
|
@@ -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
|