check_please 0.2.2 → 0.4.1

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: 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