check_please 0.2.4 → 0.5.1
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 +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 +8 -3
- data/lib/check_please/cli/flag.rb +0 -40
data/lib/check_please/cli.rb
CHANGED
@@ -1,45 +1,12 @@
|
|
1
|
-
require_relative 'cli/flag'
|
2
|
-
# require_relative 'cli/flags'
|
3
|
-
require_relative 'cli/parser'
|
4
|
-
require_relative 'cli/runner'
|
5
|
-
|
6
1
|
module CheckPlease
|
7
2
|
|
8
3
|
module CLI
|
9
|
-
|
10
|
-
|
11
|
-
end
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
FLAGS = []
|
16
|
-
def self.flag(long:, short: nil, &block)
|
17
|
-
flag = Flag.new(short, long, &block)
|
18
|
-
FLAGS << flag
|
19
|
-
end
|
20
|
-
|
21
|
-
##### Define CLI flags here #####
|
4
|
+
autoload :Runner, "check_please/cli/parser"
|
5
|
+
autoload :Parser, "check_please/cli/runner"
|
22
6
|
|
23
|
-
|
24
|
-
|
25
|
-
f.set_key :format, :to_sym
|
26
|
-
end
|
27
|
-
|
28
|
-
flag short: "-n MAX_DIFFS", long: "--max-diffs MAX_DIFFS" do |f|
|
29
|
-
f.desc = "Stop after encountering a specified number of diffs"
|
30
|
-
f.set_key :max_diffs, :to_i
|
31
|
-
end
|
32
|
-
|
33
|
-
flag long: "--fail-fast" do |f|
|
34
|
-
f.desc = "Stop after encountering the very first diff"
|
35
|
-
f.set_key(:max_diffs) { 1 }
|
36
|
-
end
|
37
|
-
|
38
|
-
flag short: "-d MAX_DEPTH", long: "--max-depth MAX_DEPTH" do |f|
|
39
|
-
f.desc = "Limit the number of levels to descend when comparing documents (NOTE: root has depth=1)"
|
40
|
-
f.set_key :max_depth, :to_i
|
7
|
+
def self.run(exe_file_name)
|
8
|
+
Runner.new(exe_file_name).run(*ARGV.dup)
|
41
9
|
end
|
42
|
-
|
43
10
|
end
|
44
11
|
|
45
12
|
end
|
@@ -4,36 +4,41 @@ module CheckPlease
|
|
4
4
|
module CLI
|
5
5
|
|
6
6
|
class Parser
|
7
|
-
class UnrecognizedOption < StandardError
|
8
|
-
include CheckPlease::Error
|
9
|
-
end
|
10
|
-
|
11
7
|
def initialize(exe_file_name)
|
12
|
-
@exe_file_name = exe_file_name
|
13
|
-
@optparse = OptionParser.new
|
14
|
-
@optparse.banner = banner
|
15
|
-
|
16
|
-
@options = {} # yuck
|
17
|
-
CheckPlease::CLI::FLAGS.each do |flag|
|
18
|
-
flag.visit_option_parser(@optparse, @options)
|
19
|
-
end
|
8
|
+
@exe_file_name = File.basename(exe_file_name)
|
20
9
|
end
|
21
10
|
|
22
|
-
# Unfortunately, OptionParser *really* wants to use closures.
|
23
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
11
|
+
# Unfortunately, OptionParser *really* wants to use closures. I haven't
|
12
|
+
# yet figured out how to get around this, but at least it's closing on a
|
13
|
+
# local instead of an ivar... progress?
|
14
|
+
def flags_from_args!(args)
|
15
|
+
flags = Flags.new
|
16
|
+
optparse = option_parser(flags: flags)
|
17
|
+
optparse.parse!(args) # removes recognized flags from `args`
|
18
|
+
return flags
|
27
19
|
rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
|
28
|
-
raise
|
20
|
+
raise InvalidFlag, e.message, cause: e
|
29
21
|
end
|
30
22
|
|
31
23
|
def help
|
32
|
-
|
24
|
+
option_parser.help
|
33
25
|
end
|
34
26
|
|
35
27
|
private
|
36
28
|
|
29
|
+
# NOTE: if flags is nil, you'll get something that can print help, but will explode when sent :parse
|
30
|
+
def option_parser(flags: nil)
|
31
|
+
OptionParser.new.tap do |optparse|
|
32
|
+
optparse.banner = banner
|
33
|
+
CheckPlease::Flags.each_flag do |flag|
|
34
|
+
args = [ flag.cli_short, flag.cli_long, flag.description ].flatten.compact
|
35
|
+
optparse.on(*args) do |value|
|
36
|
+
flags.send "#{flag.name}=", value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
37
42
|
def banner
|
38
43
|
<<~EOF
|
39
44
|
Usage: #{@exe_file_name} <reference> <candidate> [FLAGS]
|
@@ -14,8 +14,8 @@ module CLI
|
|
14
14
|
print_help_and_exit if args.empty?
|
15
15
|
|
16
16
|
begin
|
17
|
-
|
18
|
-
rescue
|
17
|
+
flags = @parser.flags_from_args!(args)
|
18
|
+
rescue InvalidFlag => e
|
19
19
|
print_help_and_exit e.message
|
20
20
|
end
|
21
21
|
|
@@ -31,7 +31,7 @@ module CLI
|
|
31
31
|
or print_help_and_exit "Missing <candidate> argument, AND nothing was piped in"
|
32
32
|
|
33
33
|
# Looks like we're good to go!
|
34
|
-
diff_view = CheckPlease.render_diff(reference, candidate,
|
34
|
+
diff_view = CheckPlease.render_diff(reference, candidate, flags)
|
35
35
|
puts diff_view
|
36
36
|
end
|
37
37
|
|
@@ -1,30 +1,33 @@
|
|
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
|
8
|
+
|
9
|
+
def perform(reference, candidate, flags = {})
|
10
|
+
@flags = Flags(flags) # whoa, it's almost like Java in here
|
11
|
+
@diffs = Diffs.new(flags: @flags)
|
5
12
|
|
6
|
-
def perform(reference, candidate, options = {})
|
7
|
-
root = CheckPlease::Path.new
|
8
|
-
diffs = Diffs.new(options: options)
|
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
25
|
case types(ref, can)
|
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
|
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
|
|
@@ -38,54 +41,121 @@ 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
|
+
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
|
49
|
+
end
|
50
|
+
end
|
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)
|
46
56
|
|
47
|
-
|
48
|
-
|
57
|
+
key_values.compact! # NOTE: will break if nil is ever used as a key (but WHO WOULD DO THAT?!)
|
58
|
+
key_values.sort!
|
49
59
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
54
69
|
end
|
55
70
|
end
|
56
|
-
end
|
57
71
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
+
|
123
|
+
def compare_hashes(ref_hash, can_hash, path)
|
124
|
+
record_missing_keys ref_hash, can_hash, path
|
125
|
+
compare_common_keys ref_hash, can_hash, path
|
126
|
+
record_extra_keys ref_hash, can_hash, path
|
62
127
|
end
|
63
128
|
|
64
|
-
def record_missing_keys(ref_hash, can_hash, path
|
129
|
+
def record_missing_keys(ref_hash, can_hash, path)
|
65
130
|
keys = ref_hash.keys - can_hash.keys
|
66
131
|
keys.each do |k|
|
67
|
-
|
132
|
+
record_diff ref_hash[k], nil, path + k, :missing
|
68
133
|
end
|
69
134
|
end
|
70
135
|
|
71
|
-
def compare_common_keys(ref_hash, can_hash, path
|
136
|
+
def compare_common_keys(ref_hash, can_hash, path)
|
72
137
|
keys = ref_hash.keys & can_hash.keys
|
73
138
|
keys.each do |k|
|
74
|
-
compare ref_hash[k], can_hash[k], path + k
|
139
|
+
compare ref_hash[k], can_hash[k], path + k
|
75
140
|
end
|
76
141
|
end
|
77
142
|
|
78
|
-
def record_extra_keys(ref_hash, can_hash, path
|
143
|
+
def record_extra_keys(ref_hash, can_hash, path)
|
79
144
|
keys = can_hash.keys - ref_hash.keys
|
80
145
|
keys.each do |k|
|
81
|
-
|
146
|
+
record_diff nil, can_hash[k], path + k, :extra
|
82
147
|
end
|
83
148
|
end
|
84
149
|
|
85
|
-
def compare_others(ref, can, path
|
150
|
+
def compare_others(ref, can, path)
|
86
151
|
return if ref == can
|
87
|
-
|
152
|
+
record_diff ref, can, path, :mismatch
|
88
153
|
end
|
154
|
+
|
155
|
+
def record_diff(ref, can, path, type)
|
156
|
+
diff = Diff.new(type, path, ref, can)
|
157
|
+
diffs << diff
|
158
|
+
end
|
89
159
|
end
|
90
160
|
|
91
161
|
end
|
data/lib/check_please/diff.rb
CHANGED
@@ -3,20 +3,12 @@ module CheckPlease
|
|
3
3
|
class Diff
|
4
4
|
COLUMNS = %i[ type path reference candidate ]
|
5
5
|
|
6
|
-
attr_reader
|
7
|
-
def initialize(type, reference, candidate
|
6
|
+
attr_reader(*COLUMNS)
|
7
|
+
def initialize(type, path, reference, candidate)
|
8
8
|
@type = type
|
9
|
+
@path = path.to_s
|
9
10
|
@reference = reference
|
10
11
|
@candidate = candidate
|
11
|
-
@path = path.to_s
|
12
|
-
end
|
13
|
-
|
14
|
-
def ref_display
|
15
|
-
reference.inspect
|
16
|
-
end
|
17
|
-
|
18
|
-
def can_display
|
19
|
-
candidate.inspect
|
20
12
|
end
|
21
13
|
|
22
14
|
def attributes
|
@@ -28,8 +20,8 @@ module CheckPlease
|
|
28
20
|
s << self.class.name
|
29
21
|
s << " type=#{type}"
|
30
22
|
s << " path=#{path}"
|
31
|
-
s << " ref=#{
|
32
|
-
s << " can=#{
|
23
|
+
s << " ref=#{reference.inspect}"
|
24
|
+
s << " can=#{candidate.inspect}"
|
33
25
|
s << ">"
|
34
26
|
s
|
35
27
|
end
|
data/lib/check_please/diffs.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
|
3
3
|
module CheckPlease
|
4
|
+
using Refinements
|
4
5
|
|
5
6
|
# Custom collection class for Diff instances.
|
6
7
|
# Can retrieve members using indexes or paths.
|
7
8
|
class Diffs
|
8
|
-
attr_reader :
|
9
|
-
def initialize(diff_list = nil,
|
10
|
-
@
|
9
|
+
attr_reader :flags
|
10
|
+
def initialize(diff_list = nil, flags: {})
|
11
|
+
@flags = Flags(flags)
|
11
12
|
@list = []
|
12
13
|
@hash = {}
|
13
14
|
Array(diff_list).each do |diff|
|
@@ -29,7 +30,11 @@ module CheckPlease
|
|
29
30
|
end
|
30
31
|
|
31
32
|
def <<(diff)
|
32
|
-
if
|
33
|
+
if flags.fail_fast && length > 0
|
34
|
+
throw :max_diffs_reached
|
35
|
+
end
|
36
|
+
|
37
|
+
if (n = flags.max_diffs)
|
33
38
|
# It seems no one can help me now / I'm in too deep, there's no way out
|
34
39
|
throw :max_diffs_reached if length >= n
|
35
40
|
end
|
@@ -38,14 +43,14 @@ module CheckPlease
|
|
38
43
|
@hash[diff.path] = diff
|
39
44
|
end
|
40
45
|
|
41
|
-
def record(ref, can, path, type)
|
42
|
-
self << Diff.new(type, ref, can, path)
|
43
|
-
end
|
44
|
-
|
45
46
|
def data
|
46
47
|
@list.map(&:attributes)
|
47
48
|
end
|
48
49
|
|
50
|
+
def to_s(flags = {})
|
51
|
+
CheckPlease::Printers.render(self, flags)
|
52
|
+
end
|
53
|
+
|
49
54
|
extend Forwardable
|
50
55
|
def_delegators :@list, *%i[
|
51
56
|
each
|
data/lib/check_please/error.rb
CHANGED
@@ -6,4 +6,28 @@ module CheckPlease
|
|
6
6
|
# instead....
|
7
7
|
end
|
8
8
|
|
9
|
+
class DuplicateKeyError < ::IndexError
|
10
|
+
include CheckPlease::Error
|
11
|
+
end
|
12
|
+
|
13
|
+
class InvalidFlag < ArgumentError
|
14
|
+
include CheckPlease::Error
|
15
|
+
end
|
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
|
+
|
9
33
|
end
|
@@ -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
|