mustermann 0.4.0 → 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.
- checksums.yaml +4 -4
- data/README.md +61 -46
- data/lib/mustermann.rb +3 -2
- data/lib/mustermann/ast/compiler.rb +2 -0
- data/lib/mustermann/ast/node.rb +4 -0
- data/lib/mustermann/ast/parser.rb +1 -22
- data/lib/mustermann/ast/pattern.rb +3 -3
- data/lib/mustermann/ast/transformer.rb +53 -4
- data/lib/mustermann/caster.rb +2 -10
- data/lib/mustermann/composite.rb +13 -3
- data/lib/mustermann/concat.rb +124 -0
- data/lib/mustermann/equality_map.rb +60 -0
- data/lib/mustermann/expander.rb +10 -6
- data/lib/mustermann/identity.rb +1 -0
- data/lib/mustermann/pattern.rb +53 -7
- data/lib/mustermann/regexp_based.rb +1 -1
- data/lib/mustermann/regular.rb +17 -2
- data/lib/mustermann/simple_match.rb +18 -5
- data/lib/mustermann/sinatra.rb +64 -16
- data/lib/mustermann/sinatra/parser.rb +45 -0
- data/lib/mustermann/sinatra/safe_renderer.rb +26 -0
- data/lib/mustermann/sinatra/try_convert.rb +48 -0
- data/lib/mustermann/version.rb +1 -1
- data/mustermann.gemspec +1 -2
- data/spec/composite_spec.rb +20 -5
- data/spec/concat_spec.rb +114 -0
- data/spec/equality_map_spec.rb +25 -0
- data/spec/mustermann_spec.rb +8 -5
- data/spec/regular_spec.rb +40 -0
- data/spec/sinatra_spec.rb +85 -5
- metadata +15 -42
- data/lib/mustermann/router.rb +0 -9
- data/lib/mustermann/router/rack.rb +0 -47
- data/lib/mustermann/router/simple.rb +0 -142
- data/spec/router/rack_spec.rb +0 -39
- data/spec/router/simple_spec.rb +0 -32
@@ -0,0 +1,60 @@
|
|
1
|
+
module Mustermann
|
2
|
+
# A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity.
|
3
|
+
# Used for caching. Note that `fetch` is not guaranteed to return the object, even if it has not been
|
4
|
+
# garbage collected yet, especially when used concurrently. Therefore, the block passed to `fetch` has to
|
5
|
+
# be idempotent.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class ExpensiveComputation
|
9
|
+
# @map = Mustermann::EqualityMap.new
|
10
|
+
#
|
11
|
+
# def self.new(*args)
|
12
|
+
# @map.fetch(*args) { super }
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @see #fetch
|
17
|
+
class EqualityMap
|
18
|
+
attr_reader :map
|
19
|
+
|
20
|
+
def self.new
|
21
|
+
defined?(ObjectSpace::WeakMap) ? super : {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@keys = {}
|
26
|
+
@map = ObjectSpace::WeakMap.new
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [Array<#hash>] key for caching
|
30
|
+
# @yield block that will be called to populate entry if missing (has to be idempotent)
|
31
|
+
# @return value stored in map or result of block
|
32
|
+
def fetch(*key)
|
33
|
+
identity = @keys[key.hash]
|
34
|
+
key = identity == key ? identity : key
|
35
|
+
|
36
|
+
# it is ok that this is not thread-safe, worst case it has double cost in
|
37
|
+
# generating, object equality is not guaranteed anyways
|
38
|
+
@map[key] ||= track(key, yield)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param [#hash] key for identifying the object
|
42
|
+
# @param [Object] object to be stored
|
43
|
+
# @return [Object] same as the second parameter
|
44
|
+
def track(key, object)
|
45
|
+
ObjectSpace.define_finalizer(object, finalizer(key.hash))
|
46
|
+
@keys[key.hash] = key
|
47
|
+
object
|
48
|
+
end
|
49
|
+
|
50
|
+
# Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object.
|
51
|
+
#
|
52
|
+
# @param [Fixnum] hash for key
|
53
|
+
# @return [Proc] finalizer callback
|
54
|
+
def finalizer(hash)
|
55
|
+
proc { @keys.delete(hash) }
|
56
|
+
end
|
57
|
+
|
58
|
+
private :track, :finalizer
|
59
|
+
end
|
60
|
+
end
|
data/lib/mustermann/expander.rb
CHANGED
@@ -17,7 +17,7 @@ module Mustermann
|
|
17
17
|
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}.
|
18
18
|
# @param [Symbol] additional_values behavior when encountering additional values, see {#expand}.
|
19
19
|
# @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}.
|
20
|
-
def initialize(*patterns, additional_values: :raise, **options)
|
20
|
+
def initialize(*patterns, additional_values: :raise, **options, &block)
|
21
21
|
unless additional_values == :raise or additional_values == :ignore or additional_values == :append
|
22
22
|
raise ArgumentError, "Illegal value %p for additional_values" % additional_values
|
23
23
|
end
|
@@ -26,8 +26,8 @@ module Mustermann
|
|
26
26
|
@api_expander = AST::Expander.new
|
27
27
|
@additional_values = additional_values
|
28
28
|
@options = options
|
29
|
-
@caster = Caster.new
|
30
|
-
add(*patterns)
|
29
|
+
@caster = Caster.new
|
30
|
+
add(*patterns, &block)
|
31
31
|
end
|
32
32
|
|
33
33
|
# Add patterns to expand.
|
@@ -42,8 +42,12 @@ module Mustermann
|
|
42
42
|
def add(*patterns)
|
43
43
|
patterns.each do |pattern|
|
44
44
|
pattern = Mustermann.new(pattern, **@options)
|
45
|
-
|
46
|
-
|
45
|
+
if block_given?
|
46
|
+
@api_expander.add(yield(pattern))
|
47
|
+
else
|
48
|
+
raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
|
49
|
+
@api_expander.add(pattern.to_ast)
|
50
|
+
end
|
47
51
|
@patterns << pattern
|
48
52
|
end
|
49
53
|
self
|
@@ -196,7 +200,7 @@ module Mustermann
|
|
196
200
|
def map_values(values)
|
197
201
|
values = values.dup
|
198
202
|
@api_expander.keys.each { |key| values[key] ||= values.delete(key.to_s) if values.include? key.to_s }
|
199
|
-
caster.cast(values)
|
203
|
+
caster.cast(values).delete_if { |k, v| v.nil? }
|
200
204
|
end
|
201
205
|
|
202
206
|
private :with_rest, :slice, :append, :caster, :map_values, :split_values
|
data/lib/mustermann/identity.rb
CHANGED
data/lib/mustermann/pattern.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'mustermann/error'
|
2
2
|
require 'mustermann/simple_match'
|
3
|
-
require '
|
3
|
+
require 'mustermann/equality_map'
|
4
4
|
require 'uri'
|
5
5
|
|
6
6
|
module Mustermann
|
@@ -47,16 +47,23 @@ module Mustermann
|
|
47
47
|
# @return [Mustermann::Pattern] a new instance of Mustermann::Pattern
|
48
48
|
# @see #initialize
|
49
49
|
def self.new(string, ignore_unknown_options: false, **options)
|
50
|
-
|
50
|
+
if ignore_unknown_options
|
51
|
+
options = options.select { |key, value| supported?(key, **options) if key != :ignore_unknown_options }
|
52
|
+
else
|
51
53
|
unsupported = options.keys.detect { |key| not supported?(key, **options) }
|
52
54
|
raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
|
53
55
|
end
|
54
56
|
|
55
|
-
@map ||=
|
56
|
-
@map.fetch(string, options) { super(string, options) }
|
57
|
+
@map ||= EqualityMap.new
|
58
|
+
@map.fetch(string, options) { super(string, options) { options } }
|
57
59
|
end
|
58
60
|
|
59
61
|
supported_options :uri_decode, :ignore_unknown_options
|
62
|
+
attr_reader :uri_decode
|
63
|
+
|
64
|
+
# options hash passed to new (with unsupported options removed)
|
65
|
+
# @!visibility private
|
66
|
+
attr_reader :options
|
60
67
|
|
61
68
|
# @overload initialize(string, **options)
|
62
69
|
# @param [String] string the string representation of the pattern
|
@@ -67,6 +74,7 @@ module Mustermann
|
|
67
74
|
def initialize(string, uri_decode: true, **options)
|
68
75
|
@uri_decode = uri_decode
|
69
76
|
@string = string.to_s.dup
|
77
|
+
@options = yield.freeze if block_given?
|
70
78
|
end
|
71
79
|
|
72
80
|
# @return [String] the string representation of the pattern
|
@@ -98,6 +106,26 @@ module Mustermann
|
|
98
106
|
raise NotImplementedError, 'subclass responsibility'
|
99
107
|
end
|
100
108
|
|
109
|
+
# Used by Ruby internally for hashing.
|
110
|
+
# @return [Fixnum] same has value for patterns that are equal
|
111
|
+
def hash
|
112
|
+
self.class.hash | @string.hash | options.hash
|
113
|
+
end
|
114
|
+
|
115
|
+
# Two patterns are considered equal if they are of the same type, have the same pattern string
|
116
|
+
# and the same options.
|
117
|
+
# @return [true, false]
|
118
|
+
def ==(other)
|
119
|
+
other.class == self.class and other.to_s == @string and other.options == options
|
120
|
+
end
|
121
|
+
|
122
|
+
# Two patterns are considered equal if they are of the same type, have the same pattern string
|
123
|
+
# and the same options.
|
124
|
+
# @return [true, false]
|
125
|
+
def eql?(other)
|
126
|
+
other.class.eql?(self.class) and other.to_s.eql?(@string) and other.options.eql?(options)
|
127
|
+
end
|
128
|
+
|
101
129
|
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
|
102
130
|
# Will return the count of the matching characters if it matches.
|
103
131
|
#
|
@@ -234,7 +262,7 @@ module Mustermann
|
|
234
262
|
# pattern |= Mustermann.new('/example/*nested')
|
235
263
|
# pattern.to_templates # => ["/{name}", "/example/{+nested}"]
|
236
264
|
#
|
237
|
-
# Template generation is supported by almost all patterns (notable
|
265
|
+
# Template generation is supported by almost all patterns (notable exceptions are
|
238
266
|
# {Mustermann::Shell}, {Mustermann::Regular} and {Mustermann::Simple}).
|
239
267
|
# Union {Mustermann::Composite} patterns (with the | operator) support template generation
|
240
268
|
# if all patterns they are composed of also support it.
|
@@ -284,12 +312,30 @@ module Mustermann
|
|
284
312
|
# @param [Mustermann::Pattern, String] other the other pattern
|
285
313
|
# @return [Mustermann::Pattern] a composite pattern
|
286
314
|
def |(other)
|
287
|
-
Mustermann.new(self, other, operator: __callee__, type: :identity)
|
315
|
+
Mustermann::Composite.new(self, other, operator: __callee__, type: :identity)
|
288
316
|
end
|
289
317
|
|
290
318
|
alias_method :&, :|
|
291
319
|
alias_method :^, :|
|
292
320
|
|
321
|
+
# @example
|
322
|
+
# require 'mustermann'
|
323
|
+
# prefix = Mustermann.new("/:prefix")
|
324
|
+
# about = prefix + "/about"
|
325
|
+
# about.params("/main/about") # => {"prefix" => "main"}
|
326
|
+
#
|
327
|
+
# Creates a concatenated pattern by combingin self with the other pattern supplied.
|
328
|
+
# Patterns of different types can be mixed. The availability of `to_templates` and
|
329
|
+
# `expand` depends on the patterns being concatenated.
|
330
|
+
#
|
331
|
+
# String input is treated as identity pattern.
|
332
|
+
#
|
333
|
+
# @param [Mustermann::Pattern, String] other pattern to be appended
|
334
|
+
# @return [Mustermann::Pattern] concatenated pattern
|
335
|
+
def +(other)
|
336
|
+
Concat.new(self, other, type: :identity)
|
337
|
+
end
|
338
|
+
|
293
339
|
# @example
|
294
340
|
# pattern = Mustermann.new('/:a/:b')
|
295
341
|
# strings = ["foo/bar", "/foo/bar", "/foo/bar/"]
|
@@ -332,7 +378,7 @@ module Mustermann
|
|
332
378
|
end
|
333
379
|
|
334
380
|
# @!visibility private
|
335
|
-
def unescape(string, decode =
|
381
|
+
def unescape(string, decode = uri_decode)
|
336
382
|
return string unless decode and string
|
337
383
|
@@uri.unescape(string)
|
338
384
|
end
|
data/lib/mustermann/regular.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'mustermann'
|
2
2
|
require 'mustermann/regexp_based'
|
3
|
+
require 'strscan'
|
3
4
|
|
4
5
|
module Mustermann
|
5
6
|
# Regexp pattern implementation.
|
@@ -10,20 +11,34 @@ module Mustermann
|
|
10
11
|
# @see Mustermann::Pattern
|
11
12
|
# @see file:README.md#simple Syntax description in the README
|
12
13
|
class Regular < RegexpBased
|
14
|
+
include Concat::Native
|
13
15
|
register :regexp, :regular
|
16
|
+
supported_options :check_anchors
|
14
17
|
|
15
18
|
# @param (see Mustermann::Pattern#initialize)
|
16
19
|
# @return (see Mustermann::Pattern#initialize)
|
17
20
|
# @see (see Mustermann::Pattern#initialize)
|
18
|
-
def initialize(string, **options)
|
21
|
+
def initialize(string, check_anchors: true, **options)
|
19
22
|
string = $1 if string.to_s =~ /\A\(\?\-mix\:(.*)\)\Z/ && string.inspect == "/#$1/"
|
23
|
+
@check_anchors = check_anchors
|
20
24
|
super(string, **options)
|
21
25
|
end
|
22
26
|
|
23
27
|
def compile(**options)
|
28
|
+
if @check_anchors
|
29
|
+
scanner = ::StringScanner.new(@string)
|
30
|
+
check_anchors(scanner) until scanner.eos?
|
31
|
+
end
|
32
|
+
|
24
33
|
/#{@string}/
|
25
34
|
end
|
26
35
|
|
27
|
-
|
36
|
+
def check_anchors(scanner)
|
37
|
+
return scanner.scan_until(/\]/) if scanner.scan(/\[/)
|
38
|
+
return scanner.scan(/\\?./) unless illegal = scanner.scan(/\\[AzZ]|[\^\$]/)
|
39
|
+
raise CompileError, "regular expression should not contain %s: %p" % [illegal.to_s, @string]
|
40
|
+
end
|
41
|
+
|
42
|
+
private :compile, :check_anchors
|
28
43
|
end
|
29
44
|
end
|
@@ -3,8 +3,10 @@ module Mustermann
|
|
3
3
|
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
|
4
4
|
class SimpleMatch
|
5
5
|
# @api private
|
6
|
-
def initialize(string)
|
7
|
-
@string
|
6
|
+
def initialize(string = "", names: [], captures: [])
|
7
|
+
@string = string.dup
|
8
|
+
@names = names
|
9
|
+
@captures = captures
|
8
10
|
end
|
9
11
|
|
10
12
|
# @return [String] the string that was matched against
|
@@ -14,17 +16,28 @@ module Mustermann
|
|
14
16
|
|
15
17
|
# @return [Array<String>] empty array for imitating MatchData interface
|
16
18
|
def names
|
17
|
-
|
19
|
+
@names.dup
|
18
20
|
end
|
19
21
|
|
20
22
|
# @return [Array<String>] empty array for imitating MatchData interface
|
21
23
|
def captures
|
22
|
-
|
24
|
+
@captures.dup
|
23
25
|
end
|
24
26
|
|
25
27
|
# @return [nil] imitates MatchData interface
|
26
28
|
def [](*args)
|
27
|
-
|
29
|
+
args.map! do |arg|
|
30
|
+
next arg unless arg.is_a? Symbol or arg.is_a? String
|
31
|
+
names.index(arg.to_s)
|
32
|
+
end
|
33
|
+
@captures[*args]
|
34
|
+
end
|
35
|
+
|
36
|
+
# @!visibility private
|
37
|
+
def +(other)
|
38
|
+
SimpleMatch.new(@string + other.to_s,
|
39
|
+
names: @names + other.names,
|
40
|
+
captures: @captures + other.captures)
|
28
41
|
end
|
29
42
|
|
30
43
|
# @return [String] string representation
|
data/lib/mustermann/sinatra.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
require 'mustermann'
|
2
|
+
require 'mustermann/identity'
|
2
3
|
require 'mustermann/ast/pattern'
|
4
|
+
require 'mustermann/sinatra/parser'
|
5
|
+
require 'mustermann/sinatra/safe_renderer'
|
6
|
+
require 'mustermann/sinatra/try_convert'
|
3
7
|
|
4
8
|
module Mustermann
|
5
9
|
# Sinatra 2.0 style pattern implementation.
|
@@ -10,30 +14,74 @@ module Mustermann
|
|
10
14
|
# @see Mustermann::Pattern
|
11
15
|
# @see file:README.md#sinatra Syntax description in the README
|
12
16
|
class Sinatra < AST::Pattern
|
17
|
+
include Concat::Native
|
13
18
|
register :sinatra
|
14
19
|
|
15
|
-
|
20
|
+
# Takes a string and espaces any characters that have special meaning for Sinatra patterns.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# require 'mustermann/sinatra'
|
24
|
+
# Mustermann::Sinatra.escape("/:name") # => "/\\:name"
|
25
|
+
#
|
26
|
+
# @param [#to_s] string the input string
|
27
|
+
# @return [String] the escaped string
|
28
|
+
def self.escape(string)
|
29
|
+
string.to_s.gsub(/[\?\(\)\*:\\\|\{\}]/) { |c| "\\#{c}" }
|
30
|
+
end
|
16
31
|
|
17
|
-
|
18
|
-
|
19
|
-
|
32
|
+
# Tries to convert the given input object to a Sinatra pattern with the given options, without
|
33
|
+
# changing its parsing semantics.
|
34
|
+
# @return [Mustermann::Sinatra, nil] the converted pattern, if possible
|
35
|
+
# @!visibility private
|
36
|
+
def self.try_convert(input, **options)
|
37
|
+
TryConvert.convert(input, **options)
|
38
|
+
end
|
20
39
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
40
|
+
# Creates a pattern that matches any string matching either one of the patterns.
|
41
|
+
# If a string is supplied, it is treated as a fully escaped Sinatra pattern.
|
42
|
+
#
|
43
|
+
# If the other pattern is also a Sintara pattern, it might join the two to a third
|
44
|
+
# sinatra pattern instead of generating a composite for efficency reasons.
|
45
|
+
#
|
46
|
+
# This only happens if the sinatra pattern behaves exactly the same as a composite
|
47
|
+
# would in regards to matching, parsing, expanding and template generation.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second')
|
51
|
+
# pattern === '/foo/bar' # => true
|
52
|
+
# pattern === '/fox/bar' # => true
|
53
|
+
# pattern === '/foo' # => false
|
54
|
+
#
|
55
|
+
# @param [Mustermann::Pattern, String] other the other pattern
|
56
|
+
# @return [Mustermann::Pattern] a composite pattern
|
57
|
+
# @see Mustermann::Pattern#|
|
58
|
+
def |(other)
|
59
|
+
return super unless converted = self.class.try_convert(other, **options)
|
60
|
+
return super unless converted.names.empty? or names.empty?
|
61
|
+
self.class.new(safe_string + "|" + converted.safe_string, **options)
|
25
62
|
end
|
26
63
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
64
|
+
# Generates a string represenation of the pattern that can safely be used for def interpolation
|
65
|
+
# without changing its semantics.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# require 'mustermann'
|
69
|
+
# unsafe = Mustermann.new("/:name")
|
70
|
+
#
|
71
|
+
# Mustermann.new("#{unsafe}bar").params("/foobar") # => { "namebar" => "foobar" }
|
72
|
+
# Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "bar" }
|
73
|
+
#
|
74
|
+
# @return [String] string representatin of the pattern
|
75
|
+
def safe_string
|
76
|
+
@safe_string ||= SafeRenderer.translate(to_ast)
|
33
77
|
end
|
34
78
|
|
35
|
-
|
36
|
-
|
79
|
+
# @!visibility private
|
80
|
+
def native_concat(other)
|
81
|
+
return unless converted = self.class.try_convert(other, **options)
|
82
|
+
safe_string + converted.safe_string
|
37
83
|
end
|
84
|
+
|
85
|
+
private :native_concat
|
38
86
|
end
|
39
87
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Mustermann
|
2
|
+
class Sinatra < AST::Pattern
|
3
|
+
# Sinatra syntax definition.
|
4
|
+
# @!visibility private
|
5
|
+
class Parser < AST::Parser
|
6
|
+
on(nil, ??, ?)) { |c| unexpected(c) }
|
7
|
+
|
8
|
+
on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
|
9
|
+
on(?:) { |c| node(:capture) { scan(/\w+/) } }
|
10
|
+
on(?\\) { |c| node(:char, expect(/./)) }
|
11
|
+
on(?() { |c| node(:group) { read unless scan(?)) } }
|
12
|
+
on(?|) { |c| node(:or) }
|
13
|
+
|
14
|
+
on ?{ do |char|
|
15
|
+
current_pos = buffer.pos
|
16
|
+
type = scan(?+) ? :named_splat : :capture
|
17
|
+
name = expect(/[\w\.]+/)
|
18
|
+
if type == :capture && scan(?|)
|
19
|
+
buffer.pos = current_pos
|
20
|
+
capture = proc do
|
21
|
+
start = pos
|
22
|
+
match = expect(/(?<capture>[^\|}]+)/)
|
23
|
+
node(:capture, match[:capture], start: start)
|
24
|
+
end
|
25
|
+
grouped_captures = node(:group, [capture[]]) do
|
26
|
+
if scan(?|)
|
27
|
+
[min_size(pos - 1, pos, node(:or)), capture[]]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
grouped_captures if expect(?})
|
31
|
+
else
|
32
|
+
type = :splat if type == :named_splat and name == 'splat'
|
33
|
+
expect(?})
|
34
|
+
node(type, name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
suffix ?? do |char, element|
|
39
|
+
node(:optional, element)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private_constant :Parser
|
44
|
+
end
|
45
|
+
end
|