mustermann-contrib 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +1239 -0
  3. data/examples/highlighting.rb +35 -0
  4. data/highlighting.png +0 -0
  5. data/irb.png +0 -0
  6. data/lib/mustermann/cake.rb +18 -0
  7. data/lib/mustermann/express.rb +37 -0
  8. data/lib/mustermann/file_utils.rb +217 -0
  9. data/lib/mustermann/file_utils/glob_pattern.rb +39 -0
  10. data/lib/mustermann/fileutils.rb +1 -0
  11. data/lib/mustermann/flask.rb +198 -0
  12. data/lib/mustermann/grape.rb +35 -0
  13. data/lib/mustermann/pyramid.rb +28 -0
  14. data/lib/mustermann/rails.rb +46 -0
  15. data/lib/mustermann/shell.rb +56 -0
  16. data/lib/mustermann/simple.rb +50 -0
  17. data/lib/mustermann/string_scanner.rb +313 -0
  18. data/lib/mustermann/strscan.rb +1 -0
  19. data/lib/mustermann/template.rb +62 -0
  20. data/lib/mustermann/uri_template.rb +1 -0
  21. data/lib/mustermann/versions.rb +46 -0
  22. data/lib/mustermann/visualizer.rb +38 -0
  23. data/lib/mustermann/visualizer/highlight.rb +137 -0
  24. data/lib/mustermann/visualizer/highlighter.rb +37 -0
  25. data/lib/mustermann/visualizer/highlighter/ad_hoc.rb +94 -0
  26. data/lib/mustermann/visualizer/highlighter/ast.rb +102 -0
  27. data/lib/mustermann/visualizer/highlighter/composite.rb +45 -0
  28. data/lib/mustermann/visualizer/highlighter/dummy.rb +18 -0
  29. data/lib/mustermann/visualizer/highlighter/regular.rb +104 -0
  30. data/lib/mustermann/visualizer/pattern_extension.rb +68 -0
  31. data/lib/mustermann/visualizer/renderer/ansi.rb +23 -0
  32. data/lib/mustermann/visualizer/renderer/generic.rb +46 -0
  33. data/lib/mustermann/visualizer/renderer/hansi_template.rb +34 -0
  34. data/lib/mustermann/visualizer/renderer/html.rb +50 -0
  35. data/lib/mustermann/visualizer/renderer/sexp.rb +37 -0
  36. data/lib/mustermann/visualizer/tree.rb +63 -0
  37. data/lib/mustermann/visualizer/tree_renderer.rb +78 -0
  38. data/mustermann-contrib.gemspec +19 -0
  39. data/spec/cake_spec.rb +90 -0
  40. data/spec/express_spec.rb +209 -0
  41. data/spec/file_utils_spec.rb +119 -0
  42. data/spec/flask_spec.rb +361 -0
  43. data/spec/flask_subclass_spec.rb +368 -0
  44. data/spec/grape_spec.rb +747 -0
  45. data/spec/pattern_extension_spec.rb +49 -0
  46. data/spec/pyramid_spec.rb +101 -0
  47. data/spec/rails_spec.rb +647 -0
  48. data/spec/shell_spec.rb +147 -0
  49. data/spec/simple_spec.rb +268 -0
  50. data/spec/string_scanner_spec.rb +271 -0
  51. data/spec/template_spec.rb +841 -0
  52. data/spec/visualizer_spec.rb +199 -0
  53. data/theme.png +0 -0
  54. data/tree.png +0 -0
  55. metadata +126 -0
@@ -0,0 +1,35 @@
1
+ require 'mustermann'
2
+ require 'mustermann/ast/pattern'
3
+
4
+ module Mustermann
5
+ # Grape style pattern implementation.
6
+ #
7
+ # @example
8
+ # Mustermann.new('/:foo', type: :grape) === '/bar' # => true
9
+ #
10
+ # @see Mustermann::Pattern
11
+ # @see file:README.md#grape Syntax description in the README
12
+ class Grape < AST::Pattern
13
+ register :grape
14
+
15
+ on(nil, ??, ?)) { |c| unexpected(c) }
16
+
17
+ on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
18
+ on(?:) { |c| node(:capture, constraint: "[^/\\?#\.]") { scan(/\w+/) } }
19
+ on(?\\) { |c| node(:char, expect(/./)) }
20
+ on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) }
21
+ on(?|) { |c| node(:or) }
22
+
23
+ on ?{ do |char|
24
+ type = scan(?+) ? :named_splat : :capture
25
+ name = expect(/[\w\.]+/)
26
+ type = :splat if type == :named_splat and name == 'splat'
27
+ expect(?})
28
+ node(type, name)
29
+ end
30
+
31
+ suffix ?? do |char, element|
32
+ node(:optional, element)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ require 'mustermann'
2
+ require 'mustermann/ast/pattern'
3
+
4
+ module Mustermann
5
+ # Pyramid style pattern implementation.
6
+ #
7
+ # @example
8
+ # Mustermann.new('/<foo>', type: :pryamid) === '/bar' # => true
9
+ #
10
+ # @see Mustermann::Pattern
11
+ # @see file:README.md#pryamid Syntax description in the README
12
+ class Pyramid < AST::Pattern
13
+ register :pyramid
14
+
15
+ on(nil, ?}) { |c| unexpected(c) }
16
+
17
+ on(?{) do |char|
18
+ name = expect(/\w+/, char: char)
19
+ constraint = read_brackets(?{, ?}) if scan(?:)
20
+ expect(?}) unless constraint
21
+ node(:capture, name, constraint: constraint)
22
+ end
23
+
24
+ on(?*) do |char|
25
+ node(:named_splat, expect(/\w+$/, char: char), convert: -> e { e.split(?/) })
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ require 'mustermann'
2
+ require 'mustermann/ast/pattern'
3
+ require 'mustermann/versions'
4
+
5
+ module Mustermann
6
+ # Rails style pattern implementation.
7
+ #
8
+ # @example
9
+ # Mustermann.new('/:foo', type: :rails) === '/bar' # => true
10
+ #
11
+ # @see Mustermann::Pattern
12
+ # @see file:README.md#rails Syntax description in the README
13
+ class Rails < AST::Pattern
14
+ extend Versions
15
+ register :rails
16
+
17
+ # first parser, no optional parts
18
+ version('2.3') do
19
+ on(nil) { |c| unexpected(c) }
20
+ on(?*) { |c| node(:named_splat) { scan(/\w+/) } }
21
+ on(?:) { |c| node(:capture) { scan(/\w+/) } }
22
+ end
23
+
24
+ # rack-mount
25
+ version('3.0', '3.1') do
26
+ on(?)) { |c| unexpected(c) }
27
+ on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) }
28
+ on(?\\) { |c| node(:char, expect(/./)) }
29
+ end
30
+
31
+ # stand-alone journey
32
+ version('3.2') do
33
+ on(?|) { |c| raise ParseError, "the implementation of | is broken in ActionDispatch, cannot compile compatible pattern" }
34
+ on(?\\) { |c| node(:char, c) }
35
+ end
36
+
37
+ # embedded journey, broken (ignored) escapes
38
+ version('4.0', '4.1') { on(?\\) { |c| read } }
39
+
40
+ # escapes got fixed in 4.2
41
+ version('4.2') { on(?\\) { |c| node(:char, expect(/./)) } }
42
+
43
+ # Rails 5.0 fixes |
44
+ version('5.0') { on(?|) { |c| node(:or) }}
45
+ end
46
+ end
@@ -0,0 +1,56 @@
1
+ require 'mustermann'
2
+ require 'mustermann/pattern'
3
+ require 'mustermann/simple_match'
4
+
5
+ module Mustermann
6
+ # Matches strings that are identical to the pattern.
7
+ #
8
+ # @example
9
+ # Mustermann.new('/*.*', type: :shell) === '/bar' # => false
10
+ #
11
+ # @see Mustermann::Pattern
12
+ # @see file:README.md#shell Syntax description in the README
13
+ class Shell < Pattern
14
+ include Concat::Native
15
+ register :shell
16
+
17
+ # @!visibility private
18
+ # @return [#highlight, nil]
19
+ # highlighing logic for mustermann-visualizer,
20
+ # nil if mustermann-visualizer hasn't been loaded
21
+ def highlighter
22
+ return unless defined? Mustermann::Visualizer::Highlighter
23
+ @@highlighter ||= Mustermann::Visualizer::Highlighter.create do
24
+ on('\\') { |matched| escaped(matched, scanner.getch) }
25
+ on(/[\*\[\]]/, :special)
26
+ on("{") { nested(:union, ?{, ?}, ?,) }
27
+ end
28
+ end
29
+
30
+ # @param (see Mustermann::Pattern#initialize)
31
+ # @return (see Mustermann::Pattern#initialize)
32
+ # @see (see Mustermann::Pattern#initialize)
33
+ def initialize(string, **options)
34
+ @flags = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
35
+ super(string, **options)
36
+ end
37
+
38
+ # @param (see Mustermann::Pattern#===)
39
+ # @return (see Mustermann::Pattern#===)
40
+ # @see (see Mustermann::Pattern#===)
41
+ def ===(string)
42
+ File.fnmatch? @string, unescape(string), @flags
43
+ end
44
+
45
+ # @param (see Mustermann::Pattern#peek_size)
46
+ # @return (see Mustermann::Pattern#peek_size)
47
+ # @see (see Mustermann::Pattern#peek_size)
48
+ def peek_size(string)
49
+ @peek_string ||= @string + "{**,/**,/**/*}"
50
+ super if File.fnmatch? @peek_string, unescape(string), @flags
51
+ end
52
+
53
+ # Used by {Mustermann::FileUtils} to not use a generic glob pattern.
54
+ alias_method :to_glob, :to_s
55
+ end
56
+ end
@@ -0,0 +1,50 @@
1
+ require 'mustermann'
2
+ require 'mustermann/regexp_based'
3
+
4
+ module Mustermann
5
+ # Sinatra 1.3 style pattern implementation.
6
+ #
7
+ # @example
8
+ # Mustermann.new('/:foo', type: :simple) === '/bar' # => true
9
+ #
10
+ # @see Mustermann::Pattern
11
+ # @see file:README.md#simple Syntax description in the README
12
+ class Simple < RegexpBased
13
+ register :simple
14
+ supported_options :greedy, :space_matches_plus
15
+ instance_delegate highlighter: 'self.class'
16
+
17
+ # @!visibility private
18
+ # @return [#highlight, nil]
19
+ # highlighing logic for mustermann-visualizer,
20
+ # nil if mustermann-visualizer hasn't been loaded
21
+ def self.highlighter
22
+ return unless defined? Mustermann::Visualizer::Highlighter
23
+ @highlighter ||= Mustermann::Visualizer::Highlighter.create do
24
+ on(/:(\w+)/) { |matched| element(:capture, ':') { element(:name, matched[1..-1]) } }
25
+ on("*" => :splat, "?" => :optional)
26
+ end
27
+ end
28
+
29
+ def compile(greedy: true, uri_decode: true, space_matches_plus: true, **options)
30
+ pattern = @string.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c, uri_decode, space_matches_plus) }
31
+ pattern.gsub!(/((:\w+)|\*)/) do |match|
32
+ match == "*" ? "(?<splat>.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})"
33
+ end
34
+ Regexp.new(pattern)
35
+ rescue SyntaxError, RegexpError => error
36
+ type = error.message["invalid group name"] ? CompileError : ParseError
37
+ raise type, error.message, error.backtrace
38
+ end
39
+
40
+ def encoded(char, uri_decode, space_matches_plus)
41
+ return Regexp.escape(char) unless uri_decode
42
+ parser = URI::Parser.new
43
+ encoded = Regexp.union(parser.escape(char), parser.escape(char, /./).downcase, parser.escape(char, /./).upcase)
44
+ encoded = Regexp.union(encoded, encoded('+', true, true)) if space_matches_plus and char == " "
45
+ encoded
46
+ end
47
+
48
+ private :compile, :encoded
49
+ end
50
+ end
@@ -0,0 +1,313 @@
1
+ require 'mustermann'
2
+ require 'mustermann/pattern_cache'
3
+ require 'delegate'
4
+
5
+ module Mustermann
6
+ # Class inspired by Ruby's StringScanner to scan an input string using multiple patterns.
7
+ #
8
+ # @example
9
+ # require 'mustermann/string_scanner'
10
+ # scanner = Mustermann::StringScanner.new("here is our example string")
11
+ #
12
+ # scanner.scan("here") # => "here"
13
+ # scanner.getch # => " "
14
+ #
15
+ # if scanner.scan(":verb our")
16
+ # scanner.scan(:noun, capture: :word)
17
+ # scanner[:verb] # => "is"
18
+ # scanner[:nound] # => "example"
19
+ # end
20
+ #
21
+ # scanner.rest # => "string"
22
+ #
23
+ # @note
24
+ # This structure is not thread-safe, you should not scan on the same StringScanner instance concurrently.
25
+ # Even if it was thread-safe, scanning concurrently would probably lead to unwanted behaviour.
26
+ class StringScanner
27
+ # Exception raised if scan/unscan operation cannot be performed.
28
+ ScanError = Class.new(::ScanError)
29
+ PATTERN_CACHE = PatternCache.new
30
+ private_constant :PATTERN_CACHE
31
+
32
+ # Patterns created by {#scan} will be globally cached, since we assume that there is a finite number
33
+ # of different patterns used and that they are more likely to be reused than not.
34
+ # This method allows clearing the cache.
35
+ #
36
+ # @see Mustermann::PatternCache
37
+ def self.clear_cache
38
+ PATTERN_CACHE.clear
39
+ end
40
+
41
+ # @return [Integer] number of cached patterns
42
+ # @see clear_cache
43
+ # @api private
44
+ def self.cache_size
45
+ PATTERN_CACHE.size
46
+ end
47
+
48
+ # Encapsulates return values for {StringScanner#scan}, {StringScanner#check}, and friends.
49
+ # Behaves like a String (the substring which matched the pattern), but also exposes its position
50
+ # in the main string and any params parsed from it.
51
+ class ScanResult < DelegateClass(String)
52
+ # The scanner this result came from.
53
+ # @example
54
+ # require 'mustermann/string_scanner'
55
+ # scanner = Mustermann::StringScanner.new('foo/bar')
56
+ # scanner.scan(:name).scanner == scanner # => true
57
+ attr_reader :scanner
58
+
59
+ # @example
60
+ # require 'mustermann/string_scanner'
61
+ # scanner = Mustermann::StringScanner.new('foo/bar')
62
+ # scanner.scan(:name).position # => 0
63
+ # scanner.getch.position # => 3
64
+ # scanner.scan(:name).position # => 4
65
+ #
66
+ # @return [Integer] position the substring starts at
67
+ attr_reader :position
68
+ alias_method :pos, :position
69
+
70
+ # @example
71
+ # require 'mustermann/string_scanner'
72
+ # scanner = Mustermann::StringScanner.new('foo/bar')
73
+ # scanner.scan(:name).length # => 3
74
+ # scanner.getch.length # => 1
75
+ # scanner.scan(:name).length # => 3
76
+ #
77
+ # @return [Integer] length of the substring
78
+ attr_reader :length
79
+
80
+ # Params parsed from the substring.
81
+ # Will not include params from previous scan results.
82
+ #
83
+ # @example
84
+ # require 'mustermann/string_scanner'
85
+ # scanner = Mustermann::StringScanner.new('foo/bar')
86
+ # scanner.scan(:name).params # => { "name" => "foo" }
87
+ # scanner.getch.params # => {}
88
+ # scanner.scan(:name).params # => { "name" => "bar" }
89
+ #
90
+ # @see Mustermann::StringScanner#params
91
+ # @see Mustermann::StringScanner#[]
92
+ #
93
+ # @return [Hash] params parsed from the substring
94
+ attr_reader :params
95
+
96
+ # @api private
97
+ def initialize(scanner, position, length, params = {})
98
+ @scanner, @position, @length, @params = scanner, position, length, params
99
+ end
100
+
101
+ # @api private
102
+ # @!visibility private
103
+ def __getobj__
104
+ @__getobj__ ||= scanner.to_s[position, length]
105
+ end
106
+ end
107
+
108
+ # @return [Hash] default pattern options used for {#scan} and similar methods
109
+ # @see #initialize
110
+ attr_reader :pattern_options
111
+
112
+ # Params from all previous matches from {#scan} and {#scan_until},
113
+ # but not from {#check} and {#check_until}. Changes can be reverted
114
+ # with {#unscan} and it can be completely cleared via {#reset}.
115
+ #
116
+ # @return [Hash] current params
117
+ attr_reader :params
118
+
119
+ # @return [Integer] current scan position on the input string
120
+ attr_accessor :position
121
+ alias_method :pos, :position
122
+ alias_method :pos=, :position=
123
+
124
+ # @example with different default type
125
+ # require 'mustermann/string_scanner'
126
+ # scanner = Mustermann::StringScanner.new("foo/bar/baz", type: :shell)
127
+ # scanner.scan('*') # => "foo"
128
+ # scanner.scan('**/*') # => "/bar/baz"
129
+ #
130
+ # @param [String] string the string to scan
131
+ # @param [Hash] pattern_options default options used for {#scan}
132
+ def initialize(string = "", **pattern_options)
133
+ @pattern_options = pattern_options
134
+ @string = String(string).dup
135
+ reset
136
+ end
137
+
138
+ # Resets the {#position} to the start and clears all {#params}.
139
+ # @return [Mustermann::StringScanner] the scanner itself
140
+ def reset
141
+ @position = 0
142
+ @params = {}
143
+ @history = []
144
+ self
145
+ end
146
+
147
+ # Moves the position to the end of the input string.
148
+ # @return [Mustermann::StringScanner] the scanner itself
149
+ def terminate
150
+ track_result ScanResult.new(self, @position, size - @position)
151
+ self
152
+ end
153
+
154
+ # Checks if the given pattern matches any substring starting at the current position.
155
+ #
156
+ # If it does, it will advance the current {#position} to the end of the substring and merges any params parsed
157
+ # from the substring into {#params}.
158
+ #
159
+ # @param (see Mustermann.new)
160
+ # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
161
+ def scan(pattern, **options)
162
+ track_result check(pattern, **options)
163
+ end
164
+
165
+ # Checks if the given pattern matches any substring starting at any position after the current position.
166
+ #
167
+ # If it does, it will advance the current {#position} to the end of the substring and merges any params parsed
168
+ # from the substring into {#params}.
169
+ #
170
+ # @param (see Mustermann.new)
171
+ # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
172
+ def scan_until(pattern, **options)
173
+ result, prefix = check_until_with_prefix(pattern, **options)
174
+ track_result(prefix, result)
175
+ end
176
+
177
+ # Reverts the last operation that advanced the position.
178
+ #
179
+ # Operations advancing the position: {#terminate}, {#scan}, {#scan_until}, {#getch}.
180
+ # @return [Mustermann::StringScanner] the scanner itself
181
+ def unscan
182
+ raise ScanError, 'unscan failed: previous match record not exist' if @history.empty?
183
+ previous = @history[0..-2]
184
+ reset
185
+ previous.each { |r| track_result(*r) }
186
+ self
187
+ end
188
+
189
+ # Checks if the given pattern matches any substring starting at the current position.
190
+ #
191
+ # Does not affect {#position} or {#params}.
192
+ #
193
+ # @param (see Mustermann.new)
194
+ # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
195
+ def check(pattern, **options)
196
+ params, length = create_pattern(pattern, **options).peek_params(rest)
197
+ ScanResult.new(self, @position, length, params) if params
198
+ end
199
+
200
+ # Checks if the given pattern matches any substring starting at any position after the current position.
201
+ #
202
+ # Does not affect {#position} or {#params}.
203
+ #
204
+ # @param (see Mustermann.new)
205
+ # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
206
+ def check_until(pattern, **options)
207
+ check_until_with_prefix(pattern, **options).first
208
+ end
209
+
210
+ def check_until_with_prefix(pattern, **options)
211
+ start = @position
212
+ @position += 1 until eos? or result = check(pattern, **options)
213
+ prefix = ScanResult.new(self, start, @position - start) if result
214
+ [result, prefix]
215
+ ensure
216
+ @position = start
217
+ end
218
+
219
+ # Reads a single character and advances the {#position} by one.
220
+ # @return [Mustermann::StringScanner::ScanResult, nil] the character, nil if at end of string
221
+ def getch
222
+ track_result ScanResult.new(self, @position, 1) unless eos?
223
+ end
224
+
225
+ # Appends the given string to the string being scanned
226
+ #
227
+ # @example
228
+ # require 'mustermann/string_scanner'
229
+ # scanner = Mustermann::StringScanner.new
230
+ # scanner << "foo"
231
+ # scanner.scan(/.+/) # => "foo"
232
+ #
233
+ # @param [String] string will be appended
234
+ # @return [Mustermann::StringScanner] the scanner itself
235
+ def <<(string)
236
+ @string << string
237
+ self
238
+ end
239
+
240
+ # @return [true, false] whether or not the end of the string has been reached
241
+ def eos?
242
+ @position >= @string.size
243
+ end
244
+
245
+ # @return [true, false] whether or not the current position is at the start of a line
246
+ def beginning_of_line?
247
+ @position == 0 or @string[@position - 1] == "\n"
248
+ end
249
+
250
+ # @return [String] outstanding string not yet matched, empty string at end of input string
251
+ def rest
252
+ @string[@position..-1] || ""
253
+ end
254
+
255
+ # @return [Integer] number of character remaining to be scanned
256
+ def rest_size
257
+ @position > size ? 0 : size - @position
258
+ end
259
+
260
+ # Allows to peek at a number of still unscanned characters without advacing the {#position}.
261
+ #
262
+ # @param [Integer] length how many characters to look at
263
+ # @return [String] the substring
264
+ def peek(length = 1)
265
+ @string[@position, length]
266
+ end
267
+
268
+ # Shorthand for accessing {#params}. Accepts symbols as keys.
269
+ def [](key)
270
+ params[key.to_s]
271
+ end
272
+
273
+ # (see #params)
274
+ def to_h
275
+ params.dup
276
+ end
277
+
278
+ # @return [String] the input string
279
+ # @see #initialize
280
+ # @see #<<
281
+ def to_s
282
+ @string.dup
283
+ end
284
+
285
+ # @return [Integer] size of the input string
286
+ def size
287
+ @string.size
288
+ end
289
+
290
+ # @!visibility private
291
+ def inspect
292
+ "#<%p %d/%d @ %p>" % [ self.class, @position, @string.size, @string ]
293
+ end
294
+
295
+ # @!visibility private
296
+ def create_pattern(pattern, **options)
297
+ PATTERN_CACHE.create_pattern(pattern, **options, **pattern_options)
298
+ end
299
+
300
+ # @!visibility private
301
+ def track_result(*results)
302
+ results.compact!
303
+ @history << results if results.any?
304
+ results.each do |result|
305
+ @params.merge! result.params
306
+ @position += result.length
307
+ end
308
+ results.last
309
+ end
310
+
311
+ private :create_pattern, :track_result, :check_until_with_prefix
312
+ end
313
+ end