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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.test_queue_stats +0 -0
- data/.travis.yml +6 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +644 -0
- data/Rakefile +3 -0
- data/bench/capturing.rb +24 -0
- data/bench/simple_vs_sinatra.rb +23 -0
- data/bench/template_vs_addressable.rb +23 -0
- data/lib/mustermann.rb +40 -0
- data/lib/mustermann/ast.rb +403 -0
- data/lib/mustermann/error.rb +14 -0
- data/lib/mustermann/extension.rb +52 -0
- data/lib/mustermann/identity.rb +19 -0
- data/lib/mustermann/pattern.rb +142 -0
- data/lib/mustermann/rails.rb +26 -0
- data/lib/mustermann/regexp_based.rb +30 -0
- data/lib/mustermann/shell.rb +22 -0
- data/lib/mustermann/simple.rb +35 -0
- data/lib/mustermann/simple_match.rb +30 -0
- data/lib/mustermann/sinatra.rb +32 -0
- data/lib/mustermann/template.rb +140 -0
- data/lib/mustermann/version.rb +3 -0
- data/mustermann.gemspec +28 -0
- data/spec/ast_spec.rb +8 -0
- data/spec/extension_spec.rb +153 -0
- data/spec/identity_spec.rb +82 -0
- data/spec/mustermann_spec.rb +0 -0
- data/spec/pattern_spec.rb +16 -0
- data/spec/rails_spec.rb +453 -0
- data/spec/regexp_based_spec.rb +8 -0
- data/spec/shell_spec.rb +108 -0
- data/spec/simple_match_spec.rb +10 -0
- data/spec/simple_spec.rb +236 -0
- data/spec/sinatra_spec.rb +554 -0
- data/spec/support.rb +78 -0
- data/spec/template_spec.rb +814 -0
- metadata +227 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
module Mustermann
|
2
|
+
# Raised if anything goes wrong while generating a {Pattern}.
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# Raised if anything goes wrong while compiling a {Pattern}.
|
6
|
+
class CompileError < Error; end
|
7
|
+
|
8
|
+
# Raised if anything goes wrong while parsing a {Pattern}.
|
9
|
+
class ParseError < Error; end
|
10
|
+
|
11
|
+
#@!visibility private
|
12
|
+
class UnexpectedClosingGroup < ParseError; end
|
13
|
+
private_constant :UnexpectedClosingGroup
|
14
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'sinatra/version'
|
2
|
+
fail "no need to load the Mustermann extension for #{::Sinatra::VERSION}" if ::Sinatra::VERSION >= '2.0.0'
|
3
|
+
|
4
|
+
require 'mustermann'
|
5
|
+
|
6
|
+
module Mustermann
|
7
|
+
# Sinatra 1.x extension switching default pattern parsing over to Mustermann.
|
8
|
+
#
|
9
|
+
# @example With classic Sinatra application
|
10
|
+
# require 'sinatra'
|
11
|
+
# require 'mustermann'
|
12
|
+
#
|
13
|
+
# register Mustermann
|
14
|
+
# get('/:id', capture: /\d+/) { ... }
|
15
|
+
#
|
16
|
+
# @example With modular Sinatra application
|
17
|
+
# require 'sinatra/base'
|
18
|
+
# require 'mustermann'
|
19
|
+
#
|
20
|
+
# class MyApp < Sinatra::Base
|
21
|
+
# register Mustermann
|
22
|
+
# get('/:id', capture: /\d+/) { ... }
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @see file:README.md#Sinatra_Integration "Sinatra Integration" in the README
|
26
|
+
module Extension
|
27
|
+
def compile!(verb, path, block, except: nil, capture: nil, pattern: { }, **options)
|
28
|
+
if path.respond_to? :to_str
|
29
|
+
pattern[:except] = except if except
|
30
|
+
pattern[:capture] = capture if capture
|
31
|
+
|
32
|
+
if settings.respond_to? :pattern
|
33
|
+
pattern.merge!(settings.pattern || {}) do |key, local, global|
|
34
|
+
next local unless local.is_a? Hash
|
35
|
+
next global.merge(local) if global.is_a? Hash
|
36
|
+
Hash.new(global).merge! local
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
path = Mustermann.new(path, **pattern)
|
41
|
+
|
42
|
+
if defined? Template and path.is_a? Template
|
43
|
+
condition { params.merge! path.params(request.path_info) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
super(verb, path, block, options)
|
48
|
+
end
|
49
|
+
|
50
|
+
private :compile!
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'mustermann/pattern'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# Matches strings that are identical to the pattern.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Mustermann.new('/:foo', type: :identity) === '/bar' # => false
|
8
|
+
#
|
9
|
+
# @see Pattern
|
10
|
+
# @see file:README.md#identity Syntax description in the README
|
11
|
+
class Identity < Pattern
|
12
|
+
# @param (see Pattern#===)
|
13
|
+
# @return (see Pattern#===)
|
14
|
+
# @see (see Pattern#===)
|
15
|
+
def ===(string)
|
16
|
+
unescape(string) == @string
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'mustermann/error'
|
2
|
+
require 'mustermann/simple_match'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Mustermann
|
6
|
+
# Superclass for all pattern implementations.
|
7
|
+
# @abstract
|
8
|
+
class Pattern
|
9
|
+
# List of supported options.
|
10
|
+
#
|
11
|
+
# @overload supported_options
|
12
|
+
# @return [Array<Symbol>] list of supported options
|
13
|
+
# @overload supported_options(*list)
|
14
|
+
# Adds options to the list.
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
# @param [Symbol] *list adds options to the list of supported options
|
18
|
+
# @return [Array<Symbol>] list of supported options
|
19
|
+
def self.supported_options(*list)
|
20
|
+
@supported_options ||= []
|
21
|
+
options = @supported_options.concat(list)
|
22
|
+
options += superclass.supported_options if self < Pattern
|
23
|
+
options
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param [Symbol] option The option to check.
|
27
|
+
# @return [Boolean] Whether or not option is supported.
|
28
|
+
def self.supported?(option)
|
29
|
+
supported_options.include? option
|
30
|
+
end
|
31
|
+
|
32
|
+
# @overload new(string, **options)
|
33
|
+
# @param (see #initialize)
|
34
|
+
# @raise (see #initialize)
|
35
|
+
# @raise [ArgumentError] if some option is not supported
|
36
|
+
# @return [Pattern] a new instance of Pattern
|
37
|
+
# @see #initialize
|
38
|
+
def self.new(string, ignore_unknown_options: false, **options)
|
39
|
+
unless ignore_unknown_options
|
40
|
+
unsupported = options.keys.detect { |key| not supported?(key) }
|
41
|
+
raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
|
42
|
+
end
|
43
|
+
|
44
|
+
super(string, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
supported_options :uri_decode, :ignore_unknown_options
|
48
|
+
|
49
|
+
# @overload initialize(string, **options)
|
50
|
+
# @param [String] string the string repesentation of the pattern
|
51
|
+
# @param [Hash] options options for fine-tuning the pattern behavior
|
52
|
+
# @raise [Mustermann::Error] if the pattern can't be generated from the string
|
53
|
+
# @see file:README.md#Types_and_Options "Types and Options" in the README
|
54
|
+
# @see Mustermann.new
|
55
|
+
def initialize(string, uri_decode: true, **options)
|
56
|
+
@uri_decode = uri_decode
|
57
|
+
@string = string.dup
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String] the string representation of the pattern
|
61
|
+
def to_s
|
62
|
+
@string.dup
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param [String] string The string to match against
|
66
|
+
# @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
|
67
|
+
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-match Regexp#match
|
68
|
+
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
|
69
|
+
# @see SimpleMatch
|
70
|
+
def match(string)
|
71
|
+
SimpleMatch.new(string) if self === string
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param [String] string The string to match against
|
75
|
+
# @return [Integer, nil] nil if pattern does not match the string, zero if it does.
|
76
|
+
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~
|
77
|
+
def =~(string)
|
78
|
+
0 if self === string
|
79
|
+
end
|
80
|
+
|
81
|
+
# @param [String] string The string to match against
|
82
|
+
# @return [Boolean] Whether or not the pattern matches the given string
|
83
|
+
# @note Needs to be overridden by subclass.
|
84
|
+
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-3D-3D Regexp#===
|
85
|
+
def ===(string)
|
86
|
+
raise NotImplementedError, 'subclass responsibility'
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Array<String>] capture names.
|
90
|
+
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
|
91
|
+
def named_captures
|
92
|
+
{}
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Hash{String}: Array<Integer>] capture names mapped to capture index.
|
96
|
+
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
|
97
|
+
def names
|
98
|
+
[]
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param [String] string the string to match against
|
102
|
+
# @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
|
103
|
+
def params(string = nil, captures: nil)
|
104
|
+
return unless captures ||= match(string)
|
105
|
+
params = named_captures.map do |name, positions|
|
106
|
+
values = positions.map { |pos| map_param(name, captures[pos]) }.flatten
|
107
|
+
values = values.first if values.size < 2 and not always_array? name
|
108
|
+
[name, values]
|
109
|
+
end
|
110
|
+
|
111
|
+
Hash[params]
|
112
|
+
end
|
113
|
+
|
114
|
+
# @!visibility private
|
115
|
+
def inspect
|
116
|
+
"#<%p:%p>" % [self.class, @string]
|
117
|
+
end
|
118
|
+
|
119
|
+
# @!visibility private
|
120
|
+
def map_param(key, value)
|
121
|
+
unescape(value, true)
|
122
|
+
end
|
123
|
+
|
124
|
+
# @!visibility private
|
125
|
+
def unescape(string, decode = @uri_decode)
|
126
|
+
return string unless decode and string
|
127
|
+
@uri ||= URI::Parser.new
|
128
|
+
@uri.unescape(string)
|
129
|
+
end
|
130
|
+
|
131
|
+
# @!visibility private
|
132
|
+
ALWAYS_ARRAY = %w[splat captures]
|
133
|
+
|
134
|
+
# @!visibility private
|
135
|
+
def always_array?(key)
|
136
|
+
ALWAYS_ARRAY.include? key
|
137
|
+
end
|
138
|
+
|
139
|
+
private :unescape, :map_param
|
140
|
+
private_constant :ALWAYS_ARRAY
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'mustermann/ast'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# Rails style pattern implementation.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Mustermann.new('/:foo', type: :rails) === '/bar' # => true
|
8
|
+
#
|
9
|
+
# @see Pattern
|
10
|
+
# @see file:README.md#rails Syntax description in the README
|
11
|
+
class Rails < AST
|
12
|
+
def parse_element(buffer)
|
13
|
+
case char = buffer.getch
|
14
|
+
when nil then unexpected("end of string")
|
15
|
+
when ?) then unexpected(char, exception: UnexpectedClosingGroup)
|
16
|
+
when ?* then NamedSplat.parse { buffer.scan(/\w+/) }
|
17
|
+
when ?/ then Separator.new(char)
|
18
|
+
when ?( then Optional.new(Group.parse { parse_buffer(buffer) })
|
19
|
+
when ?: then Capture.parse { buffer.scan(/\w+/) }
|
20
|
+
else Char.new(char)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private :parse_element
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'mustermann/pattern'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Superclass for patterns that internally compile to a regular expression.
|
6
|
+
# @see Pattern
|
7
|
+
# @abstract
|
8
|
+
class RegexpBased < Pattern
|
9
|
+
# @return [Regexp] regular expression equivalent to the pattern.
|
10
|
+
attr_reader :regexp
|
11
|
+
alias_method :to_regexp, :regexp
|
12
|
+
|
13
|
+
# @param (see Pattern#initialize)
|
14
|
+
# @return (see Pattern#initialize)
|
15
|
+
# @see (see Pattern#initialize)
|
16
|
+
def initialize(string, **options)
|
17
|
+
@regexp = compile(string, **options)
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
extend Forwardable
|
22
|
+
def_delegators :regexp, :===, :=~, :match, :names, :named_captures
|
23
|
+
|
24
|
+
def compile(string, **options)
|
25
|
+
raise NotImplementedError, 'subclass responsibility'
|
26
|
+
end
|
27
|
+
|
28
|
+
private :compile
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'mustermann/pattern'
|
2
|
+
require 'mustermann/simple_match'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Matches strings that are identical to the pattern.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Mustermann.new('/*.*', type: :shell) === '/bar' # => false
|
9
|
+
#
|
10
|
+
# @see Pattern
|
11
|
+
# @see file:README.md#shell Syntax description in the README
|
12
|
+
class Shell < Pattern
|
13
|
+
FLAGS ||= File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
|
14
|
+
|
15
|
+
# @param (see Pattern#===)
|
16
|
+
# @return (see Pattern#===)
|
17
|
+
# @see (see Pattern#===)
|
18
|
+
def ===(string)
|
19
|
+
File.fnmatch? @string, unescape(string), FLAGS
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'mustermann/regexp_based'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# Sinatra 1.3 style pattern implementation.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Mustermann.new('/:foo', type: :simple) === '/bar' # => true
|
8
|
+
#
|
9
|
+
# @see Pattern
|
10
|
+
# @see file:README.md#simple Syntax description in the README
|
11
|
+
class Simple < RegexpBased
|
12
|
+
supported_options :greedy, :space_matches_plus
|
13
|
+
|
14
|
+
def compile(string, greedy: true, uri_decode: true, space_matches_plus: true, **options)
|
15
|
+
pattern = string.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c, uri_decode, space_matches_plus) }
|
16
|
+
pattern.gsub!(/((:\w+)|\*)/) do |match|
|
17
|
+
match == "*" ? "(?<splat>.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})"
|
18
|
+
end
|
19
|
+
/\A#{Regexp.new(pattern)}\Z/
|
20
|
+
rescue SyntaxError, RegexpError => error
|
21
|
+
type = error.message["invalid group name"] ? CompileError : ParseError
|
22
|
+
raise type, error.message, error.backtrace
|
23
|
+
end
|
24
|
+
|
25
|
+
def encoded(char, uri_decode, space_matches_plus)
|
26
|
+
return Regexp.escape(char) unless uri_decode
|
27
|
+
parser = URI::Parser.new
|
28
|
+
encoded = Regexp.union(parser.escape(char), parser.escape(char, /./).downcase, parser.escape(char, /./).upcase)
|
29
|
+
encoded = Regexp.union(encoded, encoded('+', true, true)) if space_matches_plus and char == " "
|
30
|
+
encoded
|
31
|
+
end
|
32
|
+
|
33
|
+
private :compile, :encoded
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Mustermann
|
2
|
+
# Fakes MatchData for patterns that do not support capturing.
|
3
|
+
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
|
4
|
+
class SimpleMatch
|
5
|
+
# @api private
|
6
|
+
def initialize(string)
|
7
|
+
@string = string.dup
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [String] the string that was matched against
|
11
|
+
def to_s
|
12
|
+
@string.dup
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Array<String>] empty array for immitating MatchData interface
|
16
|
+
def names
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Array<String>] empty array for immitating MatchData interface
|
21
|
+
def captures
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [nil] imitates MatchData interface
|
26
|
+
def [](*args)
|
27
|
+
captures[*args]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'mustermann/ast'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# Sinatra 2.0 style pattern implementation.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Mustermann.new('/:foo') === '/bar' # => true
|
8
|
+
#
|
9
|
+
# @see Pattern
|
10
|
+
# @see file:README.md#sinatra Syntax description in the README
|
11
|
+
class Sinatra < AST
|
12
|
+
def parse_element(buffer)
|
13
|
+
case char = buffer.getch
|
14
|
+
when nil, ?? then unexpected(char)
|
15
|
+
when ?) then unexpected(char, exception: UnexpectedClosingGroup)
|
16
|
+
when ?* then Splat.new
|
17
|
+
when ?/ then Separator.new(char)
|
18
|
+
when ?( then Group.parse { parse_buffer(buffer) }
|
19
|
+
when ?: then Capture.parse { buffer.scan(/\w+/) }
|
20
|
+
when ?\\ then Char.new expect(buffer, /./)
|
21
|
+
else Char.new(char)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_suffix(element, buffer)
|
26
|
+
return element unless buffer.scan(/\?/)
|
27
|
+
Optional.new(element)
|
28
|
+
end
|
29
|
+
|
30
|
+
private :parse_element, :parse_suffix
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'mustermann/ast'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# URI template pattern implementation.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Mustermann.new('/{foo}') === '/bar' # => true
|
8
|
+
#
|
9
|
+
# @see Pattern
|
10
|
+
# @see file:README.md#template Syntax description in the README
|
11
|
+
# @see http://tools.ietf.org/html/rfc6570 RFC 6570
|
12
|
+
class Template < AST
|
13
|
+
Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
|
14
|
+
OPERATORS ||= {
|
15
|
+
nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
|
16
|
+
?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
|
17
|
+
?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
|
18
|
+
?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
|
19
|
+
}
|
20
|
+
|
21
|
+
# AST node for template expressions.
|
22
|
+
# @!visibility private
|
23
|
+
class Expression < Group
|
24
|
+
# @!visibility private
|
25
|
+
attr_accessor :operator
|
26
|
+
|
27
|
+
# makes sure we have the proper surrounding characters for the operator
|
28
|
+
# @!visibility private
|
29
|
+
def transform
|
30
|
+
self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
|
31
|
+
new_payload = payload.inject { |list, element| Array(list) << separator << element }
|
32
|
+
@payload = Array(new_payload).map!(&:transform)
|
33
|
+
payload.unshift separator(operator.prefix) if operator.prefix
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# @!visibility private
|
38
|
+
def compile(greedy: true, **options)
|
39
|
+
super(allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
|
40
|
+
parametric: operator.parametric, separator: operator.separator, **options)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @!visibility private
|
44
|
+
def separator(char = operator.separator)
|
45
|
+
AST.const_get(:Separator).new(char) # uhm
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# AST node for template variables.
|
50
|
+
# @!visibility private
|
51
|
+
class Variable < Capture
|
52
|
+
# @!visibility private
|
53
|
+
attr_accessor :prefix, :explode
|
54
|
+
|
55
|
+
# @!visibility private
|
56
|
+
def compile(**options)
|
57
|
+
return super(**options) if explode or not options[:parametric]
|
58
|
+
parametric super(parametric: false, **options)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @!visibility private
|
62
|
+
def pattern(parametric: false, **options)
|
63
|
+
register_param(parametric: parametric, **options)
|
64
|
+
pattern = super(**options)
|
65
|
+
pattern = parametric(pattern) if parametric
|
66
|
+
pattern = "#{pattern}(?:#{Regexp.escape(options.fetch(:separator))}#{pattern})*" if explode
|
67
|
+
pattern
|
68
|
+
end
|
69
|
+
|
70
|
+
# @!visibility private
|
71
|
+
def parametric(string)
|
72
|
+
"#{Regexp.escape(name)}(?:=#{string})?"
|
73
|
+
end
|
74
|
+
|
75
|
+
# @!visibility private
|
76
|
+
def qualified(string, **options)
|
77
|
+
prefix ? "#{string}{1,#{prefix}}" : super(string, **options)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @!visibility private
|
81
|
+
def default(allow_reserved: false, **options)
|
82
|
+
allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]'
|
83
|
+
end
|
84
|
+
|
85
|
+
# @!visibility private
|
86
|
+
def register_param(parametric: false, split_params: nil, separator: nil, **options)
|
87
|
+
return unless explode and split_params
|
88
|
+
split_params[name] = { separator: separator, parametric: parametric }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @!visibility private
|
93
|
+
def parse_element(buffer)
|
94
|
+
parse_expression(buffer) || parse_literal(buffer)
|
95
|
+
end
|
96
|
+
|
97
|
+
# @!visibility private
|
98
|
+
def parse_expression(buffer)
|
99
|
+
return unless buffer.scan(/\{/)
|
100
|
+
operator = buffer.scan(/[\+\#\.\/;\?\&\=\,\!\@\|]/)
|
101
|
+
expression = Expression.new(parse_variable(buffer), operator: operator)
|
102
|
+
expression.parse { parse_variable(buffer) if buffer.scan(/,/) }
|
103
|
+
expression if expect(buffer, ?})
|
104
|
+
end
|
105
|
+
|
106
|
+
# @!visibility private
|
107
|
+
def parse_variable(buffer)
|
108
|
+
match = expect(buffer, /(?<name>\w+)(?:\:(?<prefix>\d{1,4})|(?<explode>\*))?/)
|
109
|
+
Variable.new(match[:name], prefix: match[:prefix], explode: match[:explode])
|
110
|
+
end
|
111
|
+
|
112
|
+
# @!visibility private
|
113
|
+
def parse_literal(buffer)
|
114
|
+
return unless char = buffer.getch
|
115
|
+
raise unexpected(?}) if char == ?}
|
116
|
+
char == ?/ ? Separator.new('/') : Char.new(char)
|
117
|
+
end
|
118
|
+
|
119
|
+
# @!visibility private
|
120
|
+
def compile(*args, **options)
|
121
|
+
@split_params = {}
|
122
|
+
super(*args, split_params: @split_params, **options)
|
123
|
+
end
|
124
|
+
|
125
|
+
# @!visibility private
|
126
|
+
def map_param(key, value)
|
127
|
+
return super unless variable = @split_params[key]
|
128
|
+
value = value.split variable[:separator]
|
129
|
+
value.map! { |e| e.sub(/\A#{key}=/, '') } if variable[:parametric]
|
130
|
+
value.map! { |e| super(key, e) }
|
131
|
+
end
|
132
|
+
|
133
|
+
# @!visibility private
|
134
|
+
def always_array?(key)
|
135
|
+
@split_params.include? key
|
136
|
+
end
|
137
|
+
|
138
|
+
private :parse_element, :parse_expression, :parse_literal, :parse_variable, :map_param, :always_array?
|
139
|
+
end
|
140
|
+
end
|