check_please 0.5.0 → 0.5.5

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: fe333e55fbfdb89d0ea68b3f9ceefde98f3aa4b798792e9dd13745b76757a499
4
- data.tar.gz: cf8cfc0205d3b8d7a3d10ec1c0c5487ac140dd7bc41c0c640eb2d72ec3d1e3f7
3
+ metadata.gz: 9c80defc851cfda5e74a5590da7be40388643084c1e06818639685bfe05e8a46
4
+ data.tar.gz: fe1a9a3b8be29b8e148917ed19052ae2d0195218624b0bf195421cc29c83c20b
5
5
  SHA512:
6
- metadata.gz: 918e8b6edec715a8096872c46fa12adc029244b886525c704e9b769fecae6595d38d0b658ebe64a53ebe3474d6ddba76f7d8222baefbef582da4c7dec27e686e
7
- data.tar.gz: 4e0da6a791cc4e539ed75f893a3950336bcf5703e6dbb6435e02bac23f94e3207c06dca5cf13551a4a81980474e4e796a911ea333bfec42b5d97aa1e6ef0f3c8
6
+ metadata.gz: 0b70a2c80630b51d789327c36e9b288aa2484d2b80cc888f5e8a94acf6bed1d7eef67c659f7ac17a9e7ed6c14aacc8cad9c18414881b2cfe07bf4e96aaad785e
7
+ data.tar.gz: 2cde0b422e2ae297517680f2408201b3f7c88e2e1f81da667d2c875dfddd31b544bef57864dff3ac8739be01b09676e5fd22ff1cbb54677e64f791ff606d3a05
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- check_please (0.5.0)
4
+ check_please (0.5.5)
5
5
  table_print
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -19,7 +19,7 @@ structures parsed from either of those.
19
19
  * [Flags](#flags)
20
20
  * [Setting Flags in the CLI](#setting-flags-in-the-cli)
21
21
  * [Setting Flags in Ruby](#setting-flags-in-ruby)
22
- * ["Reentrant" Flags](#reentrant-flags)
22
+ * [Repeatable Flags](#repeatable-flags)
23
23
  * [Expanded Documentation for Specific Flags](#expanded-documentation-for-specific-flags)
24
24
  * [Flag: match_by_key](#flag-match_by_key)
25
25
  * [TODO (maybe)](#todo-maybe)
data/Rakefile CHANGED
@@ -59,6 +59,10 @@ task :toc do
59
59
  end
60
60
  end
61
61
 
62
- # By making TOC generation a prerequisite of release, we *should* at least be
63
- # forced to update the TOC whenever we publish a new version of the gem...
64
- task :release => :toc
62
+ # Okay, so. We want the TOC to be up to date *before* the `release` task runs.
63
+ #
64
+ # We tried making the 'toc' task a dependency of 'release', but that just adds
65
+ # it to the end of the dependencies, and generates the TOC after publishing.
66
+ #
67
+ # Trying the 'build' task instead...
68
+ task :build => :toc
data/lib/check_please.rb CHANGED
@@ -7,17 +7,17 @@ require "check_please/error"
7
7
  require "check_please/version"
8
8
 
9
9
  module CheckPlease
10
- autoload :Reification, "check_please/reification"
11
- autoload :CLI, "check_please/cli"
12
- autoload :Comparison, "check_please/comparison"
13
- autoload :Diff, "check_please/diff"
14
- autoload :Diffs, "check_please/diffs"
15
- autoload :Flag, "check_please/flag"
16
- autoload :Flags, "check_please/flags"
17
- autoload :Path, "check_please/path"
18
- autoload :PathSegment, "check_please/path_segment"
19
- autoload :Printers, "check_please/printers"
20
- autoload :Refinements, "check_please/refinements"
10
+ autoload :Reification, "check_please/reification"
11
+ autoload :CLI, "check_please/cli"
12
+ autoload :Comparison, "check_please/comparison"
13
+ autoload :Diff, "check_please/diff"
14
+ autoload :Diffs, "check_please/diffs"
15
+ autoload :Flag, "check_please/flag"
16
+ autoload :Flags, "check_please/flags"
17
+ autoload :Path, "check_please/path"
18
+ autoload :PathSegment, "check_please/path_segment"
19
+ autoload :PathSegmentMatcher, "check_please/path_segment_matcher"
20
+ autoload :Printers, "check_please/printers"
21
21
  end
22
22
 
23
23
 
@@ -143,4 +143,38 @@ module CheckPlease
143
143
  EOF
144
144
  end
145
145
 
146
+ Flags.define :match_by_value do |flag|
147
+ flag.repeatable
148
+ flag.coerce { |value| CheckPlease::Path.reify(value) }
149
+
150
+ flag.cli_long = "--match-by-value FOO"
151
+ flag.description = <<~EOF
152
+ When comparing two arrays that match a specified path, the candidate
153
+ array will be scanned for each element in the reference array.
154
+ May be repeated; values will be treated as an 'OR' list.
155
+ NOTE: explodes if either array at a given path contains other collections.
156
+ NOTE: paths of 'extra' diffs use the index in the candidate array.
157
+ EOF
158
+ end
159
+
160
+ Flags.define :indifferent_keys do |flag|
161
+ flag.default = false
162
+ flag.coerce { |value| !!value }
163
+
164
+ flag.cli_long = "--indifferent-keys"
165
+ flag.description = <<~EOF
166
+ When comparing hashes, convert symbol keys to strings
167
+ EOF
168
+ end
169
+
170
+ Flags.define :indifferent_values do |flag|
171
+ flag.default = false
172
+ flag.coerce { |value| !!value }
173
+
174
+ flag.cli_long = "--indifferent-values"
175
+ flag.description = <<~EOF
176
+ When comparing values (that aren't arrays or hashes), convert symbols to strings
177
+ EOF
178
+ end
179
+
146
180
  end
@@ -1,5 +1,4 @@
1
1
  module CheckPlease
2
- using Refinements
3
2
 
4
3
  class Comparison
5
4
  def self.perform(reference, candidate, flags = {})
@@ -7,13 +6,13 @@ module CheckPlease
7
6
  end
8
7
 
9
8
  def perform(reference, candidate, flags = {})
10
- @flags = Flags(flags) # whoa, it's almost like Java in here
9
+ @flags = Flags.reify(flags)
11
10
  @diffs = Diffs.new(flags: @flags)
12
11
 
13
12
  catch(:max_diffs_reached) do
14
13
  compare reference, candidate, CheckPlease::Path.root
15
14
  end
16
- diffs
15
+ diffs.filter_by_flags(@flags)
17
16
  end
18
17
 
19
18
  private
@@ -22,7 +21,7 @@ module CheckPlease
22
21
  def compare(ref, can, path)
23
22
  return if path.excluded?(flags)
24
23
 
25
- case types(ref, can)
24
+ case types_for_compare(ref, can)
26
25
  when [ :array, :array ] ; compare_arrays ref, can, path
27
26
  when [ :hash, :hash ] ; compare_hashes ref, can, path
28
27
  when [ :other, :other ] ; compare_others ref, can, path
@@ -31,7 +30,7 @@ module CheckPlease
31
30
  end
32
31
  end
33
32
 
34
- def types(*list)
33
+ def types_for_compare(*list)
35
34
  list.map { |e|
36
35
  case e
37
36
  when Array ; :array
@@ -42,8 +41,11 @@ module CheckPlease
42
41
  end
43
42
 
44
43
  def compare_arrays(ref_array, can_array, path)
45
- if ( key = path.key_for_compare(flags) )
44
+ case
45
+ when ( key = path.key_to_match_by(flags) )
46
46
  compare_arrays_by_key ref_array, can_array, path, key
47
+ when path.match_by_value?(flags)
48
+ compare_arrays_by_value ref_array, can_array, path
47
49
  else
48
50
  compare_arrays_by_index ref_array, can_array, path
49
51
  end
@@ -52,6 +54,7 @@ module CheckPlease
52
54
  def compare_arrays_by_key(ref_array, can_array, path, key_name)
53
55
  refs_by_key = index_array!(ref_array, path, key_name, "reference")
54
56
  cans_by_key = index_array!(can_array, path, key_name, "candidate")
57
+
55
58
  key_values = (refs_by_key.keys | cans_by_key.keys)
56
59
 
57
60
  key_values.compact! # NOTE: will break if nil is ever used as a key (but WHO WOULD DO THAT?!)
@@ -79,6 +82,10 @@ module CheckPlease
79
82
  "The element at position #{i} in the #{ref_or_can} array is not a hash."
80
83
  end
81
84
 
85
+ if flags.indifferent_keys
86
+ h = stringify_symbol_keys(h)
87
+ end
88
+
82
89
  # try to get the value of the attribute identified by key_name
83
90
  key_value = h.fetch(key_name) {
84
91
  raise CheckPlease::NoSuchKeyError, \
@@ -102,6 +109,45 @@ module CheckPlease
102
109
  elements_by_key
103
110
  end
104
111
 
112
+ # FIXME: this can generate duplicate paths.
113
+ # Time to introduce lft_path, rgt_path ?
114
+ def compare_arrays_by_value(ref_array, can_array, path)
115
+ assert_can_match_by_value! ref_array
116
+ assert_can_match_by_value! can_array
117
+
118
+ matches = can_array.map { false }
119
+
120
+ # Look for missing values
121
+ ref_array.each.with_index do |ref, i|
122
+ new_path = path + (i+1) # count in human pls
123
+
124
+ # Weird, but necessary to handle duplicates properly
125
+ j = can_array.index.with_index { |can, j|
126
+ matches[j] == false && can == ref
127
+ }
128
+
129
+ if j
130
+ matches[j] = true
131
+ else
132
+ record_diff ref, nil, new_path, :missing
133
+ end
134
+ end
135
+
136
+ # Look for extra values
137
+ can_array.zip(matches).each.with_index do |(can, match), i|
138
+ next if match
139
+ new_path = path + (i+1) # count in human pls
140
+ record_diff nil, can, new_path, :extra
141
+ end
142
+ end
143
+
144
+ def assert_can_match_by_value!(array)
145
+ if array.any? { |e| Array === e || Hash === e }
146
+ raise CheckPlease::BehaviorUndefined,
147
+ "match_by_value behavior is not defined for collections!"
148
+ end
149
+ end
150
+
105
151
  def compare_arrays_by_index(ref_array, can_array, path)
106
152
  max_len = [ ref_array, can_array ].map(&:length).max
107
153
  (0...max_len).each do |i|
@@ -121,11 +167,27 @@ module CheckPlease
121
167
  end
122
168
 
123
169
  def compare_hashes(ref_hash, can_hash, path)
170
+ if flags.indifferent_keys
171
+ ref_hash = stringify_symbol_keys(ref_hash)
172
+ can_hash = stringify_symbol_keys(can_hash)
173
+ end
124
174
  record_missing_keys ref_hash, can_hash, path
125
175
  compare_common_keys ref_hash, can_hash, path
126
176
  record_extra_keys ref_hash, can_hash, path
127
177
  end
128
178
 
179
+ def stringify_symbol_keys(h)
180
+ Hash[
181
+ h.map { |k,v|
182
+ [ stringify_symbol(k), v ]
183
+ }
184
+ ]
185
+ end
186
+
187
+ def stringify_symbol(x)
188
+ Symbol === x ? x.to_s : x
189
+ end
190
+
129
191
  def record_missing_keys(ref_hash, can_hash, path)
130
192
  keys = ref_hash.keys - can_hash.keys
131
193
  keys.each do |k|
@@ -148,6 +210,10 @@ module CheckPlease
148
210
  end
149
211
 
150
212
  def compare_others(ref, can, path)
213
+ if flags.indifferent_values
214
+ ref = stringify_symbol(ref)
215
+ can = stringify_symbol(can)
216
+ end
151
217
  return if ref == can
152
218
  record_diff ref, can, path, :mismatch
153
219
  end
@@ -1,14 +1,13 @@
1
1
  require 'forwardable'
2
2
 
3
3
  module CheckPlease
4
- using Refinements
5
4
 
6
5
  # Custom collection class for Diff instances.
7
6
  # Can retrieve members using indexes or paths.
8
7
  class Diffs
9
8
  attr_reader :flags
10
9
  def initialize(diff_list = nil, flags: {})
11
- @flags = Flags(flags)
10
+ @flags = Flags.reify(flags)
12
11
  @list = []
13
12
  @hash = {}
14
13
  Array(diff_list).each do |diff|
@@ -47,10 +46,27 @@ module CheckPlease
47
46
  @list.map(&:attributes)
48
47
  end
49
48
 
49
+ def filter_by_flags(flags)
50
+ new_list = @list.reject { |diff| Path.new(diff.path).excluded?(flags) }
51
+ self.class.new(new_list, flags: flags)
52
+ end
53
+
50
54
  def to_s(flags = {})
51
55
  CheckPlease::Printers.render(self, flags)
52
56
  end
53
57
 
58
+ def method_missing(meth, *args, &blk)
59
+ if formats.include?(meth.to_sym)
60
+ CheckPlease::Printers.render(self, format: meth)
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def formats
67
+ CheckPlease::Printers::FORMATS
68
+ end
69
+
54
70
  extend Forwardable
55
71
  def_delegators :@list, *%i[
56
72
  each
@@ -6,6 +6,10 @@ module CheckPlease
6
6
  # instead....
7
7
  end
8
8
 
9
+ class BehaviorUndefined < ::StandardError
10
+ include CheckPlease::Error
11
+ end
12
+
9
13
  class DuplicateKeyError < ::IndexError
10
14
  include CheckPlease::Error
11
15
  end
@@ -3,6 +3,9 @@ module CheckPlease
3
3
  # NOTE: this gets all of its attributes defined (via .define) in ../check_please.rb
4
4
 
5
5
  class Flags
6
+ include CheckPlease::Reification
7
+ can_reify Hash
8
+
6
9
  BY_NAME = {} ; private_constant :BY_NAME
7
10
 
8
11
  def self.[](name)
@@ -17,7 +17,12 @@ module CheckPlease
17
17
  def initialize(name_or_segments = [])
18
18
  case name_or_segments
19
19
  when String, Symbol, Numeric, nil
20
- names = name_or_segments.to_s.split(SEPARATOR)
20
+ string = name_or_segments.to_s
21
+ if string =~ %r(//)
22
+ raise InvalidPath, "paths cannot have empty segments"
23
+ end
24
+
25
+ names = string.split(SEPARATOR)
21
26
  names.shift until names.empty? || names.first =~ /\S/
22
27
  segments = PathSegment.reify(names)
23
28
  when Array
@@ -26,10 +31,6 @@ module CheckPlease
26
31
  raise InvalidPath, "not sure what to do with #{name_or_segments.inspect}"
27
32
  end
28
33
 
29
- if segments.any?(&:empty?)
30
- raise InvalidPath, "#{self.class.name} cannot contain empty segments"
31
- end
32
-
33
34
  @segments = Array(segments)
34
35
 
35
36
  @to_s = SEPARATOR + @segments.join(SEPARATOR)
@@ -81,13 +82,10 @@ module CheckPlease
81
82
  "<#{self.class.name} '#{to_s}'>"
82
83
  end
83
84
 
84
- # TODO: Naming Things
85
- def key_for_compare(flags)
86
- mbk_exprs = unpack_mbk_exprs(flags)
87
- matches = mbk_exprs.select { |mbk_expr|
88
- # NOTE: matching on parent because MBK '/foo/:id' should return 'id' for path '/foo'
89
- mbk_expr.parent.match?(self)
90
- }
85
+ def key_to_match_by(flags)
86
+ key_exprs = unpack_key_exprs(flags.match_by_key)
87
+ # NOTE: match on parent because if self.to_s == '/foo', MBK '/foo/:id' should return 'id'
88
+ matches = key_exprs.select { |e| e.parent.match?(self) }
91
89
 
92
90
  case matches.length
93
91
  when 0 ; nil
@@ -96,6 +94,10 @@ module CheckPlease
96
94
  end
97
95
  end
98
96
 
97
+ def match_by_value?(flags)
98
+ flags.match_by_value.any? { |e| e.match?(self) }
99
+ end
100
+
99
101
  def match?(path_or_string)
100
102
  # If the strings are literally equal, we're good..
101
103
  return true if self == path_or_string
@@ -123,7 +125,7 @@ module CheckPlease
123
125
  # (as of this writing, this should never actually happen, but I'm being thorough)
124
126
  def ancestor_on_list?(paths)
125
127
  paths.any? { |path|
126
- ancestors.any? { |ancestor| ancestor == path }
128
+ ancestors.any? { |ancestor| ancestor.match?(path) }
127
129
  }
128
130
  end
129
131
 
@@ -152,7 +154,7 @@ module CheckPlease
152
154
 
153
155
  # O(n) check to see if the path itself is on a list
154
156
  def self_on_list?(paths)
155
- paths.any? { |path| self == path }
157
+ paths.any? { |path| self.match?(path) }
156
158
  end
157
159
 
158
160
  def too_deep?(flags)
@@ -160,8 +162,8 @@ module CheckPlease
160
162
  depth > flags.max_depth
161
163
  end
162
164
 
163
- def unpack_mbk_exprs(flags)
164
- flags.match_by_key
165
+ def unpack_key_exprs(path_list)
166
+ path_list
165
167
  .map { |path| path.send(:key_exprs) }
166
168
  .flatten
167
169
  .uniq { |e| e.to_s } # use the block form so we don't have to implement #hash and #eql? in horrible ways
@@ -30,19 +30,16 @@ module CheckPlease
30
30
 
31
31
  def initialize(name = nil)
32
32
  @name = name.to_s.strip
33
- if @name =~ %r(\s) # has any whitespace
34
- raise InvalidPathSegment, <<~EOF
35
- #{name.inspect} is not a valid #{self.class} name
36
- EOF
33
+
34
+ case @name
35
+ when "", /\s/ # blank or has any whitespace
36
+ raise InvalidPathSegment, "#{name.inspect} is not a valid #{self.class} name"
37
37
  end
38
+
38
39
  parse_key_and_value
39
40
  freeze
40
41
  end
41
42
 
42
- def empty?
43
- name.empty?
44
- end
45
-
46
43
  def key_expr?
47
44
  name.match?(KEY_EXPR)
48
45
  end
@@ -52,23 +49,12 @@ module CheckPlease
52
49
  end
53
50
 
54
51
  def match?(other_segment_or_string)
55
- other = self.class.reify(other_segment_or_string)
56
-
57
- match_types = [ self.match_type, other.match_type ]
58
- case match_types
59
- when [ :plain, :plain ] ; self.name == other.name
60
- when [ :key, :key_value ] ; self.key == other.key
61
- when [ :key_value, :key ] ; self.key == other.key
62
- else ; false
63
- end
52
+ other = reify(other_segment_or_string)
53
+ PathSegmentMatcher.call(self, other)
64
54
  end
65
55
 
66
- protected
67
-
68
- def match_type
69
- return :key if key_expr?
70
- return :key_value if key_val_expr?
71
- :plain
56
+ def splat?
57
+ name == '*'
72
58
  end
73
59
 
74
60
  private
@@ -0,0 +1,44 @@
1
+ module CheckPlease
2
+
3
+ class PathSegmentMatcher
4
+ def self.call(a,b)
5
+ new(a,b).call
6
+ end
7
+
8
+ attr_reader :a, :b, :types
9
+ def initialize(a, b)
10
+ @a, @b = a, b
11
+ @types = [ _type(a), _type(b) ].sort
12
+ end
13
+
14
+ def call
15
+ return true if either?(:splat)
16
+ return a.name == b.name if both?(:plain)
17
+ return a.key == b.key if key_and_key_value?
18
+
19
+ false
20
+ end
21
+
22
+ private
23
+
24
+ def _type(x)
25
+ return :splat if x.splat?
26
+ return :key if x.key_expr?
27
+ return :key_value if x.key_val_expr?
28
+ :plain
29
+ end
30
+
31
+ def both?(type)
32
+ types.uniq == [type]
33
+ end
34
+
35
+ def either?(type)
36
+ types.include?(type)
37
+ end
38
+
39
+ def key_and_key_value?
40
+ types == [ :key, :key_value ]
41
+ end
42
+ end
43
+
44
+ end
@@ -1,20 +1,21 @@
1
1
  module CheckPlease
2
- using Refinements
3
2
 
4
3
  module Printers
5
4
  autoload :Base, "check_please/printers/base"
6
5
  autoload :JSON, "check_please/printers/json"
6
+ autoload :Long, "check_please/printers/long"
7
7
  autoload :TablePrint, "check_please/printers/table_print"
8
8
 
9
9
  PRINTERS_BY_FORMAT = {
10
10
  table: Printers::TablePrint,
11
11
  json: Printers::JSON,
12
+ long: Printers::Long,
12
13
  }
13
14
  FORMATS = PRINTERS_BY_FORMAT.keys.sort
14
15
  DEFAULT_FORMAT = :table
15
16
 
16
17
  def self.render(diffs, flags = {})
17
- flags = Flags(flags)
18
+ flags = Flags.reify(flags)
18
19
  printer = PRINTERS_BY_FORMAT[flags.format]
19
20
  printer.render(diffs)
20
21
  end
@@ -0,0 +1,31 @@
1
+ module CheckPlease
2
+ module Printers
3
+
4
+ class Long < Base
5
+ def to_s
6
+ return "" if diffs.empty?
7
+
8
+ out = build_string do |io|
9
+ diffs.each do |diff|
10
+ t = diff.type.to_sym
11
+ ref, can = *[ diff.reference, diff.candidate ].map(&:inspect)
12
+ diff_string = <<~EOF.strip
13
+ #{diff.path} [#{diff.type}]
14
+ reference: #{( t == :extra ) ? "[no value]" : ref}
15
+ candidate: #{( t == :missing ) ? "[no value]" : can}
16
+ EOF
17
+
18
+ io.puts diff_string
19
+ io.puts
20
+ end
21
+ end
22
+
23
+ out.strip
24
+ end
25
+
26
+ private
27
+ end
28
+
29
+ end
30
+ end
31
+
@@ -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.5.0"
4
+ VERSION = "0.5.5" # about to release? rerun `bundle lock` to update Gemfile.lock
5
5
  end
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.5.0
4
+ version: 0.5.5
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: 2021-01-26 00:00:00.000000000 Z
11
+ date: 2021-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: table_print
@@ -87,11 +87,12 @@ files:
87
87
  - lib/check_please/flags.rb
88
88
  - lib/check_please/path.rb
89
89
  - lib/check_please/path_segment.rb
90
+ - lib/check_please/path_segment_matcher.rb
90
91
  - lib/check_please/printers.rb
91
92
  - lib/check_please/printers/base.rb
92
93
  - lib/check_please/printers/json.rb
94
+ - lib/check_please/printers/long.rb
93
95
  - lib/check_please/printers/table_print.rb
94
- - lib/check_please/refinements.rb
95
96
  - lib/check_please/reification.rb
96
97
  - lib/check_please/version.rb
97
98
  - usage_examples.rb
@@ -1,16 +0,0 @@
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