check_please 0.3.0 → 0.5.2

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