check_please 0.1.0 → 0.2.4
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 +132 -13
- data/Rakefile +16 -1
- data/exe/check_please +4 -0
- data/lib/check_please.rb +19 -13
- data/lib/check_please/cli.rb +45 -0
- data/lib/check_please/cli/flag.rb +40 -0
- data/lib/check_please/cli/parser.rb +58 -0
- data/lib/check_please/cli/runner.rb +79 -0
- data/lib/check_please/comparison.rb +9 -3
- data/lib/check_please/diffs.rb +8 -1
- data/lib/check_please/error.rb +9 -0
- data/lib/check_please/path.rb +4 -0
- data/lib/check_please/printers.rb +2 -2
- data/lib/check_please/printers/json.rb +0 -2
- data/lib/check_please/printers/table_print.rb +6 -1
- data/lib/check_please/version.rb +3 -1
- metadata +10 -4
- data/bin/check_please +0 -70
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4433e70c9c7da92bbc95c1bfb2f22a947be4e3d30c9fa5b1d735826cc45dde1
|
4
|
+
data.tar.gz: 249804476d1b19c1f9add240b20f5e05b9258d4a70f992b5bc8a3654339e7f5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be7a4320ecb7bf5836ed62ed629e3445b20e2e847e497847a5cdbab856694b46b7f039e8c423622b0b35f5d098398c1f060655e9f4f11aacc2b15adeeb1dda61
|
7
|
+
data.tar.gz: e15695cfa60a8c2cf29b54b32c33ce76a5758fe76cd8ac92cb90b362d1597060de469b99c4a2e182a5b35b28b4d1757508b6e8b058a6371fd7455ad2a30d7ae3
|
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,29 +19,147 @@ Or install it yourself as:
|
|
18
19
|
|
19
20
|
$ gem install check_please
|
20
21
|
|
22
|
+
## Terminology
|
23
|
+
|
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
|
+
|
27
|
+
* **Reference** is always used to refer to the "target" or "source of truth."
|
28
|
+
We assume you're comparing two things because you want one of them to be like
|
29
|
+
the other; the **reference** is what you're aiming for.
|
30
|
+
* **Candidate** is always used to refer to some JSON you'd like to compare
|
31
|
+
against the **reference**. _(We could've also used "sample," but it turns
|
32
|
+
out that "reference" and "candidate" are the same length, which makes code
|
33
|
+
line up neatly in a monospaced font...)_
|
34
|
+
* A **diff** is what CheckPlease calls an individual discrepancy between the
|
35
|
+
**reference** and the **candidate**. More on this in "Understanding the Output",
|
36
|
+
below.
|
37
|
+
|
21
38
|
## Usage
|
22
39
|
|
23
|
-
###
|
40
|
+
### From the Terminal
|
24
41
|
|
25
42
|
Use the `bin/check_please` executable. (To get started, run it with the '-h' flag.)
|
26
43
|
|
27
|
-
|
44
|
+
Note that the executable assumes you've saved your **reference** to a file.
|
45
|
+
Once that's done, you can either save the **candidate** to a file as well if
|
46
|
+
that fits your workflow, **or** you can pipe it to `bin/check_please` in lieu
|
47
|
+
of giving it a second filename as the argument. (This is especially useful if
|
48
|
+
you're copying an XHR response out of a web browser's dev tools and have a tool
|
49
|
+
like MacOS's `pbpaste` utility.)
|
50
|
+
|
51
|
+
### From RSpec
|
52
|
+
|
53
|
+
See [check_please_rspec_matcher](https://github.com/RealGeeks/check_please_rspec_matcher).
|
54
|
+
|
55
|
+
### From Ruby
|
56
|
+
|
57
|
+
Create two strings, each containing a JSON or YAML document, and pass them to
|
58
|
+
`CheckPlease.render_diff`. You'll get back a third string containing a report
|
59
|
+
of all the differences CheckPlease found in the two JSON strings. (See also:
|
60
|
+
[./usage_examples.rb](usage_examples.rb).)
|
61
|
+
|
62
|
+
### Understanding the Output
|
63
|
+
|
64
|
+
CheckPlease follows the Unix philosophy of "no news is good news". If your
|
65
|
+
**candidate** matches your **reference**, you'll get an empty message.
|
66
|
+
|
67
|
+
But let's be honest: how often is that going to happen? No, you're using this
|
68
|
+
tool because you want a human-friendly summary of all the places that your
|
69
|
+
**candidate** fell short.
|
70
|
+
|
71
|
+
When CheckPlease compares your two samples, it generates a list of diffs to
|
72
|
+
describe any discrepancies it encounters.
|
73
|
+
|
74
|
+
An example would probably help here.
|
75
|
+
|
76
|
+
_(NOTE: these examples may fall out of date with the code. They're swiped
|
77
|
+
from [the CLI integration spec](spec/cli_integration_spec.rb), so please
|
78
|
+
consider that more authoritative than this README. If you do spot a
|
79
|
+
difference, please feel free to open an issue!)_
|
80
|
+
|
81
|
+
Given the following **reference** JSON:
|
82
|
+
```
|
83
|
+
{
|
84
|
+
"id": 42,
|
85
|
+
"name": "The Answer",
|
86
|
+
"words": [ "what", "do", "you", "get", "when", "you", "multiply", "six", "by", "nine" ],
|
87
|
+
"meta": { "foo": "spam", "bar": "eggs", "yak": "bacon" }
|
88
|
+
}
|
89
|
+
```
|
90
|
+
|
91
|
+
And the following **candidate** JSON:
|
92
|
+
```
|
93
|
+
{
|
94
|
+
"id": 42,
|
95
|
+
"name": [ "I am large, and contain multitudes." ],
|
96
|
+
"words": [ "what", "do", "we", "get", "when", "I", "multiply", "six", "by", "nine", "dude" ],
|
97
|
+
"meta": { "foo": "foo", "yak": "bacon" }
|
98
|
+
}
|
99
|
+
```
|
100
|
+
|
101
|
+
CheckPlease should produce the following output:
|
102
|
+
|
103
|
+
```
|
104
|
+
TYPE | PATH | REFERENCE | CANDIDATE
|
105
|
+
--------------|-----------|------------|-------------------------------
|
106
|
+
type_mismatch | /name | The Answer | ["I am large, and contain m...
|
107
|
+
mismatch | /words/3 | you | we
|
108
|
+
mismatch | /words/6 | you | I
|
109
|
+
extra | /words/11 | | dude
|
110
|
+
missing | /meta/bar | eggs |
|
111
|
+
mismatch | /meta/foo | spam | foo
|
112
|
+
```
|
113
|
+
|
114
|
+
Let's start with the leftmost column...
|
115
|
+
|
116
|
+
#### Diff Types
|
117
|
+
|
118
|
+
The above example is intended to illustrate every possible type of diff that
|
119
|
+
CheckPlease defines:
|
120
|
+
|
121
|
+
* **type_mismatch** means that both the **reference** and the **candidate** had
|
122
|
+
a value at the given path, but one value was an Array or a Hash and the other
|
123
|
+
was not. **When CheckPlease encounters a type mismatch, it does not compare
|
124
|
+
anything "below" the given path.** _(Technical note: CheckPlease uses a
|
125
|
+
"recursive descent" strategy to traverse the **reference** data structure,
|
126
|
+
and it stops when it encounters a type mismatch in order to avoid producing a
|
127
|
+
lot of "garbage" diff output.)_
|
128
|
+
* **mismatch** means that both the **reference** and the **candidate** had a
|
129
|
+
value at the given path, and neither value was an Array or a Hash.
|
130
|
+
* **extra** means that, inside an Array or a Hash, the **candidate** contained
|
131
|
+
values that were not found in the **reference**.
|
132
|
+
* **missing** is the opposite of **extra**: inside an Array or a Hash, the
|
133
|
+
**reference** contained values that were not found in the **candidate**.
|
134
|
+
|
135
|
+
#### Paths
|
136
|
+
|
137
|
+
The second column contains a path expression. This is extremely lo-fi:
|
138
|
+
|
139
|
+
* The root of the data structure is defined as "/".
|
140
|
+
* If an element in the data structure is an array, its child elements will have
|
141
|
+
a **one-based** index appended to their parent's path.
|
142
|
+
* If an element in the data structure is an object ("Hash" in Ruby), the key
|
143
|
+
for each element will be appended to their parent's path, and the values will
|
144
|
+
be compared.
|
145
|
+
|
146
|
+
_**Being primarily a Ruby developer, I'm quite ignorant of conventions in the
|
147
|
+
JS community; if there's an existing convention for paths, please open an
|
148
|
+
issue!**_
|
149
|
+
|
150
|
+
#### Output Formats
|
28
151
|
|
29
|
-
|
30
|
-
|
31
|
-
CheckPlease found in the two JSON strings. (See also: ./usage_examples.rb.)
|
152
|
+
CheckPlease produces tabular output by default. (It leans heavily on the
|
153
|
+
amazing [table_print](http://tableprintgem.com) gem for this.)
|
32
154
|
|
33
|
-
|
34
|
-
|
35
|
-
|
155
|
+
If you want to incorporate CheckPlease into some other toolchain, it can also
|
156
|
+
print diffs as JSON to facilitate parsing. In Ruby, pass `format: :json` to
|
157
|
+
`CheckPlease.render_diff`; in the CLI, use the `-f`/`--format` switch.
|
36
158
|
|
37
159
|
## TODO
|
38
160
|
|
39
|
-
* rspec custom matcher (separate gem?)
|
40
161
|
* command line flags for :allthethings:!
|
41
|
-
* limit to first N
|
42
162
|
* sort by path?
|
43
|
-
* max depth (for iterative refinement?)
|
44
163
|
* detect timestamps and compare after parsing?
|
45
164
|
* ignore sub-second precision (option / CLI flag)?
|
46
165
|
* possibly support plugins for other folks to add custom coercions?
|
data/Rakefile
CHANGED
@@ -1,6 +1,21 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rspec/core/rake_task"
|
3
3
|
|
4
|
-
|
4
|
+
namespace :spec do
|
5
|
+
RSpec::Core::RakeTask.new(:all)
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:not_cli) do |t|
|
8
|
+
t.rspec_opts = "--tag ~cli"
|
9
|
+
end
|
10
|
+
task fast: :not_cli
|
11
|
+
|
12
|
+
# These are much slower than the rest, since they use Kernel#`
|
13
|
+
RSpec::Core::RakeTask.new(:cli) do |t|
|
14
|
+
t.rspec_opts = "--tag cli"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# By default, `rake spec` should run fast specs first, then cli if those all pass
|
19
|
+
task :spec => [ "spec:not_cli", "spec:cli" ]
|
5
20
|
|
6
21
|
task :default => :spec
|
data/exe/check_please
ADDED
data/lib/check_please.rb
CHANGED
@@ -1,38 +1,44 @@
|
|
1
1
|
require_relative "check_please/version"
|
2
|
+
require_relative "check_please/error"
|
2
3
|
require_relative "check_please/path"
|
3
4
|
require_relative "check_please/comparison"
|
4
5
|
require_relative "check_please/diff"
|
5
6
|
require_relative "check_please/diffs"
|
6
7
|
require_relative "check_please/printers"
|
8
|
+
require_relative "check_please/cli"
|
9
|
+
|
10
|
+
require 'yaml'
|
11
|
+
require 'json'
|
7
12
|
|
8
13
|
module CheckPlease
|
9
|
-
|
14
|
+
ELEVATOR_PITCH = "Tool for parsing and diffing two JSON documents."
|
10
15
|
|
11
|
-
def self.diff(reference, candidate)
|
16
|
+
def self.diff(reference, candidate, options = {})
|
12
17
|
reference = maybe_parse(reference)
|
13
18
|
candidate = maybe_parse(candidate)
|
14
|
-
Comparison.perform(reference, candidate)
|
19
|
+
Comparison.perform(reference, candidate, options)
|
15
20
|
end
|
16
21
|
|
17
|
-
def self.render_diff(reference, candidate,
|
18
|
-
diffs = diff(reference, candidate)
|
19
|
-
Printers.render(diffs,
|
22
|
+
def self.render_diff(reference, candidate, options = {})
|
23
|
+
diffs = diff(reference, candidate, options)
|
24
|
+
Printers.render(diffs, options)
|
20
25
|
end
|
21
26
|
|
22
27
|
class << self
|
23
28
|
private
|
24
29
|
|
25
30
|
# Maybe you gave us JSON strings, maybe you gave us Ruby objects.
|
26
|
-
# We just don't know!
|
27
|
-
|
31
|
+
# Heck, maybe you even gave us some YAML! We just don't know!
|
32
|
+
# That's what makes it so exciting!
|
33
|
+
def maybe_parse(document)
|
28
34
|
|
29
|
-
case
|
30
|
-
when String ;
|
31
|
-
else ;
|
35
|
+
case document
|
36
|
+
when String ; return YAML.load(document) # don't worry, if this raises we'll assume you've already parsed it
|
37
|
+
else ; return document
|
32
38
|
end
|
33
39
|
|
34
|
-
rescue JSON::ParserError
|
35
|
-
return
|
40
|
+
rescue JSON::ParserError, Psych::SyntaxError
|
41
|
+
return document
|
36
42
|
end
|
37
43
|
end
|
38
44
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative 'cli/flag'
|
2
|
+
# require_relative 'cli/flags'
|
3
|
+
require_relative 'cli/parser'
|
4
|
+
require_relative 'cli/runner'
|
5
|
+
|
6
|
+
module CheckPlease
|
7
|
+
|
8
|
+
module CLI
|
9
|
+
def self.run(exe_file_name)
|
10
|
+
Runner.new(__FILE__).run(*ARGV.dup)
|
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 #####
|
22
|
+
|
23
|
+
flag short: "-f FORMAT", long: "--format FORMAT" do |f|
|
24
|
+
f.desc = "format in which to present diffs (available options: [#{CheckPlease::Printers::FORMATS.join(", ")}])"
|
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
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module CheckPlease
|
4
|
+
module CLI
|
5
|
+
|
6
|
+
class Parser
|
7
|
+
class UnrecognizedOption < StandardError
|
8
|
+
include CheckPlease::Error
|
9
|
+
end
|
10
|
+
|
11
|
+
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
|
20
|
+
end
|
21
|
+
|
22
|
+
# Unfortunately, OptionParser *really* wants to use closures.
|
23
|
+
# I haven't yet figured out how to get around this...
|
24
|
+
def consume_flags!(args)
|
25
|
+
@optparse.parse!(args) # removes recognized flags from `args`
|
26
|
+
return @options
|
27
|
+
rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
|
28
|
+
raise UnrecognizedOption, e.message, cause: e
|
29
|
+
end
|
30
|
+
|
31
|
+
def help
|
32
|
+
@optparse.help
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def banner
|
38
|
+
<<~EOF
|
39
|
+
Usage: #{@exe_file_name} <reference> <candidate> [FLAGS]
|
40
|
+
|
41
|
+
#{CheckPlease::ELEVATOR_PITCH}
|
42
|
+
|
43
|
+
Arguments:
|
44
|
+
<reference> is the name of a file to use as, well, the reference.
|
45
|
+
<candidate> is the name of a file to compare against the reference.
|
46
|
+
|
47
|
+
NOTE: If you have a utility like MacOS's `pbpaste`, you MAY omit
|
48
|
+
the <candidate> arg, and pipe the second document instead, like:
|
49
|
+
|
50
|
+
$ pbpaste | #{@exe_file_name} <reference>
|
51
|
+
|
52
|
+
FLAGS:
|
53
|
+
EOF
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module CheckPlease
|
2
|
+
module CLI
|
3
|
+
|
4
|
+
class Runner
|
5
|
+
def initialize(exe_file_name)
|
6
|
+
@parser = Parser.new(exe_file_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
# NOTE: unusually for me, I'm using Ruby's `or` keyword in this method.
|
10
|
+
# `or` short circuits just like `||`, but has lower precedence, which
|
11
|
+
# enables some shenanigans...
|
12
|
+
def run(*args)
|
13
|
+
args.flatten!
|
14
|
+
print_help_and_exit if args.empty?
|
15
|
+
|
16
|
+
begin
|
17
|
+
options = @parser.consume_flags!(args)
|
18
|
+
rescue Parser::UnrecognizedOption => e
|
19
|
+
print_help_and_exit e.message
|
20
|
+
end
|
21
|
+
|
22
|
+
# The reference MUST be the first arg...
|
23
|
+
reference = \
|
24
|
+
read_file(args.shift) \
|
25
|
+
or print_help_and_exit "Missing <reference> argument"
|
26
|
+
|
27
|
+
# The candidate MAY be the second arg, or it might have been piped in...
|
28
|
+
candidate = \
|
29
|
+
read_file(args.shift) \
|
30
|
+
|| read_piped_stdin \
|
31
|
+
or print_help_and_exit "Missing <candidate> argument, AND nothing was piped in"
|
32
|
+
|
33
|
+
# Looks like we're good to go!
|
34
|
+
diff_view = CheckPlease.render_diff(reference, candidate, options)
|
35
|
+
puts diff_view
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def print_help_and_exit(message = nil)
|
43
|
+
puts "\n>>> #{message}\n\n" if message
|
44
|
+
puts @parser.help
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_file(filename)
|
49
|
+
return nil if filename.nil?
|
50
|
+
File.read(filename)
|
51
|
+
rescue Errno::ENOENT
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# Unfortunately, ARGF won't help us here because it doesn't seem to want to
|
56
|
+
# read from stdin after it's already pulled a file out of ARGV. So, we
|
57
|
+
# have to read from stdin ourselves.
|
58
|
+
#
|
59
|
+
# BUT THAT'S NOT ALL! If the user didn't actually pipe any data,
|
60
|
+
# $stdin.read will block until they manually send EOF or hit Ctrl+C.
|
61
|
+
#
|
62
|
+
# Fortunately, we can detect whether $stdin.read will block by checking to
|
63
|
+
# see if it is a TTY. (Wait, what century is this again?)
|
64
|
+
#
|
65
|
+
# For fun and posterity, here's an experiment you can use to demonstrate this:
|
66
|
+
#
|
67
|
+
# $ ruby -e 'puts $stdin.tty? ? "YES YOU ARE A TTY" : "nope, no tty here"'
|
68
|
+
# YES YOU ARE A TTY
|
69
|
+
#
|
70
|
+
# $ cat foo | ruby -e 'puts $stdin.tty? ? "YES YOU ARE A TTY" : "nope, no tty here"'
|
71
|
+
# nope, no tty here
|
72
|
+
def read_piped_stdin
|
73
|
+
return nil if $stdin.tty?
|
74
|
+
return $stdin.read
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -3,16 +3,22 @@ module CheckPlease
|
|
3
3
|
module Comparison
|
4
4
|
extend self
|
5
5
|
|
6
|
-
def perform(reference, candidate)
|
6
|
+
def perform(reference, candidate, options = {})
|
7
7
|
root = CheckPlease::Path.new
|
8
|
-
diffs = Diffs.new
|
9
|
-
|
8
|
+
diffs = Diffs.new(options: options)
|
9
|
+
catch(:max_diffs_reached) do
|
10
|
+
compare reference, candidate, root, diffs
|
11
|
+
end
|
10
12
|
diffs
|
11
13
|
end
|
12
14
|
|
13
15
|
private
|
14
16
|
|
15
17
|
def compare(ref, can, path, diffs)
|
18
|
+
if (d = diffs.options[:max_depth])
|
19
|
+
return if path.depth > d + 1
|
20
|
+
end
|
21
|
+
|
16
22
|
case types(ref, can)
|
17
23
|
when [ :array, :array ] ; compare_arrays ref, can, path, diffs
|
18
24
|
when [ :hash, :hash ] ; compare_hashes ref, can, path, diffs
|
data/lib/check_please/diffs.rb
CHANGED
@@ -5,7 +5,9 @@ module CheckPlease
|
|
5
5
|
# Custom collection class for Diff instances.
|
6
6
|
# Can retrieve members using indexes or paths.
|
7
7
|
class Diffs
|
8
|
-
|
8
|
+
attr_reader :options
|
9
|
+
def initialize(diff_list = nil, options: {})
|
10
|
+
@options = options
|
9
11
|
@list = []
|
10
12
|
@hash = {}
|
11
13
|
Array(diff_list).each do |diff|
|
@@ -27,6 +29,11 @@ module CheckPlease
|
|
27
29
|
end
|
28
30
|
|
29
31
|
def <<(diff)
|
32
|
+
if (n = options[:max_diffs])
|
33
|
+
# It seems no one can help me now / I'm in too deep, there's no way out
|
34
|
+
throw :max_diffs_reached if length >= n
|
35
|
+
end
|
36
|
+
|
30
37
|
@list << diff
|
31
38
|
@hash[diff.path] = diff
|
32
39
|
end
|
data/lib/check_please/path.rb
CHANGED
@@ -12,8 +12,8 @@ module CheckPlease
|
|
12
12
|
FORMATS = PRINTERS_BY_FORMAT.keys.sort
|
13
13
|
DEFAULT_FORMAT = :table
|
14
14
|
|
15
|
-
def self.render(diffs,
|
16
|
-
format
|
15
|
+
def self.render(diffs, options = {})
|
16
|
+
format = options[:format] || DEFAULT_FORMAT
|
17
17
|
printer = PRINTERS_BY_FORMAT[format.to_sym]
|
18
18
|
printer.render(diffs)
|
19
19
|
end
|
@@ -14,11 +14,12 @@ module Printers
|
|
14
14
|
def to_s
|
15
15
|
return "" if diffs.empty?
|
16
16
|
|
17
|
-
build_string do |io|
|
17
|
+
out = build_string do |io|
|
18
18
|
switch_tableprint_io(io) do
|
19
19
|
tp diffs.data, *TP_OPTS
|
20
20
|
end
|
21
21
|
end
|
22
|
+
strip_trailing_whitespace(out)
|
22
23
|
end
|
23
24
|
|
24
25
|
private
|
@@ -31,6 +32,10 @@ module Printers
|
|
31
32
|
ensure
|
32
33
|
config.io = @old_io
|
33
34
|
end
|
35
|
+
|
36
|
+
def strip_trailing_whitespace(s)
|
37
|
+
s.lines.map(&:rstrip).join("\n")
|
38
|
+
end
|
34
39
|
end
|
35
40
|
|
36
41
|
end
|
data/lib/check_please/version.rb
CHANGED
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.2.4
|
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-18 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,14 +70,19 @@ 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
|
78
|
+
- lib/check_please/cli.rb
|
79
|
+
- lib/check_please/cli/flag.rb
|
80
|
+
- lib/check_please/cli/parser.rb
|
81
|
+
- lib/check_please/cli/runner.rb
|
77
82
|
- lib/check_please/comparison.rb
|
78
83
|
- lib/check_please/diff.rb
|
79
84
|
- lib/check_please/diffs.rb
|
85
|
+
- lib/check_please/error.rb
|
80
86
|
- lib/check_please/path.rb
|
81
87
|
- lib/check_please/printers.rb
|
82
88
|
- lib/check_please/printers/base.rb
|
data/bin/check_please
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'optparse'
|
4
|
-
|
5
|
-
require_relative '../lib/check_please'
|
6
|
-
|
7
|
-
argv = ARGV.dup
|
8
|
-
|
9
|
-
ref_file = argv.shift
|
10
|
-
can_file = argv.shift
|
11
|
-
diff_opts = {}
|
12
|
-
|
13
|
-
@parser = OptionParser.new do |opts|
|
14
|
-
opts.banner = <<~EOF
|
15
|
-
Usage: #{__FILE__} <reference> <candidate> <options>
|
16
|
-
|
17
|
-
Tool for parsing and diffing two JSON files.
|
18
|
-
|
19
|
-
Arguments:
|
20
|
-
<reference> is the name of a file to use as the reference.
|
21
|
-
<candidate> is the name of a file to compare against the reference.
|
22
|
-
|
23
|
-
NOTE: If the <candidate> arg is omitted, stdin will be used instead.
|
24
|
-
This allows you to copy candidate JSON to the clipboard and (on a Mac) do:
|
25
|
-
|
26
|
-
$ pbpaste | #{__FILE__} <reference>
|
27
|
-
|
28
|
-
<options>:
|
29
|
-
EOF
|
30
|
-
|
31
|
-
formats = CheckPlease::Printers::FORMATS.join(", ")
|
32
|
-
|
33
|
-
opts.on("-f FORMAT", "--format FORMAT", "specify the format (available options: [#{formats}]") do |val|
|
34
|
-
diff_opts[:format] = val
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
def print_help_and_exit
|
41
|
-
@parser.parse(%w[--help])
|
42
|
-
exit # technically redundant but helps me feel better
|
43
|
-
end
|
44
|
-
|
45
|
-
def read_file(filename)
|
46
|
-
return nil if filename.to_s =~ /^\s*$/
|
47
|
-
File.read(filename)
|
48
|
-
rescue Errno::ENOENT
|
49
|
-
# no such file, buddy
|
50
|
-
return nil
|
51
|
-
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
# First off, try to read in the files the user told us about...
|
56
|
-
reference = read_file(ref_file)
|
57
|
-
candidate = read_file(can_file) || $stdin.read
|
58
|
-
|
59
|
-
print_help_and_exit if reference.to_s =~ /^\s*$/
|
60
|
-
print_help_and_exit if candidate.to_s =~ /^\s*$/
|
61
|
-
|
62
|
-
begin
|
63
|
-
@parser.parse(argv)
|
64
|
-
rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
|
65
|
-
puts "\n>>> #{e.message}\n\n"
|
66
|
-
print_help_and_exit
|
67
|
-
end
|
68
|
-
|
69
|
-
report = CheckPlease.render_diff(reference, candidate, **diff_opts)
|
70
|
-
puts report
|