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