mustermann-contrib 1.0.0.beta2

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