check_please 0.1.0 → 0.2.4
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 +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
|