mustermann 0.0.1

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