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.
@@ -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
@@ -1,27 +1,172 @@
1
1
  module CheckPlease
2
2
 
3
+ # TODO: this class is getting a bit large; maybe split out some of the stuff that uses flags?
3
4
  class Path
5
+ include CheckPlease::Reification
6
+ can_reify String, Symbol, Numeric, nil
7
+
4
8
  SEPARATOR = "/"
5
9
 
6
- def initialize(segments = [])
10
+ def self.root
11
+ new('/')
12
+ end
13
+
14
+
15
+
16
+ attr_reader :to_s, :segments
17
+ def initialize(name_or_segments = [])
18
+ case name_or_segments
19
+ when String, Symbol, Numeric, nil
20
+ names = name_or_segments.to_s.split(SEPARATOR)
21
+ names.shift until names.empty? || names.first =~ /\S/
22
+ segments = PathSegment.reify(names)
23
+ when Array
24
+ segments = PathSegment.reify(name_or_segments)
25
+ else
26
+ raise InvalidPath, "not sure what to do with #{name_or_segments.inspect}"
27
+ end
28
+
29
+ if segments.any?(&:empty?)
30
+ raise InvalidPath, "#{self.class.name} cannot contain empty segments"
31
+ end
32
+
7
33
  @segments = Array(segments)
34
+
35
+ @to_s = SEPARATOR + @segments.join(SEPARATOR)
36
+ freeze
37
+ rescue InvalidPathSegment => e
38
+ raise InvalidPath, e.message
8
39
  end
9
40
 
10
41
  def +(new_basename)
11
- self.class.new( Array(@segments) + Array(new_basename.to_s) )
42
+ new_segments = self.segments.dup
43
+ new_segments << new_basename # don't reify here; it'll get done on Path#initialize
44
+ self.class.new(new_segments)
45
+ end
46
+
47
+ def ==(other)
48
+ self.to_s == other.to_s
49
+ end
50
+
51
+ def ancestors
52
+ list = []
53
+ p = self
54
+ loop do
55
+ break if p.root?
56
+ p = p.parent
57
+ list.unshift p
58
+ end
59
+ list.reverse
60
+ end
61
+
62
+ def basename
63
+ segments.last.to_s
12
64
  end
13
65
 
14
66
  def depth
15
- 1 + @segments.length
67
+ 1 + segments.length
16
68
  end
17
69
 
18
- def to_s
19
- SEPARATOR + @segments.join(SEPARATOR)
70
+ def excluded?(flags)
71
+ return false if root? # that would just be silly
72
+
73
+ return true if too_deep?(flags)
74
+ return true if explicitly_excluded?(flags)
75
+ return true if implicitly_excluded?(flags)
76
+
77
+ false
20
78
  end
21
79
 
22
80
  def inspect
23
- to_s
81
+ "<#{self.class.name} '#{to_s}'>"
24
82
  end
83
+
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
+ }
91
+
92
+ case matches.length
93
+ when 0 ; nil
94
+ when 1 ; matches.first.segments.last.key
95
+ else ; raise "More than one match_by_key expression for path '#{self}': #{matches.map(&:to_s).inspect}"
96
+ end
97
+ end
98
+
99
+ def match?(path_or_string)
100
+ # If the strings are literally equal, we're good..
101
+ return true if self == path_or_string
102
+
103
+ # Otherwise, compare segments: do we have the same number, and do they all #match?
104
+ other = reify(path_or_string)
105
+ return false if other.depth != self.depth
106
+
107
+ seg_pairs = self.segments.zip(other.segments)
108
+ seg_pairs.all? { |a, b| a.match?(b) }
109
+ end
110
+
111
+ def parent
112
+ return nil if root? # TODO: consider the Null Object pattern
113
+ self.class.new(segments[0..-2])
114
+ end
115
+
116
+ def root?
117
+ @segments.empty?
118
+ end
119
+
120
+ private
121
+
122
+ # O(n^2) check to see if any of the path's ancestors are on a list
123
+ # (as of this writing, this should never actually happen, but I'm being thorough)
124
+ def ancestor_on_list?(paths)
125
+ paths.any? { |path|
126
+ ancestors.any? { |ancestor| ancestor == path }
127
+ }
128
+ end
129
+
130
+ def explicitly_excluded?(flags)
131
+ return false if flags.reject_paths.empty?
132
+ return true if self_on_list?(flags.reject_paths)
133
+ return true if ancestor_on_list?(flags.reject_paths)
134
+ false
135
+ end
136
+
137
+ def implicitly_excluded?(flags)
138
+ return false if flags.select_paths.empty?
139
+ return false if self_on_list?(flags.select_paths)
140
+ return false if ancestor_on_list?(flags.select_paths)
141
+ true
142
+ end
143
+
144
+ # A path of "/foo/:id/bar/:name" has two key expressions:
145
+ # - "/foo/:id"
146
+ # - "/foo/:id/bar/:name"
147
+ def key_exprs
148
+ ( [self] + ancestors )
149
+ .reject { |path| path.root? }
150
+ .select { |path| path.segments.last&.key_expr? }
151
+ end
152
+
153
+ # O(n) check to see if the path itself is on a list
154
+ def self_on_list?(paths)
155
+ paths.any? { |path| self == path }
156
+ end
157
+
158
+ def too_deep?(flags)
159
+ return false if flags.max_depth.nil?
160
+ depth > flags.max_depth
161
+ end
162
+
163
+ def unpack_mbk_exprs(flags)
164
+ flags.match_by_key
165
+ .map { |path| path.send(:key_exprs) }
166
+ .flatten
167
+ .uniq { |e| e.to_s } # use the block form so we don't have to implement #hash and #eql? in horrible ways
168
+ end
169
+
25
170
  end
26
171
 
27
172
  end
@@ -0,0 +1,88 @@
1
+ module CheckPlease
2
+
3
+ class PathSegment
4
+ include CheckPlease::Reification
5
+ can_reify String, Symbol, Numeric, nil
6
+
7
+ KEY_EXPR = %r{
8
+ ^
9
+ \: # a literal colon
10
+ ( # capture key
11
+ [^\:]+ # followed by one or more things that aren't colons
12
+ ) # end capture key
13
+ $
14
+ }x
15
+
16
+ KEY_VAL_EXPR = %r{
17
+ ^
18
+ ( # capture key
19
+ [^=]+ # stuff (just not an equal sign)
20
+ ) # end capture key
21
+ \= # an equal sign
22
+ ( # capture key value
23
+ [^=]+ # stuff (just not an equal sign)
24
+ ) # end capture key value
25
+ $
26
+ }x
27
+
28
+ attr_reader :name, :key, :key_value
29
+ alias_method :to_s, :name
30
+
31
+ def initialize(name = nil)
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
37
+ end
38
+ parse_key_and_value
39
+ freeze
40
+ end
41
+
42
+ def empty?
43
+ name.empty?
44
+ end
45
+
46
+ def key_expr?
47
+ name.match?(KEY_EXPR)
48
+ end
49
+
50
+ def key_val_expr?
51
+ name.match?(KEY_VAL_EXPR)
52
+ end
53
+
54
+ 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
64
+ end
65
+
66
+ protected
67
+
68
+ def match_type
69
+ return :key if key_expr?
70
+ return :key_value if key_val_expr?
71
+ :plain
72
+ end
73
+
74
+ private
75
+
76
+ def parse_key_and_value
77
+ case name
78
+ when KEY_EXPR
79
+ @key = $1
80
+ when KEY_VAL_EXPR
81
+ @key, @key_value = $1, $2
82
+ else
83
+ # :nothingtodohere:
84
+ end
85
+ end
86
+ end
87
+
88
+ end
@@ -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
@@ -4,11 +4,19 @@ 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
@@ -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
@@ -0,0 +1,50 @@
1
+ module CheckPlease
2
+
3
+ module Reification
4
+ def self.included(receiver)
5
+ receiver.extend ClassMethods
6
+ receiver.send :include, InstanceMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def reifiable
11
+ @_reifiable ||= []
12
+ end
13
+
14
+ def can_reify(*klasses)
15
+ klasses.flatten!
16
+
17
+ unless ( klasses - [nil] ).all? { |e| e.is_a?(Class) }
18
+ raise ArgumentError, "classes (or nil) only, please"
19
+ end
20
+
21
+ reifiable.concat klasses
22
+ reifiable.uniq!
23
+ nil
24
+ end
25
+
26
+ def reify(primitive_or_object)
27
+ case primitive_or_object
28
+ when self ; return primitive_or_object
29
+ when Array ; return primitive_or_object.map { |e| reify(e) }
30
+ when *reifiable ; return new(primitive_or_object)
31
+ end
32
+ # note early return ^^^
33
+
34
+ # that didn't work? complain!
35
+ acceptable = reifiable.map { |e| Class === e ? e.name : e.inspect }
36
+ raise ArgumentError, <<~EOF
37
+ #{self}.reify was given: #{primitive_or_object.inspect}
38
+ but only accepts: #{acceptable.join(", ")}
39
+ EOF
40
+ end
41
+ end
42
+
43
+ module InstanceMethods
44
+ def reify(x)
45
+ self.class.reify(x)
46
+ end
47
+ end
48
+ end
49
+
50
+ 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.3"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -1,7 +1,11 @@
1
1
  require 'check_please'
2
2
 
3
- reference = { foo: "wibble" }
4
- candidate = { bar: "wibble" }
3
+ reference = { "foo" => "wibble" }
4
+ candidate = { "bar" => "wibble" }
5
+
6
+
7
+
8
+ ##### Printing diffs #####
5
9
 
6
10
  puts CheckPlease.render_diff(reference, candidate)
7
11
 
@@ -12,3 +16,21 @@ _ = <<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` !
31
+
32
+ # To print these in the console, you can just do:
33
+ puts diffs
34
+
35
+ # If for some reason you want to print the JSON version, it gets a little more verbose:
36
+ puts diffs.to_s(format: :json)