mustermann 3.1.0 → 4.0.0.alpha
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/LICENSE +1 -2
- data/README.md +96 -259
- data/lib/mustermann/ast/compiler.rb +124 -29
- data/lib/mustermann/ast/pattern.rb +10 -0
- data/lib/mustermann/ast/translator.rb +7 -2
- data/lib/mustermann/concat.rb +9 -3
- data/lib/mustermann/error.rb +1 -0
- data/lib/mustermann/expander.rb +8 -3
- data/lib/mustermann/hybrid.rb +50 -0
- data/lib/mustermann/match.rb +39 -0
- data/lib/mustermann/pattern.rb +9 -32
- data/lib/mustermann/rails.rb +1 -1
- data/lib/mustermann/regexp_based.rb +22 -7
- data/lib/mustermann/router.rb +99 -0
- data/lib/mustermann/set/cache.rb +30 -0
- data/lib/mustermann/set/linear.rb +30 -0
- data/lib/mustermann/set/match.rb +16 -0
- data/lib/mustermann/set/trie.rb +173 -0
- data/lib/mustermann/set.rb +327 -0
- data/lib/mustermann/sinatra/safe_renderer.rb +1 -1
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann.rb +0 -15
- metadata +24 -9
- data/lib/mustermann/extension.rb +0 -3
- data/lib/mustermann/mapper.rb +0 -91
- data/lib/mustermann/pattern_cache.rb +0 -50
- data/lib/mustermann/simple_match.rb +0 -49
- data/lib/mustermann/to_pattern.rb +0 -51
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'mustermann/ast/translator'
|
|
3
4
|
|
|
4
5
|
module Mustermann
|
|
@@ -9,28 +10,59 @@ module Mustermann
|
|
|
9
10
|
class Compiler < Translator
|
|
10
11
|
raises CompileError
|
|
11
12
|
|
|
12
|
-
#
|
|
13
|
-
|
|
13
|
+
# Compile an array of AST nodes, detecting which captures can safely use
|
|
14
|
+
# atomic groups. A capture is safe to atomicize when its very next sibling
|
|
15
|
+
# is a *path separator* (payload '/'), because every Mustermann capture
|
|
16
|
+
# character class (Sinatra's [^\/\?#]+, Template's [\w\-\.~%]+, etc.)
|
|
17
|
+
# excludes '/', so the greedy match naturally stops before '/' and
|
|
18
|
+
# committing to it atomically never affects correctness.
|
|
19
|
+
#
|
|
20
|
+
# More permissive conditions (e.g. end-of-array) are intentionally avoided:
|
|
21
|
+
# template expressions nest captures inside inner arrays where end-of-array
|
|
22
|
+
# does NOT mean end-of-pattern, and non-'/' separators (e.g. '.' in
|
|
23
|
+
# {.a,b,c}) may appear inside the capture character class.
|
|
24
|
+
#
|
|
25
|
+
# Splats (.*?) and non-greedy captures are excluded — atomicizing them
|
|
26
|
+
# would commit to zero or one character and break match resolution.
|
|
27
|
+
# Strip `atomic:` from incoming options so a parent's value cannot bleed
|
|
28
|
+
# into siblings; each element's atomicity comes solely from its own context.
|
|
29
|
+
translate(Array) do |atomic: false, **options|
|
|
30
|
+
greedy = options.fetch(:greedy, true)
|
|
31
|
+
each_with_index.map do |element, index|
|
|
32
|
+
next_sibling = self[index + 1]
|
|
33
|
+
atomic = greedy &&
|
|
34
|
+
element.is_a?(:capture) &&
|
|
35
|
+
!element.is_a?(:splat) &&
|
|
36
|
+
next_sibling&.is_a?(:separator) &&
|
|
37
|
+
next_sibling.payload == '/'
|
|
38
|
+
t(element, **options, atomic: atomic)
|
|
39
|
+
end.join
|
|
40
|
+
end
|
|
41
|
+
|
|
14
42
|
translate(:node) { |**o| t(payload, **o) }
|
|
15
43
|
translate(:separator) { |**o| Regexp.escape(payload) }
|
|
16
|
-
translate(:optional) { |**o|
|
|
44
|
+
translate(:optional) { |**o| '(?:%s)?' % t(payload, **o) }
|
|
17
45
|
translate(:char) { |**o| t.encoded(payload, **o) }
|
|
18
46
|
|
|
19
47
|
translate :union do |**options|
|
|
20
|
-
|
|
48
|
+
'(?:%s)' % payload.map { |e| '(?:%s)' % t(e, **options) }.join('|')
|
|
21
49
|
end
|
|
22
50
|
|
|
23
51
|
translate :expression do |greedy: true, **options|
|
|
24
52
|
t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
|
|
25
|
-
|
|
53
|
+
parametric: operator.parametric, separator: operator.separator, **options)
|
|
26
54
|
end
|
|
27
55
|
|
|
28
|
-
translate :with_look_ahead do
|
|
29
|
-
|
|
56
|
+
translate :with_look_ahead do |atomic: false, **options|
|
|
57
|
+
greedy = options.fetch(:greedy, true)
|
|
58
|
+
lookahead = each_leaf.inject('') do |ahead, element|
|
|
30
59
|
ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s
|
|
31
60
|
end
|
|
32
61
|
lookahead << (at_end ? '$' : '/')
|
|
33
|
-
|
|
62
|
+
# The look-ahead already constrains what the head capture can match, so
|
|
63
|
+
# it is safe to make it atomic when greedy. Non-greedy captures rely on
|
|
64
|
+
# backtracking to extend their match and must not be committed atomically.
|
|
65
|
+
t(head, **options, lookahead: lookahead, atomic: greedy) + t(payload, **options)
|
|
34
66
|
end
|
|
35
67
|
|
|
36
68
|
# Capture compilation is complex. :(
|
|
@@ -39,9 +71,23 @@ module Mustermann
|
|
|
39
71
|
register :capture
|
|
40
72
|
|
|
41
73
|
# @!visibility private
|
|
42
|
-
|
|
74
|
+
# When +atomic: true+ is passed (set by the Array translator for captures
|
|
75
|
+
# that are followed only by a separator or end-of-pattern), the compiled
|
|
76
|
+
# content is wrapped in an atomic group <tt>(?>…)</tt>. This prevents
|
|
77
|
+
# Oniguruma from backtracking into characters the capture has already
|
|
78
|
+
# consumed, giving a measurable speedup on failing matches without
|
|
79
|
+
# changing the result for any valid input.
|
|
80
|
+
def translate(atomic: false, **options)
|
|
43
81
|
return pattern(**options) if options[:no_captures]
|
|
44
|
-
|
|
82
|
+
|
|
83
|
+
inner = translate(no_captures: true, **options)
|
|
84
|
+
# Atomic groups are only safe for pure character-class repetitions.
|
|
85
|
+
# Captures with an explicit array/hash/string option or a custom
|
|
86
|
+
# constraint produce alternations that need backtracking to resolve
|
|
87
|
+
# the correct alternative, so they must not be wrapped atomically.
|
|
88
|
+
apply_atomic = atomic && options[:capture].nil? && constraint.nil?
|
|
89
|
+
content = apply_atomic ? "(?>#{inner})" : inner
|
|
90
|
+
"(?<#{name}>#{content})"
|
|
45
91
|
end
|
|
46
92
|
|
|
47
93
|
# @return [String] regexp without the named capture
|
|
@@ -58,14 +104,44 @@ module Mustermann
|
|
|
58
104
|
end
|
|
59
105
|
|
|
60
106
|
private
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
|
|
108
|
+
def qualified(string, greedy: true,
|
|
109
|
+
**options) "#{string}#{qualifier || "+#{'?' unless greedy}"}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def with_lookahead(string, lookahead: nil,
|
|
113
|
+
**options) lookahead ? "(?:(?!#{lookahead})#{string})" : string
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def from_hash(hash,
|
|
117
|
+
**options) pattern(capture: hash[name.to_sym],
|
|
118
|
+
**options)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def from_array(array, **options)
|
|
122
|
+
Regexp.union(*array.map do |e|
|
|
123
|
+
pattern(capture: e, **options)
|
|
124
|
+
end)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def from_symbol(symbol,
|
|
128
|
+
**options) qualified(with_lookahead("[[:#{symbol}:]]", **options),
|
|
129
|
+
**options)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def from_string(string, **options)
|
|
133
|
+
Regexp.new(string.chars.map do |c|
|
|
134
|
+
t.encoded(c, **options)
|
|
135
|
+
end.join)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def from_nil(**options)
|
|
139
|
+
qualified(
|
|
140
|
+
with_lookahead(default(**options), **options), **options
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def default(**options) = constraint || '[^/\\?#]'
|
|
69
145
|
end
|
|
70
146
|
|
|
71
147
|
# @!visibility private
|
|
@@ -74,7 +150,7 @@ module Mustermann
|
|
|
74
150
|
# splats are always non-greedy
|
|
75
151
|
# @!visibility private
|
|
76
152
|
def pattern(**options)
|
|
77
|
-
constraint ||
|
|
153
|
+
constraint || '.*?'
|
|
78
154
|
end
|
|
79
155
|
end
|
|
80
156
|
|
|
@@ -83,11 +159,17 @@ module Mustermann
|
|
|
83
159
|
register :variable
|
|
84
160
|
|
|
85
161
|
# @!visibility private
|
|
86
|
-
def translate(**options)
|
|
87
|
-
|
|
162
|
+
def translate(atomic: false, **options)
|
|
163
|
+
# Exploded variables expand to `pattern(?:sep pattern)*`. The engine
|
|
164
|
+
# must be able to backtrack through that repetition when a following
|
|
165
|
+
# capture (e.g. the 'b' in {/a*,b}) needs to claim the last segment.
|
|
166
|
+
# Strip `atomic:` so Capture#translate never wraps the repetition.
|
|
167
|
+
effective_atomic = atomic && !explode
|
|
168
|
+
return super(atomic: effective_atomic, **options) if explode or !options[:parametric]
|
|
169
|
+
|
|
88
170
|
# Remove this line after fixing broken compatibility between 2.1 and 2.2
|
|
89
171
|
options.delete(:parametric) if options.has_key?(:parametric)
|
|
90
|
-
parametric super(parametric: false, **options)
|
|
172
|
+
parametric super(atomic: effective_atomic, parametric: false, **options)
|
|
91
173
|
end
|
|
92
174
|
|
|
93
175
|
# @!visibility private
|
|
@@ -117,21 +199,34 @@ module Mustermann
|
|
|
117
199
|
# @!visibility private
|
|
118
200
|
def register_param(parametric: false, split_params: nil, separator: nil, **options)
|
|
119
201
|
return unless explode and split_params
|
|
202
|
+
|
|
120
203
|
split_params[name] = { separator: separator, parametric: parametric }
|
|
121
204
|
end
|
|
122
205
|
end
|
|
123
206
|
|
|
207
|
+
# @return [Array<String>] all raw string representations of the character (literal + URI-encoded variants)
|
|
208
|
+
# @!visibility private
|
|
209
|
+
def self.char_representations(char, uri_decode: true, space_matches_plus: true)
|
|
210
|
+
if char == ' ' and space_matches_plus
|
|
211
|
+
@space_and_plus ||= char_representations(' ', space_matches_plus: false) +
|
|
212
|
+
char_representations('+', space_matches_plus: false)
|
|
213
|
+
else
|
|
214
|
+
@char_representations ||= {}
|
|
215
|
+
@char_representations[char] ||= begin
|
|
216
|
+
escaped = URI_PARSER.escape(char, /./)
|
|
217
|
+
[char, escaped.upcase, escaped.downcase].uniq
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
124
222
|
# @return [String] Regular expression for matching the given character in all representations
|
|
125
223
|
# @!visibility private
|
|
126
224
|
def encoded(char, uri_decode: true, space_matches_plus: true, **options)
|
|
127
225
|
return Regexp.escape(char) unless uri_decode
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
list << " "
|
|
133
|
-
end
|
|
134
|
-
"(?:%s)" % list.join("|")
|
|
226
|
+
|
|
227
|
+
'(?:%s)' % self.class.char_representations(char, uri_decode:, space_matches_plus:).map { |c|
|
|
228
|
+
Regexp.escape(c)
|
|
229
|
+
}.join('|')
|
|
135
230
|
end
|
|
136
231
|
|
|
137
232
|
# Compiles an AST to a regular expression.
|
|
@@ -83,6 +83,16 @@ module Mustermann
|
|
|
83
83
|
raise error.class, "#{error.message}: #{@string.inspect}", error.backtrace
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# Returns a regexp that matches strings excluded by the +except+ option,
|
|
87
|
+
# or +nil+ if no +except+ constraint was given. Used by the trie matcher
|
|
88
|
+
# to filter out excluded strings at leaf nodes.
|
|
89
|
+
# @return [Regexp, nil]
|
|
90
|
+
# @!visibility private
|
|
91
|
+
def except_regexp
|
|
92
|
+
return unless except_str = options[:except]
|
|
93
|
+
@except_regexp ||= Regexp.new("\\A#{compiler.new.translate(parse(except_str), no_captures: true, **options.except(:except))}\\z")
|
|
94
|
+
end
|
|
95
|
+
|
|
86
96
|
# Internal AST representation of pattern.
|
|
87
97
|
# @!visibility private
|
|
88
98
|
def to_ast
|
|
@@ -104,9 +104,14 @@ module Mustermann
|
|
|
104
104
|
# @return decorator encapsulating translation
|
|
105
105
|
#
|
|
106
106
|
# @!visibility private
|
|
107
|
+
def self.factory_for(node_class)
|
|
108
|
+
@factory_for ||= {}
|
|
109
|
+
@factory_for[node_class] ||= node_class.ancestors.lazy.filter_map { dispatch_table[_1.name] }.first
|
|
110
|
+
end
|
|
111
|
+
|
|
107
112
|
def decorator_for(node)
|
|
108
|
-
factory =
|
|
109
|
-
|
|
113
|
+
factory = self.class.factory_for(node.class) or
|
|
114
|
+
raise error_class, "#{self.class}: Cannot translate #{node.class}"
|
|
110
115
|
factory.new(node, self)
|
|
111
116
|
end
|
|
112
117
|
|
data/lib/mustermann/concat.rb
CHANGED
|
@@ -75,10 +75,16 @@ module Mustermann
|
|
|
75
75
|
|
|
76
76
|
# @see Mustermann::Pattern#peek_match
|
|
77
77
|
def peek_match(string)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
substring = string
|
|
79
|
+
params = {}
|
|
80
|
+
|
|
81
|
+
patterns.each do |pattern|
|
|
82
|
+
return unless part = pattern.peek_match(substring)
|
|
83
|
+
params.merge!(part.params)
|
|
84
|
+
substring = substring[part.to_s.size..-1]
|
|
81
85
|
end
|
|
86
|
+
|
|
87
|
+
Match.new(self, string[0, string.size - substring.size], params, post_match: substring)
|
|
82
88
|
end
|
|
83
89
|
|
|
84
90
|
# @see Mustermann::Pattern#peek_params
|
data/lib/mustermann/error.rb
CHANGED
|
@@ -5,5 +5,6 @@ module Mustermann
|
|
|
5
5
|
CompileError = Class.new(Error) # Raised if anything goes wrong while compiling a {Pattern}.
|
|
6
6
|
ParseError = Class.new(Error) # Raised if anything goes wrong while parsing a {Pattern}.
|
|
7
7
|
ExpandError = Class.new(Error) # Raised if anything goes wrong while expanding a {Pattern}.
|
|
8
|
+
TrieError = Class.new(CompileError) # Raised if anything goes wrong while compiling a {Trie}.
|
|
8
9
|
end
|
|
9
10
|
end
|
data/lib/mustermann/expander.rb
CHANGED
|
@@ -13,15 +13,16 @@ module Mustermann
|
|
|
13
13
|
#
|
|
14
14
|
# expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5"
|
|
15
15
|
class Expander
|
|
16
|
+
# @!visibility private
|
|
17
|
+
ADDITIONAL_VALUES = %i[raise ignore append].freeze
|
|
18
|
+
|
|
16
19
|
attr_reader :patterns, :additional_values, :caster
|
|
17
20
|
|
|
18
21
|
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}.
|
|
19
22
|
# @param [Symbol] additional_values behavior when encountering additional values, see {#expand}.
|
|
20
23
|
# @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}.
|
|
21
24
|
def initialize(*patterns, additional_values: :raise, **options, &block)
|
|
22
|
-
|
|
23
|
-
raise ArgumentError, "Illegal value %p for additional_values" % additional_values
|
|
24
|
-
end
|
|
25
|
+
raise ArgumentError, "Illegal value %p for additional_values" % additional_values unless ADDITIONAL_VALUES.include? additional_values
|
|
25
26
|
|
|
26
27
|
@patterns = []
|
|
27
28
|
@api_expander = AST::Expander.new
|
|
@@ -42,6 +43,10 @@ module Mustermann
|
|
|
42
43
|
# @return [Mustermann::Expander] the expander
|
|
43
44
|
def add(*patterns)
|
|
44
45
|
patterns.each do |pattern|
|
|
46
|
+
if pattern.is_a? Expander
|
|
47
|
+
add(*pattern.patterns)
|
|
48
|
+
next
|
|
49
|
+
end
|
|
45
50
|
pattern = Mustermann.new(pattern, **@options)
|
|
46
51
|
if block_given?
|
|
47
52
|
@api_expander.add(yield(pattern))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'mustermann/sinatra'
|
|
2
|
+
|
|
3
|
+
module Mustermann
|
|
4
|
+
# Hybrid pattern type that bridges {Mustermann::Sinatra} and Rails pattern syntax.
|
|
5
|
+
#
|
|
6
|
+
# It supports all syntax elements of {Mustermann::Sinatra}, plus URI template-style
|
|
7
|
+
# placeholders, and changes the semantics of parenthesized groups to match Rails:
|
|
8
|
+
#
|
|
9
|
+
# - A group *without* a pipe operator is **implicitly optional**, even without a
|
|
10
|
+
# trailing `?`. So `/foo(/bar)` matches both `/foo/bar` and `/foo`.
|
|
11
|
+
#
|
|
12
|
+
# - A group *with* a pipe operator is **not** implicitly optional, to avoid the
|
|
13
|
+
# ambiguity of `/scope/(a|b)` also matching `/scope/`. Add a trailing `?` to make
|
|
14
|
+
# such a group optional explicitly: `/scope/(a|b)?`.
|
|
15
|
+
#
|
|
16
|
+
# @example Implicit optional group (no pipe)
|
|
17
|
+
# require 'mustermann'
|
|
18
|
+
# pattern = Mustermann.new('/foo(/bar)', type: :hybrid)
|
|
19
|
+
# pattern === '/foo' # => true
|
|
20
|
+
# pattern === '/foo/bar' # => true
|
|
21
|
+
#
|
|
22
|
+
# @example Non-optional group with pipe
|
|
23
|
+
# pattern = Mustermann.new('/scope/(a|b)', type: :hybrid)
|
|
24
|
+
# pattern === '/scope/a' # => true
|
|
25
|
+
# pattern === '/scope/' # => false
|
|
26
|
+
#
|
|
27
|
+
# @example Explicitly optional group with pipe
|
|
28
|
+
# pattern = Mustermann.new('/scope/(a|b)?', type: :hybrid)
|
|
29
|
+
# pattern === '/scope/' # => true
|
|
30
|
+
#
|
|
31
|
+
# @example Nested implicit optional groups (Rails-style resource routing)
|
|
32
|
+
# pattern = Mustermann.new('/:controller(/:action(/:id))', type: :hybrid)
|
|
33
|
+
# pattern.params('/posts') # => { "controller" => "posts" }
|
|
34
|
+
# pattern.params('/posts/show') # => { "controller" => "posts", "action" => "show" }
|
|
35
|
+
# pattern.params('/posts/show/1') # => { "controller" => "posts", "action" => "show", "id" => "1" }
|
|
36
|
+
#
|
|
37
|
+
# @see Mustermann::Sinatra
|
|
38
|
+
class Hybrid < Sinatra
|
|
39
|
+
register :hybrid
|
|
40
|
+
|
|
41
|
+
# Parses a parenthesized group. Groups without a pipe operator are wrapped in an
|
|
42
|
+
# optional node (implicitly optional, Rails style). Groups that do contain a pipe
|
|
43
|
+
# operator are left as plain groups; append `?` to make them optional explicitly.
|
|
44
|
+
on("(") do |c|
|
|
45
|
+
n = node(:group) { read unless scan(?)) }
|
|
46
|
+
has_or = n.payload.any? { |e| e.is_a?(:or) }
|
|
47
|
+
has_or && !scan("?") ? n : node(:optional, n)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mustermann
|
|
4
|
+
class Match
|
|
5
|
+
attr_reader :pattern, :string, :params, :post_match, :pre_match
|
|
6
|
+
|
|
7
|
+
def initialize(pattern, string, params = {}, post_match: '', pre_match: '')
|
|
8
|
+
@pattern = pattern
|
|
9
|
+
@string = string.freeze
|
|
10
|
+
@params = params.freeze
|
|
11
|
+
@post_match = post_match.freeze
|
|
12
|
+
@pre_match = pre_match.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def [](key)
|
|
16
|
+
case key
|
|
17
|
+
when String then params[key]
|
|
18
|
+
when Symbol then params[key.to_s]
|
|
19
|
+
else raise ArgumentError, "key must be a String or Symbol, not #{key.class}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def deconstruct_keys(keys) = keys.to_h { |key| [key, self[key]] }
|
|
24
|
+
|
|
25
|
+
def hash = pattern.hash ^ string.hash ^ params.hash
|
|
26
|
+
|
|
27
|
+
def eql?(other)
|
|
28
|
+
return false unless other.is_a? self.class
|
|
29
|
+
pattern == other.pattern && string == other.string && params == other.params
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def values_at(*keys) = keys.map { |key| self[key] }
|
|
33
|
+
|
|
34
|
+
alias == eql?
|
|
35
|
+
alias to_s string
|
|
36
|
+
alias to_h params
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/mustermann/pattern.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'mustermann/error'
|
|
3
|
-
require 'mustermann/
|
|
3
|
+
require 'mustermann/match'
|
|
4
4
|
require 'mustermann/equality_map'
|
|
5
5
|
require 'uri'
|
|
6
6
|
|
|
@@ -84,12 +84,10 @@ module Mustermann
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# @param [String] string The string to match against
|
|
87
|
-
# @return [
|
|
88
|
-
# @see
|
|
89
|
-
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
|
|
90
|
-
# @see Mustermann::SimpleMatch
|
|
87
|
+
# @return [Mustermann::Match, nil] the match object if the pattern matches.
|
|
88
|
+
# @see Mustermann::Match
|
|
91
89
|
def match(string)
|
|
92
|
-
|
|
90
|
+
Match.new(self, string) if self === string
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
# @param [String] string The string to match against
|
|
@@ -110,7 +108,7 @@ module Mustermann
|
|
|
110
108
|
# Used by Ruby internally for hashing.
|
|
111
109
|
# @return [Integer] same has value for patterns that are equal
|
|
112
110
|
def hash
|
|
113
|
-
self.class.hash
|
|
111
|
+
self.class.hash ^ @string.hash ^ options.hash
|
|
114
112
|
end
|
|
115
113
|
|
|
116
114
|
# Two patterns are considered equal if they are of the same type, have the same pattern string
|
|
@@ -163,11 +161,11 @@ module Mustermann
|
|
|
163
161
|
# pattern.peek("/Frank/Sinatra") # => #<MatchData "/Frank" name:"Frank">
|
|
164
162
|
#
|
|
165
163
|
# @param [String] string The string to match against
|
|
166
|
-
# @return [
|
|
164
|
+
# @return [Mustermann::Match, nil] MatchData or similar object if the pattern matches.
|
|
167
165
|
# @see #peek_params
|
|
168
166
|
def peek_match(string)
|
|
169
167
|
matched = peek(string)
|
|
170
|
-
|
|
168
|
+
Match.new(self, matched, {}, post_match: string[matched.size..-1]) if matched
|
|
171
169
|
end
|
|
172
170
|
|
|
173
171
|
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
|
|
@@ -184,33 +182,12 @@ module Mustermann
|
|
|
184
182
|
# @return [Array<Hash, Integer>, nil] Array with params hash and length of substing if matched, nil otherwise
|
|
185
183
|
def peek_params(string)
|
|
186
184
|
match = peek_match(string)
|
|
187
|
-
[params
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
|
|
191
|
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
|
|
192
|
-
def named_captures
|
|
193
|
-
{}
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# @return [Array<String>] capture names.
|
|
197
|
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
|
|
198
|
-
def names
|
|
199
|
-
[]
|
|
185
|
+
match ? [match.params, match.string.size] : nil
|
|
200
186
|
end
|
|
201
187
|
|
|
202
188
|
# @param [String] string the string to match against
|
|
203
189
|
# @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
|
|
204
|
-
def params(string = nil
|
|
205
|
-
return unless captures ||= match(string)
|
|
206
|
-
params = named_captures.map do |name, positions|
|
|
207
|
-
values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten
|
|
208
|
-
values = values.first if values.size < 2 and not always_array? name
|
|
209
|
-
[name, values]
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
Hash[params]
|
|
213
|
-
end
|
|
190
|
+
def params(string = nil) = match(string)&.params
|
|
214
191
|
|
|
215
192
|
# @note This method is only implemented by certain subclasses.
|
|
216
193
|
#
|
data/lib/mustermann/rails.rb
CHANGED
|
@@ -32,17 +32,32 @@ module Mustermann
|
|
|
32
32
|
# @param (see Mustermann::Pattern#peek_match)
|
|
33
33
|
# @return (see Mustermann::Pattern#peek_match)
|
|
34
34
|
# @see (see Mustermann::Pattern#peek_match)
|
|
35
|
-
def peek_match(string)
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
def peek_match(string) = build_match(@peek_regexp.match(string))
|
|
36
|
+
|
|
37
|
+
def match(string) = build_match(@regexp.match(string))
|
|
38
|
+
|
|
39
|
+
# private
|
|
40
|
+
|
|
41
|
+
# def build_match(match)
|
|
42
|
+
# return unless match
|
|
43
|
+
# Match.new(self, match.string, match.named_captures, post_match: match.post_match, pre_match: match.pre_match)
|
|
44
|
+
# end
|
|
38
45
|
|
|
39
46
|
extend Forwardable
|
|
40
|
-
def_delegators :regexp, :===, :=~, :
|
|
47
|
+
def_delegators :regexp, :===, :=~, :names
|
|
48
|
+
|
|
49
|
+
private
|
|
41
50
|
|
|
42
|
-
def
|
|
43
|
-
|
|
51
|
+
def build_match(match)
|
|
52
|
+
return unless match
|
|
53
|
+
params = match.regexp.named_captures.to_h do |name, positions|
|
|
54
|
+
value = positions.size < 2 && !always_array?(name) ? map_param(name, match[name]) :
|
|
55
|
+
positions.flat_map { |pos| map_param(name, match[pos]) }
|
|
56
|
+
[name, value]
|
|
57
|
+
end
|
|
58
|
+
Match.new(self, match.to_s, params, post_match: match.post_match, pre_match: match.pre_match)
|
|
44
59
|
end
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
def compile(**options) = raise NotImplementedError, 'subclass responsibility'
|
|
47
62
|
end
|
|
48
63
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann'
|
|
3
|
+
require 'mustermann/set'
|
|
4
|
+
|
|
5
|
+
module Mustermann
|
|
6
|
+
# An extremely simple, Rack-compatible router implementation using {Mustermann::Set} for pattern matching.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# require 'mustermann/router'
|
|
10
|
+
#
|
|
11
|
+
# router = Mustermann::Router.new do
|
|
12
|
+
# get "/hello/:name" do |env|
|
|
13
|
+
# name = env["mustermann.match"][:name]
|
|
14
|
+
# [200, { "Content-Type" => "text/plain" }, ["Hello, #{name}!"]]
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # in config.ru
|
|
19
|
+
# run router
|
|
20
|
+
#
|
|
21
|
+
# @example Routing to other applications
|
|
22
|
+
# router = Mustermann::Router.new do
|
|
23
|
+
# get "/users", MyApp::Users::Index
|
|
24
|
+
# get "/users/:id", MyApp::Users::Show
|
|
25
|
+
# post "/users", MyApp::Users::Create
|
|
26
|
+
# fallback MyApp::NotFound
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# router.path_for(MyApp::Users::Show, id: 42) # => "/users/42"
|
|
30
|
+
#
|
|
31
|
+
# @example As middleware
|
|
32
|
+
# use Mustermann::Router do
|
|
33
|
+
# get("/up") { [200, { "Content-Type" => "text/plain" }, ["Up!"]] }
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# run MyApp
|
|
37
|
+
#
|
|
38
|
+
# @see Mustermann::Set
|
|
39
|
+
# @see https://rack.github.io/rack/
|
|
40
|
+
class Router
|
|
41
|
+
NOT_FOUND = [404, { "Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not found"]].freeze
|
|
42
|
+
VERBS = %w[GET HEAD POST PUT PATCH DELETE OPTIONS LINK UNLINK].freeze
|
|
43
|
+
private_constant :VERBS, :NOT_FOUND
|
|
44
|
+
|
|
45
|
+
# Initializes a new router.
|
|
46
|
+
# @param key [String] The key under which the route match will be stored in the Rack environment hash (default: "mustermann.match").
|
|
47
|
+
# @param options [Hash] Options to be passed to the Mustermann patterns.
|
|
48
|
+
def initialize(fallback = nil, key: "mustermann.match", **options, &block)
|
|
49
|
+
@key = key
|
|
50
|
+
@sets = VERBS.to_h { |verb| [verb, Set.new] }
|
|
51
|
+
@options = options
|
|
52
|
+
@fallback = fallback || ->(env) { NOT_FOUND }
|
|
53
|
+
instance_exec(&block) if block_given?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param env [Hash] The Rack environment hash for the request.
|
|
57
|
+
# @return [Array] The Rack response array (status, headers, body).
|
|
58
|
+
def call(env)
|
|
59
|
+
if routes = @sets[env["REQUEST_METHOD"]] and match = routes.match(env["PATH_INFO"] || "/")
|
|
60
|
+
env = env.merge(@key => match)
|
|
61
|
+
return match.value.call(env)
|
|
62
|
+
end
|
|
63
|
+
@fallback.call(env)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def fallback(fallback = nil, &block) = @fallback = fallback || block || @fallback
|
|
67
|
+
|
|
68
|
+
# Adds a route for the given verb and pattern, with the given target.
|
|
69
|
+
#
|
|
70
|
+
# @note Shorthand methods, like `get`, `post`, etc. dynamically are defined for all supported verbs.
|
|
71
|
+
#
|
|
72
|
+
# @param verb [String] HTTP verb (e.g. "GET", "POST")
|
|
73
|
+
# @param pattern [String, Mustermann::Pattern] Pattern string or Mustermann pattern (e.g. "/users/:id")
|
|
74
|
+
# @param target [#call, nil] The Rack application or middleware to call when the route matches. Can be passed a block as well.
|
|
75
|
+
# @yield [env] Block to be used as the target if no explicit target is given.
|
|
76
|
+
# @yieldparam env [Hash] The Rack environment hash for the request.
|
|
77
|
+
# @return [void]
|
|
78
|
+
def route(verb, pattern, target = nil, **options, &block)
|
|
79
|
+
raise ArgumentError, "need to provide target, :to or a block" unless target || block
|
|
80
|
+
raise ArgumentError, "unknown verb: #{verb}" unless VERBS.include?(verb)
|
|
81
|
+
pattern = Mustermann.new(pattern, **@options, **options)
|
|
82
|
+
@sets[verb].add(pattern, target || block)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Helps generate links
|
|
86
|
+
#
|
|
87
|
+
# @param app [#call] The Rack application or middleware for which to generate the path.
|
|
88
|
+
# @param (see Mustermann::Expander#expand)
|
|
89
|
+
# @return [String] The generated path.
|
|
90
|
+
def path_for(app, behavior = nil, params = {})
|
|
91
|
+
set = @sets.values.find { |s| s.has_value?(app) } || @sets[VERBS.first]
|
|
92
|
+
set.expand(app, behavior, params)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
VERBS.each do |verb|
|
|
96
|
+
define_method(verb.downcase) { |*args, **opts, &block| route(verb, *args, **opts, &block) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|