check_please 0.2.3 → 0.5.0

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