check_please 0.2.4 → 0.5.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.
@@ -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,54 +41,121 @@ 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")
55
+ key_values = (refs_by_key.keys | cans_by_key.keys)
46
56
 
47
- ref = ref_array[i]
48
- can = can_array[i]
57
+ key_values.compact! # NOTE: will break if nil is ever used as a key (but WHO WOULD DO THAT?!)
58
+ key_values.sort!
49
59
 
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
60
+ key_values.each do |key_value|
61
+ new_path = path + "#{key_name}=#{key_value}"
62
+ ref = refs_by_key[key_value]
63
+ can = cans_by_key[key_value]
64
+ case
65
+ when ref.nil? ; record_diff ref, can, new_path, :extra
66
+ when can.nil? ; record_diff ref, can, new_path, :missing
67
+ else ; compare ref, can, new_path
68
+ end
54
69
  end
55
70
  end
56
- end
57
71
 
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
72
+ def index_array!(array_of_hashes, path, key_name, ref_or_can)
73
+ elements_by_key = {}
74
+
75
+ array_of_hashes.each.with_index do |h, i|
76
+ # make sure we have a hash
77
+ unless h.is_a?(Hash)
78
+ raise CheckPlease::TypeMismatchError, \
79
+ "The element at position #{i} in the #{ref_or_can} array is not a hash."
80
+ end
81
+
82
+ # try to get the value of the attribute identified by key_name
83
+ key_value = h.fetch(key_name) {
84
+ raise CheckPlease::NoSuchKeyError, \
85
+ <<~EOF
86
+ The #{ref_or_can} hash at position #{i} has no #{key_name.inspect} key.
87
+ Keys it does have: #{h.keys.inspect}
88
+ EOF
89
+ }
90
+
91
+ # complain about dupes
92
+ if elements_by_key.has_key?(key_value)
93
+ key_val_expr = "#{key_name}=#{key_value}"
94
+ raise CheckPlease::DuplicateKeyError, \
95
+ "Duplicate #{ref_or_can} element found at path '#{path + key_val_expr}'."
96
+ end
97
+
98
+ # ok, now we can proceed
99
+ elements_by_key[key_value] = h
100
+ end
101
+
102
+ elements_by_key
103
+ end
104
+
105
+ def compare_arrays_by_index(ref_array, can_array, path)
106
+ max_len = [ ref_array, can_array ].map(&:length).max
107
+ (0...max_len).each do |i|
108
+ n = i + 1 # count in human pls
109
+ new_path = path + n
110
+
111
+ ref = ref_array[i]
112
+ can = can_array[i]
113
+
114
+ case
115
+ when ref_array.length < n ; record_diff ref, can, new_path, :extra
116
+ when can_array.length < n ; record_diff ref, can, new_path, :missing
117
+ else
118
+ compare ref, can, new_path
119
+ end
120
+ end
121
+ end
122
+
123
+ def compare_hashes(ref_hash, can_hash, path)
124
+ record_missing_keys ref_hash, can_hash, path
125
+ compare_common_keys ref_hash, can_hash, path
126
+ record_extra_keys ref_hash, can_hash, path
62
127
  end
63
128
 
64
- def record_missing_keys(ref_hash, can_hash, path, diffs)
129
+ def record_missing_keys(ref_hash, can_hash, path)
65
130
  keys = ref_hash.keys - can_hash.keys
66
131
  keys.each do |k|
67
- diffs.record ref_hash[k], nil, path + k, :missing
132
+ record_diff ref_hash[k], nil, path + k, :missing
68
133
  end
69
134
  end
70
135
 
71
- def compare_common_keys(ref_hash, can_hash, path, diffs)
136
+ def compare_common_keys(ref_hash, can_hash, path)
72
137
  keys = ref_hash.keys & can_hash.keys
73
138
  keys.each do |k|
74
- compare ref_hash[k], can_hash[k], path + k, diffs
139
+ compare ref_hash[k], can_hash[k], path + k
75
140
  end
76
141
  end
77
142
 
78
- def record_extra_keys(ref_hash, can_hash, path, diffs)
143
+ def record_extra_keys(ref_hash, can_hash, path)
79
144
  keys = can_hash.keys - ref_hash.keys
80
145
  keys.each do |k|
81
- diffs.record nil, can_hash[k], path + k, :extra
146
+ record_diff nil, can_hash[k], path + k, :extra
82
147
  end
83
148
  end
84
149
 
85
- def compare_others(ref, can, path, diffs)
150
+ def compare_others(ref, can, path)
86
151
  return if ref == can
87
- diffs.record ref, can, path, :mismatch
152
+ record_diff ref, can, path, :mismatch
88
153
  end
154
+
155
+ def record_diff(ref, can, path, type)
156
+ diff = Diff.new(type, path, ref, can)
157
+ diffs << diff
158
+ end
89
159
  end
90
160
 
91
161
  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,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
@@ -0,0 +1,88 @@
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 description=(value)
33
+ if value.is_a?(String) && value =~ /\n/m
34
+ lines = value.lines
35
+ else
36
+ lines = Array(value).map(&:to_s)
37
+ end
38
+
39
+ @description = lines.map(&:rstrip)
40
+ end
41
+
42
+ def mutually_exclusive_to(flag_name)
43
+ @validators << ->(flags, _) { flags.send(flag_name).empty? }
44
+ end
45
+
46
+ def repeatable
47
+ @repeatable = true
48
+ self.default_proc = ->{ Array.new }
49
+ end
50
+
51
+ def validate(&block)
52
+ @validators << block
53
+ end
54
+
55
+ protected
56
+
57
+ def __set__(value, on:, flags:)
58
+ val = _coerce(value)
59
+ _validate(flags, val)
60
+ if @repeatable
61
+ on[name] ||= []
62
+ on[name].concat(Array(val))
63
+ else
64
+ on[name] = val
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def _coerce(value)
71
+ return value if @coercer.nil?
72
+ @coercer.call(value)
73
+ end
74
+
75
+ def _validate(flags, value)
76
+ return if @validators.empty?
77
+ return if @validators.all? { |block| block.call(flags, value) }
78
+ raise InvalidFlag, "#{value.inspect} is not a legal value for #{name}"
79
+ end
80
+
81
+ def set_attribute!(name, value)
82
+ self.send "#{name}=", value
83
+ rescue NoMethodError
84
+ raise ArgumentError, "unrecognized attribute: #{name}"
85
+ end
86
+ end
87
+
88
+ end