check_please 0.2.2 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +43 -31
- data/{bin → exe}/check_please +1 -1
- data/lib/check_please.rb +110 -20
- 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 +39 -30
- data/lib/check_please/diff.rb +5 -13
- data/lib/check_please/diffs.rb +9 -8
- data/lib/check_please/error.rb +4 -0
- data/lib/check_please/flag.rb +78 -0
- data/lib/check_please/flags.rb +46 -0
- data/lib/check_please/path.rb +41 -3
- data/lib/check_please/printers.rb +8 -7
- data/lib/check_please/printers/json.rb +0 -2
- data/lib/check_please/printers/table_print.rb +18 -5
- data/lib/check_please/refinements.rb +16 -0
- data/lib/check_please/version.rb +1 -1
- data/usage_examples.rb +16 -0
- metadata +8 -5
- data/lib/check_please/cli/flag.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 41ad78ce4324c1b5d9c8ac3e336d46108850d6fd7d1d51d30c13b807632d612a
|
4
|
+
data.tar.gz: 9191e6e52c57b6d79761a8a0258a41378cf19af1364a3e8875e727ff8e501f06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 388b93c0277580a37b066b7fa6aade4f8feeb5f677a4ccc2221ac060d23df153c3dc16c6cdacce740448773665200855e653d74f55cfae659511e39a4391b36f
|
7
|
+
data.tar.gz: 44cb45ace2a720d90035eef279f490896a785e76c77bd4bd72c63400a828300332638442599b905dad8e4bde4b329a698fd44f4116f847699b5250c586e27c86
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# check_please
|
2
2
|
|
3
|
-
Check for differences between two JSON
|
3
|
+
Check for differences between two JSON documents, YAML documents, or Ruby data
|
4
|
+
structures parsed from either of those.
|
4
5
|
|
5
6
|
## Installation
|
6
7
|
|
@@ -18,11 +19,10 @@ Or install it yourself as:
|
|
18
19
|
|
19
20
|
$ gem install check_please
|
20
21
|
|
21
|
-
##
|
22
|
-
|
23
|
-
### Terminology
|
22
|
+
## Terminology
|
24
23
|
|
25
|
-
|
24
|
+
I know, you just want to see how to use this thing. Feel free to scroll down,
|
25
|
+
but be aware that CheckPlease uses a few words in a jargony way:
|
26
26
|
|
27
27
|
* **Reference** is always used to refer to the "target" or "source of truth."
|
28
28
|
We assume you're comparing two things because you want one of them to be like
|
@@ -35,7 +35,9 @@ CheckPlease uses a few words in a jargony way:
|
|
35
35
|
**reference** and the **candidate**. More on this in "Understanding the Output",
|
36
36
|
below.
|
37
37
|
|
38
|
-
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
### From the Terminal
|
39
41
|
|
40
42
|
Use the `bin/check_please` executable. (To get started, run it with the '-h' flag.)
|
41
43
|
|
@@ -46,19 +48,26 @@ of giving it a second filename as the argument. (This is especially useful if
|
|
46
48
|
you're copying an XHR response out of a web browser's dev tools and have a tool
|
47
49
|
like MacOS's `pbpaste` utility.)
|
48
50
|
|
49
|
-
### RSpec
|
51
|
+
### From RSpec
|
50
52
|
|
51
53
|
See [check_please_rspec_matcher](https://github.com/RealGeeks/check_please_rspec_matcher).
|
52
54
|
|
53
|
-
|
55
|
+
If you'd like more control over the output formatting, and especially if you'd
|
56
|
+
like to provide custom logic for diffing your own classes, you might be better
|
57
|
+
served by the [super_diff](https://github.com/mcmire/super_diff) gem. Check it
|
58
|
+
out!
|
54
59
|
|
55
|
-
|
56
|
-
back a third string containing a nicely formatted report of all the differences
|
57
|
-
CheckPlease found in the two JSON strings. (See also: [./usage_examples.rb](usage_examples.rb).)
|
60
|
+
### From Ruby
|
58
61
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
+
See also: [./usage_examples.rb](usage_examples.rb).
|
63
|
+
|
64
|
+
Create two strings, each containing a JSON or YAML document, and pass them to
|
65
|
+
`CheckPlease.render_diff`. You'll get back a third string containing a report
|
66
|
+
of all the differences CheckPlease found in the two JSON strings.
|
67
|
+
|
68
|
+
Or, if you'd like to inspect the diffs in your own way, use `CheckPlease.diff`
|
69
|
+
instead. You'll get back a `CheckPlease::Diffs` custom collection that
|
70
|
+
contains `CheckPlease::Diff` instances.
|
62
71
|
|
63
72
|
### Understanding the Output
|
64
73
|
|
@@ -70,9 +79,7 @@ tool because you want a human-friendly summary of all the places that your
|
|
70
79
|
**candidate** fell short.
|
71
80
|
|
72
81
|
When CheckPlease compares your two samples, it generates a list of diffs to
|
73
|
-
describe any discrepancies it encounters.
|
74
|
-
tabular format, but if you want to incorporate this into another toolchain,
|
75
|
-
CheckPlease can also print these diffs as JSON to facilitate parsing.)
|
82
|
+
describe any discrepancies it encounters.
|
76
83
|
|
77
84
|
An example would probably help here.
|
78
85
|
|
@@ -124,23 +131,22 @@ CheckPlease defines:
|
|
124
131
|
* **type_mismatch** means that both the **reference** and the **candidate** had
|
125
132
|
a value at the given path, but one value was an Array or a Hash and the other
|
126
133
|
was not. **When CheckPlease encounters a type mismatch, it does not compare
|
127
|
-
anything "below" the given path.**
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
Also, the way these get displayed is likely to change.)_
|
134
|
+
anything "below" the given path.** _(Technical note: CheckPlease uses a
|
135
|
+
"recursive descent" strategy to traverse the **reference** data structure,
|
136
|
+
and it stops when it encounters a type mismatch in order to avoid producing a
|
137
|
+
lot of "garbage" diff output.)_
|
132
138
|
* **mismatch** means that both the **reference** and the **candidate** had a
|
133
139
|
value at the given path, and neither value was an Array or a Hash.
|
134
|
-
* **extra** means that, inside an Array or a Hash, the **candidate**
|
135
|
-
|
140
|
+
* **extra** means that, inside an Array or a Hash, the **candidate** contained
|
141
|
+
values that were not found in the **reference**.
|
136
142
|
* **missing** is the opposite of **extra**: inside an Array or a Hash, the
|
137
143
|
**reference** contained values that were not found in the **candidate**.
|
138
144
|
|
139
145
|
#### Paths
|
140
146
|
|
141
|
-
The second column contains a path expression. This is extremely
|
147
|
+
The second column contains a path expression. This is extremely lo-fi:
|
142
148
|
|
143
|
-
* The
|
149
|
+
* The root of the data structure is defined as "/".
|
144
150
|
* If an element in the data structure is an array, its child elements will have
|
145
151
|
a **one-based** index appended to their parent's path.
|
146
152
|
* If an element in the data structure is an object ("Hash" in Ruby), the key
|
@@ -160,22 +166,28 @@ If you want to incorporate CheckPlease into some other toolchain, it can also
|
|
160
166
|
print diffs as JSON to facilitate parsing. In Ruby, pass `format: :json` to
|
161
167
|
`CheckPlease.render_diff`; in the CLI, use the `-f`/`--format` switch.
|
162
168
|
|
163
|
-
## TODO
|
169
|
+
## TODO (maybe)
|
164
170
|
|
165
171
|
* command line flags for :allthethings:!
|
166
172
|
* sort by path?
|
167
|
-
* max depth (for iterative refinement?)
|
168
173
|
* detect timestamps and compare after parsing?
|
169
174
|
* ignore sub-second precision (option / CLI flag)?
|
170
175
|
* possibly support plugins for other folks to add custom coercions?
|
171
|
-
* support expressions of specific paths to ignore
|
172
|
-
* wildcards? `#` for indexes, `**` to match one or more path segments?
|
173
|
-
(This could get ugly fast.)
|
174
176
|
* display filters? (e.g., { a: 1, b: 2 } ==> "Hash#3")
|
175
177
|
* shorter descriptions of values with different classes
|
176
178
|
(but maybe just the existing :type_mismatch diffs?)
|
177
179
|
* another "possibly support plugins" expansion point here
|
178
180
|
* more output formats, maybe?
|
181
|
+
* [0xABAD1DEA] support wildcards in --select-paths and --reject-paths?
|
182
|
+
* `#` for indexes, `**` to match one or more path segments?
|
183
|
+
(This could get ugly fast.)
|
184
|
+
* [0xABAD1DEA] look for a config file in ./.check_please_config or ~/.check_please_config,
|
185
|
+
combine flags found there with those in ARGV in order of precedence:
|
186
|
+
1) ARGV
|
187
|
+
2) ./.check_please
|
188
|
+
3) ~/.check_please
|
189
|
+
* but this may not actually be worth the time and complexity to implement, so
|
190
|
+
think about this first...
|
179
191
|
|
180
192
|
## Development
|
181
193
|
|
data/{bin → exe}/check_please
RENAMED
data/lib/check_please.rb
CHANGED
@@ -1,40 +1,130 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
|
5
|
+
# easier to just require these
|
6
|
+
require "check_please/error"
|
7
|
+
require "check_please/version"
|
8
|
+
|
9
|
+
module CheckPlease
|
10
|
+
autoload :CLI, "check_please/cli"
|
11
|
+
autoload :Comparison, "check_please/comparison"
|
12
|
+
autoload :Diff, "check_please/diff"
|
13
|
+
autoload :Diffs, "check_please/diffs"
|
14
|
+
autoload :Flag, "check_please/flag"
|
15
|
+
autoload :Flags, "check_please/flags"
|
16
|
+
autoload :Path, "check_please/path"
|
17
|
+
autoload :Printers, "check_please/printers"
|
18
|
+
autoload :Refinements, "check_please/refinements"
|
19
|
+
end
|
20
|
+
|
21
|
+
|
9
22
|
|
10
23
|
module CheckPlease
|
11
24
|
ELEVATOR_PITCH = "Tool for parsing and diffing two JSON documents."
|
12
25
|
|
13
|
-
def self.diff(reference, candidate,
|
26
|
+
def self.diff(reference, candidate, flags = {})
|
14
27
|
reference = maybe_parse(reference)
|
15
28
|
candidate = maybe_parse(candidate)
|
16
|
-
Comparison.perform(reference, candidate,
|
29
|
+
Comparison.perform(reference, candidate, flags)
|
17
30
|
end
|
18
31
|
|
19
|
-
def self.render_diff(reference, candidate,
|
20
|
-
diffs = diff(reference, candidate,
|
21
|
-
Printers.render(diffs,
|
32
|
+
def self.render_diff(reference, candidate, flags = {})
|
33
|
+
diffs = diff(reference, candidate, flags)
|
34
|
+
Printers.render(diffs, flags)
|
22
35
|
end
|
23
36
|
|
24
37
|
class << self
|
25
38
|
private
|
26
39
|
|
27
40
|
# Maybe you gave us JSON strings, maybe you gave us Ruby objects.
|
28
|
-
# We just don't know!
|
29
|
-
|
41
|
+
# Heck, maybe you even gave us some YAML! We just don't know!
|
42
|
+
# That's what makes it so exciting!
|
43
|
+
def maybe_parse(document)
|
30
44
|
|
31
|
-
case
|
32
|
-
when String ;
|
33
|
-
else ;
|
45
|
+
case document
|
46
|
+
when String ; return YAML.load(document) # don't worry, if this raises we'll assume you've already parsed it
|
47
|
+
else ; return document
|
34
48
|
end
|
35
49
|
|
36
|
-
rescue JSON::ParserError
|
37
|
-
return
|
50
|
+
rescue JSON::ParserError, Psych::SyntaxError
|
51
|
+
return document
|
38
52
|
end
|
39
53
|
end
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
Flags.define :format do |flag|
|
58
|
+
allowed_values = CheckPlease::Printers::FORMATS.sort
|
59
|
+
|
60
|
+
flag.coerce &:to_sym
|
61
|
+
flag.default = CheckPlease::Printers::DEFAULT_FORMAT
|
62
|
+
flag.validate { |flags, value| allowed_values.include?(value) }
|
63
|
+
|
64
|
+
flag.cli_long = "--format FORMAT"
|
65
|
+
flag.cli_short = "-f FORMAT"
|
66
|
+
flag.description = [
|
67
|
+
"Format in which to present diffs.",
|
68
|
+
" (Allowed values: [#{allowed_values.join(", ")}])",
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
Flags.define :max_diffs do |flag|
|
73
|
+
flag.coerce &:to_i
|
74
|
+
flag.validate { |flags, value| value.to_i > 0 }
|
75
|
+
|
76
|
+
flag.cli_long = "--max-diffs MAX_DIFFS"
|
77
|
+
flag.cli_short = "-n MAX_DIFFS"
|
78
|
+
flag.description = "Stop after encountering a specified number of diffs."
|
79
|
+
end
|
80
|
+
|
81
|
+
Flags.define :fail_fast do |flag|
|
82
|
+
flag.default = false
|
83
|
+
flag.coerce { |value| !!value }
|
84
|
+
|
85
|
+
flag.cli_long = "--fail-fast"
|
86
|
+
flag.description = [
|
87
|
+
"Stop after encountering the first diff.",
|
88
|
+
" (equivalent to '--max-diffs 1')",
|
89
|
+
]
|
90
|
+
end
|
91
|
+
|
92
|
+
Flags.define :max_depth do |flag|
|
93
|
+
flag.coerce &:to_i
|
94
|
+
flag.validate { |flags, value| value.to_i > 0 }
|
95
|
+
|
96
|
+
flag.cli_long = "--max_depth MAX_DEPTH"
|
97
|
+
flag.cli_short = "-d MAX_DEPTH"
|
98
|
+
flag.description = [
|
99
|
+
"Limit the number of levels to descend when comparing documents.",
|
100
|
+
" (NOTE: root has depth = 1)",
|
101
|
+
]
|
102
|
+
end
|
103
|
+
|
104
|
+
Flags.define :select_paths do |flag|
|
105
|
+
flag.reentrant
|
106
|
+
flag.mutually_exclusive_to :reject_paths
|
107
|
+
|
108
|
+
flag.cli_short = "-s PATH_EXPR"
|
109
|
+
flag.cli_long = "--select-paths PATH_EXPR"
|
110
|
+
flag.description = [
|
111
|
+
"ONLY record diffs matching the provided PATH expression.",
|
112
|
+
" May be repeated; values will be treated as an 'OR' list.",
|
113
|
+
" Can't be combined with --reject-paths.",
|
114
|
+
]
|
115
|
+
end
|
116
|
+
|
117
|
+
Flags.define :reject_paths do |flag|
|
118
|
+
flag.reentrant
|
119
|
+
flag.mutually_exclusive_to :select_paths
|
120
|
+
|
121
|
+
flag.cli_short = "-r PATH_EXPR"
|
122
|
+
flag.cli_long = "--reject-paths PATH_EXPR"
|
123
|
+
flag.description = [
|
124
|
+
"DON'T record diffs matching the provided PATH expression.",
|
125
|
+
" May be repeated; values will be treated as an 'OR' list.",
|
126
|
+
" Can't be combined with --select-paths.",
|
127
|
+
]
|
128
|
+
end
|
129
|
+
|
40
130
|
end
|
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,7 +41,7 @@ module CheckPlease
|
|
38
41
|
}
|
39
42
|
end
|
40
43
|
|
41
|
-
def compare_arrays(ref_array, can_array, path
|
44
|
+
def compare_arrays(ref_array, can_array, path)
|
42
45
|
max_len = [ ref_array, can_array ].map(&:length).max
|
43
46
|
(0...max_len).each do |i|
|
44
47
|
n = i + 1 # count in human pls
|
@@ -48,44 +51,50 @@ module CheckPlease
|
|
48
51
|
can = can_array[i]
|
49
52
|
|
50
53
|
case
|
51
|
-
when ref_array.length < n ;
|
52
|
-
when can_array.length < n ;
|
53
|
-
else
|
54
|
+
when ref_array.length < n ; record_diff ref, can, new_path, :extra
|
55
|
+
when can_array.length < n ; record_diff ref, can, new_path, :missing
|
56
|
+
else
|
57
|
+
compare ref, can, new_path
|
54
58
|
end
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
58
|
-
def compare_hashes(ref_hash, can_hash, path
|
59
|
-
record_missing_keys ref_hash, can_hash, path
|
60
|
-
compare_common_keys ref_hash, can_hash, path
|
61
|
-
record_extra_keys ref_hash, can_hash, path
|
62
|
+
def compare_hashes(ref_hash, can_hash, path)
|
63
|
+
record_missing_keys ref_hash, can_hash, path
|
64
|
+
compare_common_keys ref_hash, can_hash, path
|
65
|
+
record_extra_keys ref_hash, can_hash, path
|
62
66
|
end
|
63
67
|
|
64
|
-
def record_missing_keys(ref_hash, can_hash, path
|
68
|
+
def record_missing_keys(ref_hash, can_hash, path)
|
65
69
|
keys = ref_hash.keys - can_hash.keys
|
66
70
|
keys.each do |k|
|
67
|
-
|
71
|
+
record_diff ref_hash[k], nil, path + k, :missing
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
71
|
-
def compare_common_keys(ref_hash, can_hash, path
|
75
|
+
def compare_common_keys(ref_hash, can_hash, path)
|
72
76
|
keys = ref_hash.keys & can_hash.keys
|
73
77
|
keys.each do |k|
|
74
|
-
compare ref_hash[k], can_hash[k], path + k
|
78
|
+
compare ref_hash[k], can_hash[k], path + k
|
75
79
|
end
|
76
80
|
end
|
77
81
|
|
78
|
-
def record_extra_keys(ref_hash, can_hash, path
|
82
|
+
def record_extra_keys(ref_hash, can_hash, path)
|
79
83
|
keys = can_hash.keys - ref_hash.keys
|
80
84
|
keys.each do |k|
|
81
|
-
|
85
|
+
record_diff nil, can_hash[k], path + k, :extra
|
82
86
|
end
|
83
87
|
end
|
84
88
|
|
85
|
-
def compare_others(ref, can, path
|
89
|
+
def compare_others(ref, can, path)
|
86
90
|
return if ref == can
|
87
|
-
|
91
|
+
record_diff ref, can, path, :mismatch
|
88
92
|
end
|
93
|
+
|
94
|
+
def record_diff(ref, can, path, type)
|
95
|
+
diff = Diff.new(type, path, ref, can)
|
96
|
+
diffs << diff
|
97
|
+
end
|
89
98
|
end
|
90
99
|
|
91
100
|
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,10 +43,6 @@ 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
|
data/lib/check_please/error.rb
CHANGED
@@ -0,0 +1,78 @@
|
|
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 mutually_exclusive_to(flag_name)
|
33
|
+
@validators << ->(flags, _) { flags.send(flag_name).empty? }
|
34
|
+
end
|
35
|
+
|
36
|
+
def reentrant
|
37
|
+
@reentrant = true
|
38
|
+
self.default_proc = ->{ Array.new }
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate(&block)
|
42
|
+
@validators << block
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def __set__(value, on:, flags:)
|
48
|
+
val = _coerce(value)
|
49
|
+
_validate(flags, val)
|
50
|
+
if @reentrant
|
51
|
+
on[name] ||= []
|
52
|
+
on[name].concat(Array(val))
|
53
|
+
else
|
54
|
+
on[name] = val
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def _coerce(value)
|
61
|
+
return value if @coercer.nil?
|
62
|
+
@coercer.call(value)
|
63
|
+
end
|
64
|
+
|
65
|
+
def _validate(flags, value)
|
66
|
+
return if @validators.empty?
|
67
|
+
return if @validators.all? { |block| block.call(flags, value) }
|
68
|
+
raise InvalidFlag, "#{value.inspect} is not a legal value for #{name}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_attribute!(name, value)
|
72
|
+
self.send "#{name}=", value
|
73
|
+
rescue NoMethodError
|
74
|
+
raise ArgumentError, "unrecognized attribute: #{name}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
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
@@ -3,8 +3,15 @@ module CheckPlease
|
|
3
3
|
class Path
|
4
4
|
SEPARATOR = "/"
|
5
5
|
|
6
|
+
def self.root
|
7
|
+
new
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :to_s
|
6
11
|
def initialize(segments = [])
|
7
12
|
@segments = Array(segments)
|
13
|
+
@to_s = SEPARATOR + @segments.join(SEPARATOR)
|
14
|
+
freeze
|
8
15
|
end
|
9
16
|
|
10
17
|
def +(new_basename)
|
@@ -15,12 +22,43 @@ module CheckPlease
|
|
15
22
|
1 + @segments.length
|
16
23
|
end
|
17
24
|
|
18
|
-
def
|
19
|
-
|
25
|
+
def excluded?(flags)
|
26
|
+
return false if root?
|
27
|
+
|
28
|
+
return true if too_deep?(flags)
|
29
|
+
return true if explicitly_excluded?(flags)
|
30
|
+
return true if implicitly_excluded?(flags)
|
31
|
+
|
32
|
+
false
|
20
33
|
end
|
21
34
|
|
22
35
|
def inspect
|
23
|
-
to_s
|
36
|
+
"<CheckPlease::Path '#{to_s}'>"
|
37
|
+
end
|
38
|
+
|
39
|
+
def root?
|
40
|
+
to_s == SEPARATOR
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def explicitly_excluded?(flags)
|
46
|
+
flags.reject_paths.any?( &method(:match?) )
|
47
|
+
end
|
48
|
+
|
49
|
+
def implicitly_excluded?(flags)
|
50
|
+
return false if flags.select_paths.empty?
|
51
|
+
flags.select_paths.none?( &method(:match?) )
|
52
|
+
end
|
53
|
+
|
54
|
+
# leaving this here for a while in case it needs to grow into a public method
|
55
|
+
def match?(path_expr)
|
56
|
+
to_s.include?(path_expr)
|
57
|
+
end
|
58
|
+
|
59
|
+
def too_deep?(flags)
|
60
|
+
return false if flags.max_depth.nil?
|
61
|
+
flags.max_depth + 1 < depth
|
24
62
|
end
|
25
63
|
end
|
26
64
|
|
@@ -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,21 +4,30 @@ 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
|
15
23
|
return "" if diffs.empty?
|
16
24
|
|
17
|
-
build_string do |io|
|
25
|
+
out = build_string do |io|
|
18
26
|
switch_tableprint_io(io) do
|
19
27
|
tp diffs.data, *TP_OPTS
|
20
28
|
end
|
21
29
|
end
|
30
|
+
strip_trailing_whitespace(out)
|
22
31
|
end
|
23
32
|
|
24
33
|
private
|
@@ -31,6 +40,10 @@ module Printers
|
|
31
40
|
ensure
|
32
41
|
config.io = @old_io
|
33
42
|
end
|
43
|
+
|
44
|
+
def strip_trailing_whitespace(s)
|
45
|
+
s.lines.map(&:rstrip).join("\n")
|
46
|
+
end
|
34
47
|
end
|
35
48
|
|
36
49
|
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
|
data/lib/check_please/version.rb
CHANGED
data/usage_examples.rb
CHANGED
@@ -3,6 +3,10 @@ require 'check_please'
|
|
3
3
|
reference = { foo: "wibble" }
|
4
4
|
candidate = { bar: "wibble" }
|
5
5
|
|
6
|
+
|
7
|
+
|
8
|
+
##### Printing diffs #####
|
9
|
+
|
6
10
|
puts CheckPlease.render_diff(reference, candidate)
|
7
11
|
|
8
12
|
# this should print the following to stdout:
|
@@ -12,3 +16,15 @@ _ = <<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` !
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: check_please
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Livingston-Gray
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-11-
|
11
|
+
date: 2020-11-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: table_print
|
@@ -56,7 +56,8 @@ description: Check for differences between two JSON strings (or Ruby data struct
|
|
56
56
|
parsed from them)
|
57
57
|
email:
|
58
58
|
- geeksam@gmail.com
|
59
|
-
executables:
|
59
|
+
executables:
|
60
|
+
- check_please
|
60
61
|
extensions: []
|
61
62
|
extra_rdoc_files: []
|
62
63
|
files:
|
@@ -69,24 +70,26 @@ files:
|
|
69
70
|
- LICENSE.txt
|
70
71
|
- README.md
|
71
72
|
- Rakefile
|
72
|
-
- bin/check_please
|
73
73
|
- bin/console
|
74
74
|
- bin/setup
|
75
75
|
- check_please.gemspec
|
76
|
+
- exe/check_please
|
76
77
|
- lib/check_please.rb
|
77
78
|
- lib/check_please/cli.rb
|
78
|
-
- lib/check_please/cli/flag.rb
|
79
79
|
- lib/check_please/cli/parser.rb
|
80
80
|
- lib/check_please/cli/runner.rb
|
81
81
|
- lib/check_please/comparison.rb
|
82
82
|
- lib/check_please/diff.rb
|
83
83
|
- lib/check_please/diffs.rb
|
84
84
|
- lib/check_please/error.rb
|
85
|
+
- lib/check_please/flag.rb
|
86
|
+
- lib/check_please/flags.rb
|
85
87
|
- lib/check_please/path.rb
|
86
88
|
- lib/check_please/printers.rb
|
87
89
|
- lib/check_please/printers/base.rb
|
88
90
|
- lib/check_please/printers/json.rb
|
89
91
|
- lib/check_please/printers/table_print.rb
|
92
|
+
- lib/check_please/refinements.rb
|
90
93
|
- lib/check_please/version.rb
|
91
94
|
- usage_examples.rb
|
92
95
|
homepage: https://github.com/RealGeeks/check_please
|
@@ -1,40 +0,0 @@
|
|
1
|
-
module CheckPlease
|
2
|
-
module CLI
|
3
|
-
|
4
|
-
class Flag
|
5
|
-
ATTR_NAMES = %i[ short long desc key block ]
|
6
|
-
attr_accessor(*ATTR_NAMES)
|
7
|
-
|
8
|
-
def initialize(*args)
|
9
|
-
self.short = args.shift if args.any?
|
10
|
-
self.long = args.shift if args.any?
|
11
|
-
|
12
|
-
yield self if block_given?
|
13
|
-
|
14
|
-
missing = ATTR_NAMES.select { |e| self.send(e).nil? }
|
15
|
-
missing -= %i[ short ] # short is optional!
|
16
|
-
if missing.any?
|
17
|
-
raise ArgumentError, "Missing attributes: #{missing.join(', ')}"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def visit_option_parser(parser, options)
|
22
|
-
parser.on(short, long, desc) do |value|
|
23
|
-
block.call options, value
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def set_key(key, message = nil, &b)
|
28
|
-
raise ArgumentError if message && b
|
29
|
-
raise ArgumentError if !message && !b
|
30
|
-
|
31
|
-
self.key = key
|
32
|
-
self.block = ->(options, value) {
|
33
|
-
b ||= message.to_sym.to_proc
|
34
|
-
options[key] = b.call(value)
|
35
|
-
}
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|
40
|
-
end
|