check_please 0.2.2 → 0.4.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/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
|