check_please 0.3.0 → 0.5.2

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.
@@ -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,34 +1,37 @@
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
- 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
25
+ case types_for_compare(ref, can)
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
 
31
- def types(*list)
34
+ def types_for_compare(*list)
32
35
  list.map { |e|
33
36
  case e
34
37
  when Array ; :array
@@ -38,54 +41,146 @@ module CheckPlease
38
41
  }
39
42
  end
40
43
 
41
- def compare_arrays(ref_array, can_array, path, diffs)
42
- max_len = [ ref_array, can_array ].map(&:length).max
43
- (0...max_len).each do |i|
44
- n = i + 1 # count in human pls
45
- new_path = path + n
44
+ def compare_arrays(ref_array, can_array, path)
45
+ if ( key = path.key_for_compare(flags) )
46
+ compare_arrays_by_key ref_array, can_array, path, key
47
+ else
48
+ compare_arrays_by_index ref_array, can_array, path
49
+ end
50
+ end
51
+
52
+ def compare_arrays_by_key(ref_array, can_array, path, key_name)
53
+ refs_by_key = index_array!(ref_array, path, key_name, "reference")
54
+ cans_by_key = index_array!(can_array, path, key_name, "candidate")
46
55
 
47
- ref = ref_array[i]
48
- can = can_array[i]
56
+ key_values = (refs_by_key.keys | cans_by_key.keys)
49
57
 
50
- 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
58
+ key_values.compact! # NOTE: will break if nil is ever used as a key (but WHO WOULD DO THAT?!)
59
+ key_values.sort!
60
+
61
+ key_values.each do |key_value|
62
+ new_path = path + "#{key_name}=#{key_value}"
63
+ ref = refs_by_key[key_value]
64
+ can = cans_by_key[key_value]
65
+ case
66
+ when ref.nil? ; record_diff ref, can, new_path, :extra
67
+ when can.nil? ; record_diff ref, can, new_path, :missing
68
+ else ; compare ref, can, new_path
69
+ end
54
70
  end
55
71
  end
56
- end
57
72
 
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
73
+ def index_array!(array_of_hashes, path, key_name, ref_or_can)
74
+ elements_by_key = {}
75
+
76
+ array_of_hashes.each.with_index do |h, i|
77
+ # make sure we have a hash
78
+ unless h.is_a?(Hash)
79
+ raise CheckPlease::TypeMismatchError, \
80
+ "The element at position #{i} in the #{ref_or_can} array is not a hash."
81
+ end
82
+
83
+ if flags.indifferent_keys
84
+ h = stringify_symbol_keys(h)
85
+ end
86
+
87
+ # try to get the value of the attribute identified by key_name
88
+ key_value = h.fetch(key_name) {
89
+ raise CheckPlease::NoSuchKeyError, \
90
+ <<~EOF
91
+ The #{ref_or_can} hash at position #{i} has no #{key_name.inspect} key.
92
+ Keys it does have: #{h.keys.inspect}
93
+ EOF
94
+ }
95
+
96
+ # complain about dupes
97
+ if elements_by_key.has_key?(key_value)
98
+ key_val_expr = "#{key_name}=#{key_value}"
99
+ raise CheckPlease::DuplicateKeyError, \
100
+ "Duplicate #{ref_or_can} element found at path '#{path + key_val_expr}'."
101
+ end
102
+
103
+ # ok, now we can proceed
104
+ elements_by_key[key_value] = h
105
+ end
106
+
107
+ elements_by_key
108
+ end
109
+
110
+ def compare_arrays_by_index(ref_array, can_array, path)
111
+ max_len = [ ref_array, can_array ].map(&:length).max
112
+ (0...max_len).each do |i|
113
+ n = i + 1 # count in human pls
114
+ new_path = path + n
115
+
116
+ ref = ref_array[i]
117
+ can = can_array[i]
118
+
119
+ case
120
+ when ref_array.length < n ; record_diff ref, can, new_path, :extra
121
+ when can_array.length < n ; record_diff ref, can, new_path, :missing
122
+ else
123
+ compare ref, can, new_path
124
+ end
125
+ end
126
+ end
127
+
128
+ def compare_hashes(ref_hash, can_hash, path)
129
+ if flags.indifferent_keys
130
+ ref_hash = stringify_symbol_keys(ref_hash)
131
+ can_hash = stringify_symbol_keys(can_hash)
132
+ end
133
+ record_missing_keys ref_hash, can_hash, path
134
+ compare_common_keys ref_hash, can_hash, path
135
+ record_extra_keys ref_hash, can_hash, path
62
136
  end
63
137
 
64
- def record_missing_keys(ref_hash, can_hash, path, diffs)
138
+ def stringify_symbol_keys(h)
139
+ Hash[
140
+ h.map { |k,v|
141
+ [ stringify_symbol(k), v ]
142
+ }
143
+ ]
144
+ end
145
+
146
+ def stringify_symbol(x)
147
+ Symbol === x ? x.to_s : x
148
+ end
149
+
150
+ def record_missing_keys(ref_hash, can_hash, path)
65
151
  keys = ref_hash.keys - can_hash.keys
66
152
  keys.each do |k|
67
- diffs.record ref_hash[k], nil, path + k, :missing
153
+ record_diff ref_hash[k], nil, path + k, :missing
68
154
  end
69
155
  end
70
156
 
71
- def compare_common_keys(ref_hash, can_hash, path, diffs)
157
+ def compare_common_keys(ref_hash, can_hash, path)
72
158
  keys = ref_hash.keys & can_hash.keys
73
159
  keys.each do |k|
74
- compare ref_hash[k], can_hash[k], path + k, diffs
160
+ compare ref_hash[k], can_hash[k], path + k
75
161
  end
76
162
  end
77
163
 
78
- def record_extra_keys(ref_hash, can_hash, path, diffs)
164
+ def record_extra_keys(ref_hash, can_hash, path)
79
165
  keys = can_hash.keys - ref_hash.keys
80
166
  keys.each do |k|
81
- diffs.record nil, can_hash[k], path + k, :extra
167
+ record_diff nil, can_hash[k], path + k, :extra
82
168
  end
83
169
  end
84
170
 
85
- def compare_others(ref, can, path, diffs)
171
+ def compare_others(ref, can, path)
172
+ if flags.indifferent_values
173
+ ref = stringify_symbol(ref)
174
+ can = stringify_symbol(can)
175
+ end
86
176
  return if ref == can
87
- diffs.record ref, can, path, :mismatch
177
+ record_diff ref, can, path, :mismatch
88
178
  end
179
+
180
+ def record_diff(ref, can, path, type)
181
+ diff = Diff.new(type, path, ref, can)
182
+ diffs << diff
183
+ end
89
184
  end
90
185
 
91
186
  end
@@ -3,12 +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
12
  end
13
13
 
14
14
  def attributes
@@ -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,14 +43,14 @@ 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
48
49
 
50
+ def to_s(flags = {})
51
+ CheckPlease::Printers.render(self, flags)
52
+ end
53
+
49
54
  extend Forwardable
50
55
  def_delegators :@list, *%i[
51
56
  each
@@ -6,4 +6,28 @@ module CheckPlease
6
6
  # instead....
7
7
  end
8
8
 
9
+ class DuplicateKeyError < ::IndexError
10
+ include CheckPlease::Error
11
+ end
12
+
13
+ class InvalidFlag < ArgumentError
14
+ include CheckPlease::Error
15
+ end
16
+
17
+ class InvalidPath < ArgumentError
18
+ include CheckPlease::Error
19
+ end
20
+
21
+ class InvalidPathSegment < ArgumentError
22
+ include CheckPlease::Error
23
+ end
24
+
25
+ class NoSuchKeyError < ::KeyError
26
+ include CheckPlease::Error
27
+ end
28
+
29
+ class TypeMismatchError < ::TypeError
30
+ include CheckPlease::Error
31
+ end
32
+
9
33
  end