mustermann 0.4.0 → 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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(Caster::Nil)
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
- raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
46
- @api_expander.add(pattern.to_ast)
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
@@ -11,6 +11,7 @@ module Mustermann
11
11
  # @see Mustermann::Pattern
12
12
  # @see file:README.md#identity Syntax description in the README
13
13
  class Identity < Pattern
14
+ include Concat::Native
14
15
  register :identity
15
16
 
16
17
  # @param (see Mustermann::Pattern#===)
@@ -1,6 +1,6 @@
1
1
  require 'mustermann/error'
2
2
  require 'mustermann/simple_match'
3
- require 'tool/equality_map'
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
- unless ignore_unknown_options
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 ||= Tool::EqualityMap.new
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 execptions are
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 = @uri_decode)
381
+ def unescape(string, decode = uri_decode)
336
382
  return string unless decode and string
337
383
  @@uri.unescape(string)
338
384
  end
@@ -16,7 +16,7 @@ module Mustermann
16
16
  def initialize(string, **options)
17
17
  super
18
18
  regexp = compile(**options)
19
- @peek_regexp = /\A(#{regexp})/
19
+ @peek_regexp = /\A#{regexp}/
20
20
  @regexp = /\A#{regexp}\Z/
21
21
  end
22
22
 
@@ -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
- private :compile
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 = string.dup
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
- captures[*args]
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
@@ -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
- on(nil, ??, ?), ?|) { |c| unexpected(c) }
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
- on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
18
- on(?:) { |c| node(:capture) { scan(/\w+/) } }
19
- on(?\\) { |c| node(:char, expect(/./)) }
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
- on ?( do |char|
22
- groups = []
23
- groups << node(:group) { read unless check(?)) or scan(?|) } until scan(?))
24
- groups.size == 1 ? groups.first : node(:union, groups)
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
- on ?{ do |char|
28
- type = scan(?+) ? :named_splat : :capture
29
- name = expect(/[\w\.]+/)
30
- type = :splat if type == :named_splat and name == 'splat'
31
- expect(?})
32
- node(type, name)
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
- suffix ?? do |char, element|
36
- node(:optional, element)
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