check_please 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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