check_please 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9e2ff8cbe19131bcf4995eb057ef566bc8f4f102e138d7db5dc908befa1fbcf
4
- data.tar.gz: 616388b3ee563b7f384b7fc19b051117c0f83bd00e47092f7f6f7b9ec712c81b
3
+ metadata.gz: 1c16110449be99d5ff509c720c4365b2fe6f176f23094784628db2de0e5f3bf9
4
+ data.tar.gz: b45c9d1b7dea9b4405f4ca80dbce5f04d0aec14ad50e4e919802e09b469c86c8
5
5
  SHA512:
6
- metadata.gz: f4b7bcef1fda2618631f59fe8ad17a2630ba33271a2c6e295b96f204076c29ba283b3bb19cfba1c8550cc03ecd709fd1aa9ecbf22db4846d3d1f7b302b7c4ee5
7
- data.tar.gz: 18be8f0813b99bb66930ceb5dc2b658b377522c84d77ed3361daaecfc1aaff08f439582bb0e63056e8f8617239379c25ed44b8a517d7d8eaf92834b616d77267
6
+ metadata.gz: 023b9fdd6f227f8381dfba06a82d30961adab9b2ab5a2b5a0d820c8b8697d300f59d416c801808e88e7e625720776f1cec4cf0a6f57d1eec07e6cb656881d4b8
7
+ data.tar.gz: eca955971798d00dd45bf5e068779aebeb33b304fe592d84534081d0068eaa981f6b765fd5c4ea9a30e7d65b2edd456ed9093c039964e3691c646e0c7d23b2b0
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- check_please (0.1.0)
4
+ check_please (0.2.0)
5
5
  table_print
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # CheckPlease
1
+ # check_please
2
2
 
3
3
  Check for differences between two JSON strings (or Ruby data structures parsed from them).
4
4
 
@@ -20,24 +20,150 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
+ ### Terminology
24
+
25
+ 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
+
23
38
  ### CLI
24
39
 
25
40
  Use the `bin/check_please` executable. (To get started, run it with the '-h' flag.)
26
41
 
42
+ Note that the executable assumes you've saved your **reference** to a file.
43
+ Once that's done, you can either save the **candidate** to a file as well if
44
+ that fits your workflow, **or** you can pipe it to `bin/check_please` in lieu
45
+ of giving it a second filename as the argument. (This is especially useful if
46
+ you're copying an XHR response out of a web browser's dev tools and have a tool
47
+ like MacOS's `pbpaste` utility.)
48
+
49
+ ### RSpec Matcher
50
+
51
+ See [check_please_rspec_matcher](https://github.com/RealGeeks/check_please_rspec_matcher).
52
+
27
53
  ### From Within Ruby
28
54
 
29
55
  Create two JSON strings and pass them to `CheckPlease.render_diff`. You'll get
30
56
  back a third string containing a nicely formatted report of all the differences
31
- CheckPlease found in the two JSON strings. (See also: ./usage_examples.rb.)
57
+ CheckPlease found in the two JSON strings. (See also: [./usage_examples.rb](usage_examples.rb).)
32
58
 
33
59
  (You can also parse the JSON strings yourself and pass the resulting data
34
60
  structures in, if you're into that. I mean, I wrote this to help compare JSON
35
61
  data that's too big and complicated to scan through visually, but you do you!
36
62
 
63
+ ### Understanding the Output
64
+
65
+ CheckPlease follows the Unix philosophy of "no news is good news". If your
66
+ **candidate** matches your **reference**, you'll get an empty message.
67
+
68
+ But let's be honest: how often is that going to happen? No, you're using this
69
+ tool because you want a human-friendly summary of all the places that your
70
+ **candidate** fell short.
71
+
72
+ When CheckPlease compares your two samples, it generates a list of diffs to
73
+ describe any discrepancies it encounters. (By default, it prints that list in a
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.)
76
+
77
+ An example would probably help here.
78
+
79
+ _(NOTE: these examples may fall out of date with the code. They're swiped
80
+ from [the CLI integration spec](spec/cli_integration_spec.rb), so please
81
+ consider that more authoritative than this README. If you do spot a
82
+ difference, please feel free to open an issue!)_
83
+
84
+ Given the following **reference** JSON:
85
+ ```
86
+ {
87
+ "id": 42,
88
+ "name": "The Answer",
89
+ "words": [ "what", "do", "you", "get", "when", "you", "multiply", "six", "by", "nine" ],
90
+ "meta": { "foo": "spam", "bar": "eggs", "yak": "bacon" }
91
+ }
92
+ ```
93
+
94
+ And the following **candidate** JSON:
95
+ ```
96
+ {
97
+ "id": 42,
98
+ "name": [ "I am large, and contain multitudes." ],
99
+ "words": [ "what", "do", "we", "get", "when", "I", "multiply", "six", "by", "nine", "dude" ],
100
+ "meta": { "foo": "foo", "yak": "bacon" }
101
+ }
102
+ ```
103
+
104
+ CheckPlease should produce the following output:
105
+
106
+ ```
107
+ TYPE | PATH | REFERENCE | CANDIDATE
108
+ --------------|-----------|------------|-------------------------------
109
+ type_mismatch | /name | The Answer | ["I am large, and contain m...
110
+ mismatch | /words/3 | you | we
111
+ mismatch | /words/6 | you | I
112
+ extra | /words/11 | | dude
113
+ missing | /meta/bar | eggs |
114
+ mismatch | /meta/foo | spam | foo
115
+ ```
116
+
117
+ Let's start with the leftmost column...
118
+
119
+ #### Diff Types
120
+
121
+ The above example is intended to illustrate every possible type of diff that
122
+ CheckPlease defines:
123
+
124
+ * **type_mismatch** means that both the **reference** and the **candidate** had
125
+ a value at the given path, but one value was an Array or a Hash and the other
126
+ was not. **When CheckPlease encounters a type mismatch, it does not compare
127
+ anything "below" the given path.** producing a lot of "garbage" diffs.
128
+ _(Technical note: CheckPlease uses a "recursive descent" strategy to
129
+ traverse the **reference** data structure, and it stops when it encounters a
130
+ type mismatch in order to avoid producing a lot of "garbage" diff output.
131
+ Also, the way these get displayed is likely to change.)_
132
+ * **mismatch** means that both the **reference** and the **candidate** had a
133
+ 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
+ contained values that were not found in the **reference**.
136
+ * "**missing**" is the opposite of **extra**: inside an Array or a Hash, the
137
+ **reference** contained values that were not found in the **candidate**.
138
+
139
+ #### Paths
140
+
141
+ The second column contains a path expression. This is extremely basic:
142
+
143
+ * The first element in the data structure is defined as "/".
144
+ * If an element in the data structure is an array, its child elements will have
145
+ a **one-based** index appended to their parent's path.
146
+ * If an element in the data structure is an object ("Hash" in Ruby), the key
147
+ for each element will be appended to their parent's path, and the values will
148
+ be compared.
149
+
150
+ _**Being primarily a Ruby developer, I'm quite ignorant of conventions in the
151
+ JS community; if there's an existing convention for paths, please open an
152
+ issue!**_
153
+
154
+ #### Output Formats
155
+
156
+ CheckPlease produces tabular output by default. (It leans heavily on the
157
+ amazing [table_print](http://tableprintgem.com) gem for this.)
158
+
159
+ If you want to incorporate CheckPlease into some other toolchain, it can also
160
+ print diffs as JSON to facilitate parsing. In Ruby, pass `format: :json` to
161
+ `CheckPlease.render_diff`; in the CLI, use the `-f`/`--format` switch.
162
+
37
163
  ## TODO
38
164
 
39
- * rspec custom matcher (separate gem?)
40
165
  * command line flags for :allthethings:!
166
+ * --fail-fast
41
167
  * limit to first N
42
168
  * sort by path?
43
169
  * max depth (for iterative refinement?)
@@ -1,70 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'optparse'
4
-
5
3
  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
4
+ CheckPlease::CLI.run(__FILE__)
@@ -1,12 +1,14 @@
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"
7
9
 
8
10
  module CheckPlease
9
- class Error < StandardError; end
11
+ ELEVATOR_PITCH = "Tool for parsing and diffing two JSON documents."
10
12
 
11
13
  def self.diff(reference, candidate)
12
14
  reference = maybe_parse(reference)
@@ -14,9 +16,9 @@ module CheckPlease
14
16
  Comparison.perform(reference, candidate)
15
17
  end
16
18
 
17
- def self.render_diff(reference, candidate, format: nil)
19
+ def self.render_diff(reference, candidate, options = {})
18
20
  diffs = diff(reference, candidate)
19
- Printers.render(diffs, format)
21
+ Printers.render(diffs, options)
20
22
  end
21
23
 
22
24
  class << self
@@ -0,0 +1,29 @@
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(*args, &block)
17
+ flag = Flag.new(*args, &block)
18
+ FLAGS << flag
19
+ end
20
+
21
+ ##### Define CLI flags here #####
22
+
23
+ flag "-f FORMAT", "--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
+ end
28
+
29
+ end
@@ -0,0 +1,39 @@
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
+ if missing.any?
16
+ raise ArgumentError, "Missing attributes: #{missing.join(', ')}"
17
+ end
18
+ end
19
+
20
+ def visit_option_parser(parser, options)
21
+ parser.on(short, long, desc) do |value|
22
+ block.call options, value
23
+ end
24
+ end
25
+
26
+ def set_key(key, message = nil, &b)
27
+ raise ArgumentError if message && b
28
+ raise ArgumentError if !message && !b
29
+
30
+ self.key = key
31
+ self.block = ->(options, value) {
32
+ b ||= message.to_sym.to_proc
33
+ options[key] = b.call(value)
34
+ }
35
+ end
36
+ end
37
+
38
+ end
39
+ 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
@@ -0,0 +1,9 @@
1
+ module CheckPlease
2
+
3
+ module Error
4
+ # Rather than having a common error superclass, I'm taking a cue from
5
+ # https://avdi.codes/exceptionalruby and tagging things with a module
6
+ # instead....
7
+ end
8
+
9
+ end
@@ -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, format)
16
- format ||= DEFAULT_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
@@ -1,3 +1,5 @@
1
1
  module CheckPlease
2
- VERSION = "0.1.0"
2
+ # NOTE: 'check_please_rspec_matcher' depends on this,
3
+ # so try to keep them roughly in sync
4
+ VERSION = "0.2.0"
3
5
  end
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.1.0
4
+ version: 0.2.0
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-06 00:00:00.000000000 Z
11
+ date: 2020-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: table_print
@@ -74,9 +74,14 @@ files:
74
74
  - bin/setup
75
75
  - check_please.gemspec
76
76
  - lib/check_please.rb
77
+ - lib/check_please/cli.rb
78
+ - lib/check_please/cli/flag.rb
79
+ - lib/check_please/cli/parser.rb
80
+ - lib/check_please/cli/runner.rb
77
81
  - lib/check_please/comparison.rb
78
82
  - lib/check_please/diff.rb
79
83
  - lib/check_please/diffs.rb
84
+ - lib/check_please/error.rb
80
85
  - lib/check_please/path.rb
81
86
  - lib/check_please/printers.rb
82
87
  - lib/check_please/printers/base.rb