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.
@@ -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
@@ -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
@@ -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