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
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
|