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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b880759afe4d4cfa6b3fda2332fdea92be7a141a2faa4ce24ed1f5952a395953
4
- data.tar.gz: 580bb6e7de516510534d315f4d2a0effb539f4e195bb58a30a4f345912591523
3
+ metadata.gz: 41ad78ce4324c1b5d9c8ac3e336d46108850d6fd7d1d51d30c13b807632d612a
4
+ data.tar.gz: 9191e6e52c57b6d79761a8a0258a41378cf19af1364a3e8875e727ff8e501f06
5
5
  SHA512:
6
- metadata.gz: 2c23315e2c734b3d8234c2a8f05c3d7f3626b0d53b255c182f68de20a60df7f28c124260aedb59f015560fc625f4301c48ec29bdf3b772f495c34c6181e114d6
7
- data.tar.gz: 973adce53f7fd590bbd478e8727f3f409def0caed3815b049af9613427a866d761426fcd84c481798e73d7da3abd9c9d77a8bc726171f3030ac7d87b481ddb12
6
+ metadata.gz: 388b93c0277580a37b066b7fa6aade4f8feeb5f677a4ccc2221ac060d23df153c3dc16c6cdacce740448773665200855e653d74f55cfae659511e39a4391b36f
7
+ data.tar.gz: 44cb45ace2a720d90035eef279f490896a785e76c77bd4bd72c63400a828300332638442599b905dad8e4bde4b329a698fd44f4116f847699b5250c586e27c86
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- check_please (0.2.2)
4
+ check_please (0.4.1)
5
5
  table_print
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # check_please
2
2
 
3
- Check for differences between two JSON strings (or Ruby data structures parsed from them).
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
- ## Usage
22
-
23
- ### Terminology
22
+ ## Terminology
24
23
 
25
- CheckPlease uses a few words in a jargony way:
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
- ### CLI
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 Matcher
51
+ ### From RSpec
50
52
 
51
53
  See [check_please_rspec_matcher](https://github.com/RealGeeks/check_please_rspec_matcher).
52
54
 
53
- ### From Within Ruby
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
- Create two JSON strings and pass them to `CheckPlease.render_diff`. You'll get
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
- (You can also parse the JSON strings yourself and pass the resulting data
60
- structures in, if you're into that. I mean, I wrote this to help compare JSON
61
- data that's too big and complicated to scan through visually, but you do you!
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. (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.)
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.** 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.)_
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
- contained values that were not found in the **reference**.
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 basic:
147
+ The second column contains a path expression. This is extremely lo-fi:
142
148
 
143
- * The first element in the data structure is defined as "/".
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
 
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative '../lib/check_please'
3
+ require 'check_please'
4
4
  CheckPlease::CLI.run(__FILE__)
@@ -1,40 +1,130 @@
1
- require_relative "check_please/version"
2
- require_relative "check_please/error"
3
- require_relative "check_please/path"
4
- require_relative "check_please/comparison"
5
- require_relative "check_please/diff"
6
- require_relative "check_please/diffs"
7
- require_relative "check_please/printers"
8
- require_relative "check_please/cli"
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, options = {})
26
+ def self.diff(reference, candidate, flags = {})
14
27
  reference = maybe_parse(reference)
15
28
  candidate = maybe_parse(candidate)
16
- Comparison.perform(reference, candidate, options)
29
+ Comparison.perform(reference, candidate, flags)
17
30
  end
18
31
 
19
- def self.render_diff(reference, candidate, options = {})
20
- diffs = diff(reference, candidate, options)
21
- Printers.render(diffs, options)
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! That's what makes it so exciting!
29
- def maybe_parse(maybe_json)
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 maybe_json
32
- when String ; JSON.parse(maybe_json) # don't worry, if this raises we'll assume you've already parsed it
33
- else ; maybe_json
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 maybe_json
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
@@ -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
- 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 #####
4
+ autoload :Runner, "check_please/cli/parser"
5
+ autoload :Parser, "check_please/cli/runner"
22
6
 
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
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
- # 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
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 UnrecognizedOption, e.message, cause: e
20
+ raise InvalidFlag, e.message, cause: e
29
21
  end
30
22
 
31
23
  def help
32
- @optparse.help
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
- options = @parser.consume_flags!(args)
18
- rescue Parser::UnrecognizedOption => e
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, options)
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
- module Comparison
4
- extend self
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, diffs
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, diffs)
18
- if (d = diffs.options[:max_depth])
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, diffs
24
- when [ :hash, :hash ] ; compare_hashes ref, can, path, diffs
25
- when [ :other, :other ] ; compare_others ref, can, path, diffs
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
- diffs.record ref, can, path, :type_mismatch
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, diffs)
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 ; diffs.record ref, can, new_path, :extra
52
- when can_array.length < n ; diffs.record ref, can, new_path, :missing
53
- else ; compare ref, can, new_path, diffs
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, diffs)
59
- record_missing_keys ref_hash, can_hash, path, diffs
60
- compare_common_keys ref_hash, can_hash, path, diffs
61
- record_extra_keys ref_hash, can_hash, path, diffs
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, diffs)
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
- diffs.record ref_hash[k], nil, path + k, :missing
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, diffs)
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, diffs
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, diffs)
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
- diffs.record nil, can_hash[k], path + k, :extra
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, diffs)
89
+ def compare_others(ref, can, path)
86
90
  return if ref == can
87
- diffs.record ref, can, path, :mismatch
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
@@ -3,20 +3,12 @@ module CheckPlease
3
3
  class Diff
4
4
  COLUMNS = %i[ type path reference candidate ]
5
5
 
6
- attr_reader :type, :reference, :candidate, :path
7
- def initialize(type, reference, candidate, path)
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=#{ref_display}"
32
- s << " can=#{can_display}"
23
+ s << " ref=#{reference.inspect}"
24
+ s << " can=#{candidate.inspect}"
33
25
  s << ">"
34
26
  s
35
27
  end
@@ -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 :options
9
- def initialize(diff_list = nil, options: {})
10
- @options = options
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 (n = options[:max_diffs])
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
@@ -6,4 +6,8 @@ module CheckPlease
6
6
  # instead....
7
7
  end
8
8
 
9
+ class InvalidFlag < ArgumentError
10
+ include CheckPlease::Error
11
+ end
12
+
9
13
  end
@@ -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
@@ -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 to_s
19
- SEPARATOR + @segments.join(SEPARATOR)
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, options = {})
16
- format = options[:format] || DEFAULT_FORMAT
17
- printer = PRINTERS_BY_FORMAT[format.to_sym]
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
@@ -1,5 +1,3 @@
1
- require 'json'
2
-
3
1
  module CheckPlease
4
2
  module Printers
5
3
 
@@ -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
- :type,
9
- { :path => { width: 250 } }, # if you hit this limit, you have other problems
10
- :reference,
11
- :candidate,
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
@@ -1,5 +1,5 @@
1
1
  module CheckPlease
2
2
  # NOTE: 'check_please_rspec_matcher' depends on this,
3
3
  # so try to keep them roughly in sync
4
- VERSION = "0.2.2"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -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.2.2
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-15 00:00:00.000000000 Z
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