mustermann 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|