mustermann 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ task(:spec) { ruby '-w -S rspec' }
2
+ task(:doc_stats) { ruby '-S yard stats' }
3
+ task(default: [:spec, :doc_stats])
@@ -0,0 +1,24 @@
1
+ $:.unshift File.expand_path('../lib', __dir__)
2
+
3
+ require 'benchmark'
4
+ require 'mustermann'
5
+ require 'addressable/template'
6
+
7
+ list = [
8
+ /\A\/(?<splat>.*?)\/(?<name>[^\/\?#]+)\Z/,
9
+ Mustermann.new('/*/:name', type: :sinatra),
10
+ Mustermann.new('/*/:name', type: :simple),
11
+ Mustermann.new('/*prefix/:name', type: :rails),
12
+ Mustermann.new('{/prefix*}/{name}', type: :template),
13
+ #Addressable::Template.new('{/prefix*}/{name}')
14
+ ]
15
+
16
+ string = '/a/b/c/d'
17
+
18
+ Benchmark.bmbm do |x|
19
+ list.each do |pattern|
20
+ x.report pattern.class.to_s do
21
+ 100_000.times { pattern.match(string).captures }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ $:.unshift File.expand_path('../lib', __dir__)
2
+
3
+ require 'benchmark'
4
+ require 'mustermann/simple'
5
+ require 'mustermann/sinatra'
6
+
7
+ [Mustermann::Simple, Mustermann::Sinatra].each do |klass|
8
+ puts "", " #{klass} ".center(64, '=')
9
+ Benchmark.bmbm do |x|
10
+ no_capture = klass.new("/simple")
11
+ x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } }
12
+ x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } }
13
+
14
+ simple = klass.new("/:name")
15
+ x.report("simple, match") { 1_000.times { simple.match('/simple').captures } }
16
+ x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } }
17
+
18
+ splat = klass.new("/*")
19
+ x.report("splat, match") { 1_000.times { splat.match("/a/b/c").captures } }
20
+ x.report("splat, miss") { 1_000.times { splat.match("/a/b/c.miss") } }
21
+ end
22
+ puts
23
+ end
@@ -0,0 +1,23 @@
1
+ $:.unshift File.expand_path('../lib', __dir__)
2
+
3
+ require 'benchmark'
4
+ require 'mustermann/template'
5
+ require 'addressable/template'
6
+
7
+ [Mustermann::Template, Addressable::Template].each do |klass|
8
+ puts "", " #{klass} ".center(64, '=')
9
+ Benchmark.bmbm do |x|
10
+ no_capture = klass.new("/simple")
11
+ x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } }
12
+ x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } }
13
+
14
+ simple = klass.new("/{match}")
15
+ x.report("simple, match") { 1_000.times { simple.match('/simple').captures } }
16
+ x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } }
17
+
18
+ explode = klass.new("{/segments*}")
19
+ x.report("explode, match") { 1_000.times { explode.match("/a/b/c").captures } }
20
+ x.report("explode, miss") { 1_000.times { explode.match("/a/b/c.miss") } }
21
+ end
22
+ puts
23
+ end
@@ -0,0 +1,40 @@
1
+ # Namespace and main entry point for the Mustermann library.
2
+ #
3
+ # Under normal circumstences the only external API entry point you should be using is {Mustermann.new}.
4
+ module Mustermann
5
+ # @param [String] string The string represenation of the new pattern
6
+ # @param [Hash] options The options hash
7
+ # @return [Mustermann::Pattern] pattern corresponding to string.
8
+ # @see file:README.md#Types_and_Options "Types and Options" in the README
9
+ def self.new(string, type: :sinatra, **options)
10
+ options.any? ? self[type].new(string, **options) : self[type].new(string)
11
+ end
12
+
13
+ # @!visibility private
14
+ def self.[](key)
15
+ constant, library = register.fetch(key)
16
+ require library if library
17
+ constant.respond_to?(:new) ? constant : register[key] = const_get(constant)
18
+ end
19
+
20
+ # @!visibility private
21
+ def self.register(identifier = nil, constant = identifier.to_s.capitalize, load: "mustermann/#{identifier}")
22
+ @register ||= {}
23
+ @register[identifier] = [constant, load] if identifier
24
+ @register
25
+ end
26
+
27
+ # @!visibility private
28
+ def self.extend_object(object)
29
+ return super unless defined? ::Sinatra::Base and object < ::Sinatra::Base
30
+ require 'mustermann/extension'
31
+ object.register Extension
32
+ end
33
+
34
+ register :identity
35
+ register :rails
36
+ register :shell
37
+ register :simple
38
+ register :sinatra
39
+ register :template
40
+ end
@@ -0,0 +1,403 @@
1
+ require 'mustermann/regexp_based'
2
+ require 'strscan'
3
+
4
+ module Mustermann
5
+ # Superclass for pattern styles that parse an AST from the string pattern.
6
+ # @abstract
7
+ class AST < RegexpBased
8
+ supported_options :capture, :except, :greedy, :space_matches_plus
9
+
10
+ # @!visibility private
11
+ class Node
12
+ # @!visibility private
13
+ attr_accessor :payload
14
+
15
+ # Helper for creating a new instance and calling #parse on it.
16
+ # @return [Node]
17
+ # @!visibility private
18
+ def self.parse(element = new, &block)
19
+ element.tap { |n| n.parse(&block) }
20
+ end
21
+
22
+ # @!visibility private
23
+ def initialize(payload = nil, **options)
24
+ options.each { |key, value| public_send("#{key}=", value) }
25
+ self.payload = payload
26
+ end
27
+
28
+ # Double dispatch helper for reading from the buffer into the payload.
29
+ # @!visibility private
30
+ def parse
31
+ self.payload ||= []
32
+ while element = yield
33
+ payload << element
34
+ end
35
+ end
36
+
37
+ # @return [String] regular expression corresponding to node
38
+ # @!visibility private
39
+ def compile(options)
40
+ Array(payload).map { |e| e.compile(options) }.join
41
+ end
42
+
43
+ # @return [Node] This node after tree transformation. Might be self.
44
+ # @!visibility private
45
+ def transform
46
+ self.payload = payload.transform if payload.respond_to? :transform
47
+ return self unless Array === payload
48
+
49
+ new_payload = []
50
+ with_lookahead = []
51
+
52
+ payload.each do |element|
53
+ element = element.transform
54
+ if with_lookahead.empty?
55
+ list = element.expect_lookahead? ? with_lookahead : new_payload
56
+ list << element
57
+ elsif element.lookahead?
58
+ with_lookahead << element
59
+ else
60
+ with_lookahead = [WithLookAhead.new(with_lookahead, false)] if element.separator? and with_lookahead.size > 1
61
+ new_payload.concat(with_lookahead)
62
+ new_payload << element
63
+ with_lookahead.clear
64
+ end
65
+ end
66
+
67
+ with_lookahead = [WithLookAhead.new(with_lookahead, true)] if with_lookahead.size > 1
68
+ new_payload.concat(with_lookahead)
69
+ @payload = new_payload
70
+ self
71
+ end
72
+
73
+ # @!visibility private
74
+ def separator?
75
+ false
76
+ end
77
+
78
+ # @!visibility private
79
+ def lookahead?(in_lookahead = false)
80
+ false
81
+ end
82
+
83
+ # @!visibility private
84
+ def expect_lookahead?
85
+ false
86
+ end
87
+
88
+ # @return [String] Regular expression for matching the given character in all representations
89
+ # @!visibility private
90
+ def encoded(char, uri_decode, space_matches_plus)
91
+ return Regexp.escape(char) unless uri_decode
92
+ uri_parser = URI::Parser.new
93
+ encoded = uri_parser.escape(char, /./)
94
+ list = [uri_parser.escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
95
+ list << encoded('+', uri_decode, space_matches_plus) if space_matches_plus and char == " "
96
+ "(?:%s)" % list.join("|")
97
+ end
98
+
99
+ # @return [Array<String>]
100
+ # @!visibility private
101
+ def capture_names
102
+ return payload.capture_names if payload.respond_to? :capture_names
103
+ return [] unless payload.respond_to? :map
104
+ payload.map { |e| e.capture_names if e.respond_to? :capture_names }
105
+ end
106
+ end
107
+
108
+ # @!visibility private
109
+ class Char < Node
110
+ # @!visibility private
111
+ def compile(uri_decode: true, space_matches_plus: true, **options)
112
+ encoded(payload, uri_decode, space_matches_plus)
113
+ end
114
+
115
+ # @!visibility private
116
+ def lookahead?(in_lookahead = false)
117
+ in_lookahead
118
+ end
119
+
120
+ # @return [String] regexp to be used in lookahead for semi-greedy capturing
121
+ # @!visibility private
122
+ def lookahead(ahead, options)
123
+ ahead + compile(options)
124
+ end
125
+ end
126
+
127
+ # @!visibility private
128
+ class Separator < Node
129
+ # @!visibility private
130
+ def compile(options)
131
+ Regexp.escape(payload)
132
+ end
133
+
134
+ # @!visibility private
135
+ def separator?
136
+ true
137
+ end
138
+ end
139
+
140
+ # @!visibility private
141
+ class Optional < Node
142
+ # @return [String] regexp to be used in lookahead for semi-greedy capturing
143
+ # @!visibility private
144
+ def lookahead(ahead, options)
145
+ payload.lookahead(ahead, options)
146
+ end
147
+
148
+ # @!visibility private
149
+ def compile(options)
150
+ "(?:%s)?" % payload.compile(options)
151
+ end
152
+
153
+ # @!visibility private
154
+ def lookahead?(in_lookahead = false)
155
+ payload.lookahead? true or payload.expect_lookahead?
156
+ end
157
+ end
158
+
159
+ # @!visibility private
160
+ class Group < Node
161
+ # @!visibility private
162
+ def initialize(payload = nil, **options)
163
+ super(Array(payload), **options)
164
+ end
165
+
166
+ # @!visibility private
167
+ def lookahead?(in_lookahead = false)
168
+ return false unless payload[0..-2].all? { |e| e.lookahead? in_lookahead }
169
+ payload.last.expect_lookahead? or payload.last.lookahead? in_lookahead
170
+ end
171
+
172
+ # @!visibility private
173
+ def transform
174
+ payload.size == 1 ? payload.first.transform : super
175
+ end
176
+
177
+ # @!visibility private
178
+ def parse
179
+ super
180
+ rescue UnexpectedClosingGroup
181
+ end
182
+
183
+ # @return [String] regexp to be used in lookahead for semi-greedy capturing
184
+ # @!visibility private
185
+ def lookahead(ahead, options)
186
+ payload.inject(ahead) { |a,e| e.lookahead(a, options) }
187
+ end
188
+ end
189
+
190
+ # @!visibility private
191
+ class Capture < Node
192
+ # @!visibility private
193
+ def expect_lookahead?
194
+ true
195
+ end
196
+
197
+ # @!visibility private
198
+ def parse
199
+ self.payload ||= ""
200
+ super
201
+ end
202
+
203
+ # @!visibility private
204
+ def capture_names
205
+ [name]
206
+ end
207
+
208
+ # @!visibility private
209
+ def name
210
+ raise CompileError, "capture name can't be empty" if payload.nil? or payload.empty?
211
+ raise CompileError, "capture name must start with underscore or lower case letter" unless payload =~ /^[a-z_]/
212
+ raise CompileError, "capture name can't be #{payload}" if payload == "splat" or payload == "captures"
213
+ payload
214
+ end
215
+
216
+ # @return [String] regexp without the named capture
217
+ # @!visibility private
218
+ def pattern(capture: nil, **options)
219
+ case capture
220
+ when Symbol then from_symbol(capture, **options)
221
+ when Array then from_array(capture, **options)
222
+ when Hash then from_hash(capture, **options)
223
+ when String then from_string(capture, **options)
224
+ when nil then from_nil(**options)
225
+ else capture
226
+ end
227
+ end
228
+
229
+ # @return [String] regexp to be used in lookahead for semi-greedy capturing
230
+ # @!visibility private
231
+ def lookahead(ahead, options)
232
+ ahead + pattern(lookahead: ahead, greedy: false, **options).to_s
233
+ end
234
+
235
+ # @!visibility private
236
+ def compile(options)
237
+ return pattern(options) if options[:no_captures]
238
+ "(?<#{name}>#{compile(no_captures: true, **options)})"
239
+ end
240
+
241
+ private
242
+
243
+ def qualified(string, greedy: true, **options)
244
+ "#{string}+#{?? unless greedy}"
245
+ end
246
+
247
+ def default(**options)
248
+ "[^/\\?#]"
249
+ end
250
+
251
+ def from_nil(**options)
252
+ qualified(with_lookahead(default(**options), **options), **options)
253
+ end
254
+
255
+ def from_hash(hash, **options)
256
+ entry = hash[name.to_sym]
257
+ pattern(capture: entry, **options)
258
+ end
259
+
260
+ def from_array(array, **options)
261
+ array = array.map { |e| pattern(capture: e, **options) }
262
+ Regexp.union(*array)
263
+ end
264
+
265
+ def from_symbol(symbol, **options)
266
+ qualified(with_lookahead("[[:#{symbol}:]]", **options), **options)
267
+ end
268
+
269
+ def from_string(string, uri_decode: true, space_matches_plus: true, **options)
270
+ Regexp.new(string.chars.map { |c| encoded(c, uri_decode, space_matches_plus) }.join)
271
+ end
272
+
273
+ def with_lookahead(string, lookahead: nil, **options)
274
+ return string unless lookahead
275
+ "(?:(?!#{lookahead})#{string})"
276
+ end
277
+ end
278
+
279
+ # @!visibility private
280
+ class Splat < Capture
281
+ # @!visibility private
282
+ def expect_lookahead?
283
+ false
284
+ end
285
+
286
+ # @!visibility private
287
+ def name
288
+ "splat"
289
+ end
290
+
291
+ # @!visibility private
292
+ def pattern(options)
293
+ ".*?"
294
+ end
295
+ end
296
+
297
+ # @!visibility private
298
+ class NamedSplat < Splat
299
+ alias_method :name, :payload
300
+ end
301
+
302
+ # @!visibility private
303
+ class WithLookAhead < Node
304
+ # @!visibility private
305
+ attr_accessor :head, :at_end
306
+
307
+ # @!visibility private
308
+ def initialize(payload, at_end)
309
+ self.head, *self.payload = payload
310
+ self.at_end = at_end
311
+ end
312
+
313
+ # @!visibility private
314
+ def compile(options)
315
+ lookahead = payload.inject('') { |l,e| e.lookahead(l, options) }
316
+ lookahead << (at_end ? '$' : '/')
317
+ head.compile(lookahead: lookahead, **options) + super
318
+ end
319
+ end
320
+
321
+ # @!visibility private
322
+ class Root < Node
323
+ # @!visibility private
324
+ attr_accessor :pattern
325
+
326
+ # @!visibility private
327
+ def self.parse(string, &block)
328
+ root = new
329
+ root.pattern = string
330
+ super(root, &block).transform
331
+ end
332
+
333
+ # @!visibility private
334
+ def capture_names
335
+ super.flatten
336
+ end
337
+
338
+ # @!visibility private
339
+ def check_captures
340
+ names = capture_names
341
+ names.delete("splat")
342
+ raise CompileError, "can't use the same capture name twice" if names.uniq != names
343
+ end
344
+
345
+ # @!visibility private
346
+ def compile(except: nil, **options)
347
+ check_captures
348
+ except &&= "(?!#{except}\\Z)"
349
+ Regexp.new("\\A#{except}#{super(options)}\\Z")
350
+ rescue CompileError => e
351
+ e.message << ": #{pattern.inspect}"
352
+ raise e
353
+ end
354
+ end
355
+
356
+ # @!visibility private
357
+ def parse(string)
358
+ buffer = StringScanner.new(string)
359
+ Root.parse(string) { parse_buffer(buffer) unless buffer.eos? }
360
+ rescue ParseError => e
361
+ e.message << " while parsing #{string.inspect}"
362
+ raise e
363
+ end
364
+
365
+ # @!visibility private
366
+ def compile(string, except: nil, **options)
367
+ options[:except] = compile(except, no_captures: true, **options) if except
368
+ parse(string).compile(options)
369
+ end
370
+
371
+ # @!visibility private
372
+ def parse_buffer(buffer)
373
+ parse_suffix(parse_element(buffer), buffer)
374
+ end
375
+
376
+ # @!visibility private
377
+ def parse_element(buffer)
378
+ raise NotImplementedError, 'subclass responsibility'
379
+ end
380
+
381
+ # @!visibility private
382
+ def parse_suffix(element, buffer)
383
+ element
384
+ end
385
+
386
+ # @!visibility private
387
+ def unexpected(char, exception: ParseError)
388
+ char = char.getch if char.respond_to? :getch
389
+ char = "space" if char == " "
390
+ raise exception, "unexpected #{char || "end of string"}"
391
+ end
392
+
393
+ # @!visibility private
394
+ def expect(buffer, regexp, **options)
395
+ regexp = Regexp.new Regexp.escape(regexp.to_str) unless regexp.is_a? Regexp
396
+ string = buffer.scan(regexp) || unexpected(buffer, **options)
397
+ regexp.names.any? ? regexp.match(string) : string
398
+ end
399
+
400
+ private :parse, :compile, :parse_buffer, :parse_element, :parse_suffix, :unexpected, :expect
401
+ private_constant :Node, :Char, :Separator, :Optional, :Group, :Capture, :Splat, :NamedSplat, :WithLookAhead, :Root
402
+ end
403
+ end