mustermann19 0.3.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 +18 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/.yardopts +1 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +1081 -0
- data/Rakefile +6 -0
- data/bench/capturing.rb +57 -0
- data/bench/regexp.rb +21 -0
- data/bench/simple_vs_sinatra.rb +23 -0
- data/bench/template_vs_addressable.rb +26 -0
- data/internals.md +64 -0
- data/lib/mustermann.rb +61 -0
- data/lib/mustermann/ast/compiler.rb +168 -0
- data/lib/mustermann/ast/expander.rb +134 -0
- data/lib/mustermann/ast/node.rb +160 -0
- data/lib/mustermann/ast/parser.rb +137 -0
- data/lib/mustermann/ast/pattern.rb +84 -0
- data/lib/mustermann/ast/transformer.rb +129 -0
- data/lib/mustermann/ast/translator.rb +108 -0
- data/lib/mustermann/ast/tree_renderer.rb +29 -0
- data/lib/mustermann/ast/validation.rb +43 -0
- data/lib/mustermann/caster.rb +117 -0
- data/lib/mustermann/equality_map.rb +48 -0
- data/lib/mustermann/error.rb +6 -0
- data/lib/mustermann/expander.rb +206 -0
- data/lib/mustermann/extension.rb +52 -0
- data/lib/mustermann/identity.rb +19 -0
- data/lib/mustermann/mapper.rb +98 -0
- data/lib/mustermann/pattern.rb +182 -0
- data/lib/mustermann/rails.rb +17 -0
- data/lib/mustermann/regexp_based.rb +30 -0
- data/lib/mustermann/regular.rb +26 -0
- data/lib/mustermann/router.rb +9 -0
- data/lib/mustermann/router/rack.rb +50 -0
- data/lib/mustermann/router/simple.rb +144 -0
- data/lib/mustermann/shell.rb +29 -0
- data/lib/mustermann/simple.rb +38 -0
- data/lib/mustermann/simple_match.rb +30 -0
- data/lib/mustermann/sinatra.rb +22 -0
- data/lib/mustermann/template.rb +48 -0
- data/lib/mustermann/to_pattern.rb +45 -0
- data/lib/mustermann/version.rb +3 -0
- data/mustermann.gemspec +31 -0
- data/spec/expander_spec.rb +105 -0
- data/spec/extension_spec.rb +296 -0
- data/spec/identity_spec.rb +83 -0
- data/spec/mapper_spec.rb +83 -0
- data/spec/mustermann_spec.rb +65 -0
- data/spec/pattern_spec.rb +49 -0
- data/spec/rails_spec.rb +522 -0
- data/spec/regexp_based_spec.rb +8 -0
- data/spec/regular_spec.rb +36 -0
- data/spec/router/rack_spec.rb +39 -0
- data/spec/router/simple_spec.rb +32 -0
- data/spec/shell_spec.rb +109 -0
- data/spec/simple_match_spec.rb +10 -0
- data/spec/simple_spec.rb +237 -0
- data/spec/sinatra_spec.rb +574 -0
- data/spec/support.rb +5 -0
- data/spec/support/coverage.rb +16 -0
- data/spec/support/env.rb +15 -0
- data/spec/support/expand_matcher.rb +27 -0
- data/spec/support/match_matcher.rb +39 -0
- data/spec/support/pattern.rb +39 -0
- data/spec/template_spec.rb +815 -0
- data/spec/to_pattern_spec.rb +20 -0
- metadata +301 -0
data/Rakefile
ADDED
data/bench/capturing.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann'
|
5
|
+
require 'mustermann/regexp_based'
|
6
|
+
require 'addressable/template'
|
7
|
+
|
8
|
+
|
9
|
+
Mustermann.register(:regexp, Class.new(Mustermann::RegexpBased) {
|
10
|
+
def compile(**options)
|
11
|
+
Regexp.new(@string)
|
12
|
+
end
|
13
|
+
}, load: false)
|
14
|
+
|
15
|
+
Mustermann.register(:addressable, Class.new(Mustermann::RegexpBased) {
|
16
|
+
def compile(**options)
|
17
|
+
Addressable::Template.new(@string)
|
18
|
+
end
|
19
|
+
}, load: false)
|
20
|
+
|
21
|
+
list = [
|
22
|
+
[:sinatra, '/*/:name' ],
|
23
|
+
[:rails, '/*prefix/:name' ],
|
24
|
+
[:simple, '/*/:name' ],
|
25
|
+
[:template, '{/prefix*}/{name}' ],
|
26
|
+
[:regexp, '\A\/(?<splat>.*?)\/(?<name>[^\/\?#]+)\Z' ],
|
27
|
+
[:addressable, '{/prefix*}/{name}' ]
|
28
|
+
]
|
29
|
+
|
30
|
+
def self.assert(value)
|
31
|
+
fail unless value
|
32
|
+
end
|
33
|
+
|
34
|
+
string = '/a/b/c/d'
|
35
|
+
name = 'd'
|
36
|
+
|
37
|
+
GC.disable
|
38
|
+
|
39
|
+
puts "Compilation:"
|
40
|
+
Benchmark.bmbm do |x|
|
41
|
+
list.each do |type, pattern|
|
42
|
+
x.report(type) { 1_000.times { Mustermann.new(pattern, type: type) } }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "", "Matching with two captures (one splat, one normal):"
|
47
|
+
Benchmark.bmbm do |x|
|
48
|
+
list.each do |type, pattern|
|
49
|
+
pattern = Mustermann.new(pattern, type: type)
|
50
|
+
x.report type do
|
51
|
+
10_000.times do
|
52
|
+
match = pattern.match(string)
|
53
|
+
assert match[:name] == name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/bench/regexp.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
puts " atomic vs normal segments ".center(52, '=')
|
4
|
+
|
5
|
+
types = {
|
6
|
+
normal: /\A\/(?:a|%61)\/(?<b>[^\/\?#]+)(?:\/(?<c>[^\/\?#]+))?\Z/,
|
7
|
+
atomic: /\A\/(?:a|%61)\/(?<b>(?>[^\/\?#]+))(?:\/(?<c>(?>[^\/\?#]+)))?\Z/
|
8
|
+
}
|
9
|
+
|
10
|
+
Benchmark.bmbm do |x|
|
11
|
+
types.each do |name, regexp|
|
12
|
+
string = "/a/" << ?a * 10000 << "/" << ?a * 5000
|
13
|
+
fail unless regexp.match(string)
|
14
|
+
string << "/"
|
15
|
+
fail if regexp.match(string)
|
16
|
+
|
17
|
+
x.report name.to_s do
|
18
|
+
100.times { regexp.match(string) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann/simple'
|
5
|
+
require 'mustermann/sinatra'
|
6
|
+
|
7
|
+
[Mustermann::Simple, Mustermann::Sinatra].each do |klass|
|
8
|
+
puts "", " #{klass} ".center(64, '=')
|
9
|
+
Benchmark.bmbm do |x|
|
10
|
+
no_capture = klass.new("/simple")
|
11
|
+
x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } }
|
12
|
+
x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } }
|
13
|
+
|
14
|
+
simple = klass.new("/:name")
|
15
|
+
x.report("simple, match") { 1_000.times { simple.match('/simple').captures } }
|
16
|
+
x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } }
|
17
|
+
|
18
|
+
splat = klass.new("/*")
|
19
|
+
x.report("splat, match") { 1_000.times { splat.match("/a/b/c").captures } }
|
20
|
+
x.report("splat, miss") { 1_000.times { splat.match("/a/b/c.miss") } }
|
21
|
+
end
|
22
|
+
puts
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann/template'
|
5
|
+
require 'addressable/template'
|
6
|
+
|
7
|
+
[Mustermann::Template, Addressable::Template].each do |klass|
|
8
|
+
puts "", " #{klass} ".center(64, '=')
|
9
|
+
Benchmark.bmbm do |x|
|
10
|
+
no_capture = klass.new("/simple")
|
11
|
+
x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } }
|
12
|
+
x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } }
|
13
|
+
|
14
|
+
simple = klass.new("/{match}")
|
15
|
+
x.report("simple, match") { 1_000.times { simple.match('/simple').captures } }
|
16
|
+
x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } }
|
17
|
+
|
18
|
+
explode = klass.new("{/segments*}")
|
19
|
+
x.report("explode, match") { 1_000.times { explode.match("/a/b/c").captures } }
|
20
|
+
x.report("explode, miss") { 1_000.times { explode.match("/a/b/c.miss") } }
|
21
|
+
|
22
|
+
expand = klass.new("/prefix/{foo}/something/{bar}")
|
23
|
+
x.report("expand") { 100.times { expand.expand(foo: 'foo', bar: 'bar').to_s } }
|
24
|
+
end
|
25
|
+
puts
|
26
|
+
end
|
data/internals.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Internal API
|
2
|
+
|
3
|
+
This document describes how to use [Mustermann](README.md)'s internal API.
|
4
|
+
|
5
|
+
It is a secondary goal to keep the internal API as stable as possible, in a state where it would well be possible to interface with it.
|
6
|
+
However, the internal API is not covered by Semantic Versioning. As a rule of thumb, no backwards incompatible changes should be introduced to the API in minor releases (starting from 1.0.0).
|
7
|
+
|
8
|
+
Should the internal API gain widespread/production use, we might consider moving parts of it over into the public API.
|
9
|
+
|
10
|
+
Here is a quick example of what you can do with this:
|
11
|
+
|
12
|
+
``` ruby
|
13
|
+
require 'mustermann/ast/pattern'
|
14
|
+
|
15
|
+
class MyPattern < Mustermann::AST::Pattern
|
16
|
+
on("~") { |c| node(:capture, buffer[1]) if expect(/\{(\w+)\}/) }
|
17
|
+
on("+") { |c| node(:named_splat, buffer[1]) if expect(/\{(\w+)\}/) }
|
18
|
+
on("?") { |c| node(:optional, node(:capture, buffer[1])) if expect(/\{(\w+)\}/) }
|
19
|
+
end
|
20
|
+
|
21
|
+
pattern = MyPattern.new("/+{prefix}/~{page}/?{optional}")
|
22
|
+
pattern.params("/a/") # => nil
|
23
|
+
pattern.params("/a/b/") # => { "prefix" => "a", "page" => "b", "optional" => nil }
|
24
|
+
pattern.params("/a/b/c") # => { "prefix" => "a", "page" => "b", "optional" => "c" }
|
25
|
+
pattern.params("/a/b/c/") # => { "prefix" => "a/b", "page" => "c", "optional" => nil }
|
26
|
+
|
27
|
+
pattern.expand(prefix: "a", page: "foo") # => "/a/foo/"
|
28
|
+
pattern.expand(prefix: "a/b", page: "c/d") # => "/a/b/c%2Fd/"
|
29
|
+
|
30
|
+
require 'mustermann'
|
31
|
+
Mustermann.register(:my_pattern, MyPattern, load: false)
|
32
|
+
Mustermann.new('/+{prefix}/~{page}/?{optional}', type: :my_pattern) # => #<MyPattern:"/+{prefix}/~{page}/?{optional}">
|
33
|
+
|
34
|
+
require 'sinatra/base'
|
35
|
+
class MyApp < Sinatra::Base
|
36
|
+
register Mustermann
|
37
|
+
set :pattern, type: :my_pattern
|
38
|
+
|
39
|
+
get '/hello/~{name}' do
|
40
|
+
"Hello #{params[:name].capitalize}!"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
require 'mustermann/ast/tree_renderer'
|
45
|
+
ast = MyPattern::Parser.parse(pattern.to_s)
|
46
|
+
puts Mustermann::AST::TreeRenderer.render(ast)
|
47
|
+
|
48
|
+
```
|
49
|
+
|
50
|
+
## Pattern Registration
|
51
|
+
|
52
|
+
...
|
53
|
+
|
54
|
+
## Build Your Own Pattern
|
55
|
+
|
56
|
+
...
|
57
|
+
|
58
|
+
## Patterns Based on Regular Expressions
|
59
|
+
|
60
|
+
...
|
61
|
+
|
62
|
+
## AST-Based Patterns
|
63
|
+
|
64
|
+
...
|
data/lib/mustermann.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'mustermann/pattern'
|
2
|
+
|
3
|
+
# Namespace and main entry point for the Mustermann library.
|
4
|
+
#
|
5
|
+
# Under normal circumstances the only external API entry point you should be using is {Mustermann.new}.
|
6
|
+
module Mustermann
|
7
|
+
# @param [String, Pattern, Regexp, #to_pattern] input The representation of the new pattern
|
8
|
+
# @param [Hash] options The options hash
|
9
|
+
# @return [Mustermann::Pattern] pattern corresponding to string.
|
10
|
+
# @raise (see [])
|
11
|
+
# @raise (see Mustermann::Pattern.new)
|
12
|
+
# @see file:README.md#Types_and_Options "Types and Options" in the README
|
13
|
+
def self.new(input, options = {})
|
14
|
+
type = options.delete(:type) || :sinatra
|
15
|
+
case input
|
16
|
+
when Pattern then input
|
17
|
+
when Regexp then self[:regexp].new(input, options)
|
18
|
+
when String then self[type].new(input, options)
|
19
|
+
else input.to_pattern(options.merge(type: type))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Maps a type to its factory.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# Mustermann[:sinatra] # => Mustermann::Sinatra
|
27
|
+
#
|
28
|
+
# @param [Symbol] key a pattern type identifier
|
29
|
+
# @raise [ArgumentError] if the type is not supported
|
30
|
+
# @return [Class, #new] pattern factory
|
31
|
+
def self.[](key)
|
32
|
+
constant, library = register.fetch(key) { raise ArgumentError, "unsupported type %p" % key }
|
33
|
+
require library if library
|
34
|
+
constant.respond_to?(:new) ? constant : register[key] = const_get(constant)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @!visibility private
|
38
|
+
def self.register(*identifiers)
|
39
|
+
options = identifiers.last.is_a?(Hash) ? identifiers.pop : {}
|
40
|
+
constant = options[:constant] || identifiers.first.to_s.capitalize
|
41
|
+
load = options[:load] || "mustermann/#{identifiers.first}"
|
42
|
+
@register ||= {}
|
43
|
+
identifiers.each { |i| @register[i] = [constant, load] }
|
44
|
+
@register
|
45
|
+
end
|
46
|
+
|
47
|
+
# @!visibility private
|
48
|
+
def self.extend_object(object)
|
49
|
+
return super unless defined? ::Sinatra::Base and object.is_a? Class and object < ::Sinatra::Base
|
50
|
+
require 'mustermann/extension'
|
51
|
+
object.register Extension
|
52
|
+
end
|
53
|
+
|
54
|
+
register :identity
|
55
|
+
register :rails
|
56
|
+
register :regular, :regexp
|
57
|
+
register :shell
|
58
|
+
register :simple
|
59
|
+
register :sinatra
|
60
|
+
register :template
|
61
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# @see Mustermann::AST::Pattern
|
5
|
+
module AST
|
6
|
+
# Regexp compilation logic.
|
7
|
+
# @!visibility private
|
8
|
+
class Compiler < Translator
|
9
|
+
raises CompileError
|
10
|
+
|
11
|
+
# Trivial compilations
|
12
|
+
translate(Array) { |o = {}| map { |e| t(e, o) }.join }
|
13
|
+
translate(:node) { |o = {}| t(payload, o) }
|
14
|
+
translate(:separator) { |o = {}| Regexp.escape(payload) }
|
15
|
+
translate(:optional) { |o = {}| "(?:%s)?" % t(payload, o) }
|
16
|
+
translate(:char) { |o = {}| t.encoded(payload, o) }
|
17
|
+
|
18
|
+
translate :expression do |options = {}|
|
19
|
+
greedy = options.fetch(:greedy, true)
|
20
|
+
t(payload, options.merge(allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
|
21
|
+
parametric: operator.parametric, separator: operator.separator))
|
22
|
+
end
|
23
|
+
|
24
|
+
translate :with_look_ahead do |options = {}|
|
25
|
+
lookahead = each_leaf.inject("") do |ahead, element|
|
26
|
+
ahead + t(element, options.merge(skip_optional: true, lookahead: ahead, greedy: false, no_captures: true)).to_s
|
27
|
+
end
|
28
|
+
lookahead << (at_end ? '$' : '/')
|
29
|
+
t(head, options.merge(lookahead: lookahead)) + t(payload, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Capture compilation is complex. :(
|
33
|
+
# @!visibility private
|
34
|
+
class Capture < NodeTranslator
|
35
|
+
register :capture
|
36
|
+
|
37
|
+
# @!visibility private
|
38
|
+
def translate(options = {})
|
39
|
+
return pattern(options) if options[:no_captures]
|
40
|
+
"(?<#{name}>#{translate(options.merge(no_captures: true))})"
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [String] regexp without the named capture
|
44
|
+
# @!visibility private
|
45
|
+
def pattern(options = {})
|
46
|
+
capture = options.delete(:capture)
|
47
|
+
case capture
|
48
|
+
when Symbol then from_symbol(capture, options)
|
49
|
+
when Array then from_array(capture, options)
|
50
|
+
when Hash then from_hash(capture, options)
|
51
|
+
when String then from_string(capture, options)
|
52
|
+
when nil then from_nil(options)
|
53
|
+
else capture
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def qualified(string, options = {})
|
59
|
+
greedy = options.fetch(:greedy, true)
|
60
|
+
"#{string}+#{?? unless greedy}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_lookahead(string, options = {})
|
64
|
+
lookahead = options.delete(:lookahead)
|
65
|
+
lookahead ? "(?:(?!#{lookahead})#{string})" : string
|
66
|
+
end
|
67
|
+
def from_hash(hash, options = {}) pattern(options.merge(capture: hash[name.to_sym])) end
|
68
|
+
def from_array(array, options = {}) Regexp.union(*array.map { |e| pattern(options.merge(capture: e)) }) end
|
69
|
+
def from_symbol(symbol, options = {}) qualified(with_lookahead("[[:#{symbol}:]]", options), options) end
|
70
|
+
def from_string(string, options = {}) Regexp.new(string.chars.map { |c| t.encoded(c, options) }.join) end
|
71
|
+
def from_nil(options = {}) qualified(with_lookahead(default(options), options), options) end
|
72
|
+
def default(options = {}) "[^/\\?#]" end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @!visibility private
|
76
|
+
class Splat < Capture
|
77
|
+
register :splat, :named_splat
|
78
|
+
# splats are always non-greedy
|
79
|
+
# @!visibility private
|
80
|
+
def pattern(options = {})
|
81
|
+
".*?"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @!visibility private
|
86
|
+
class Variable < Capture
|
87
|
+
register :variable
|
88
|
+
|
89
|
+
# @!visibility private
|
90
|
+
def translate(options = {})
|
91
|
+
return super(options) if explode or not options[:parametric]
|
92
|
+
parametric super(options.merge(parametric: false))
|
93
|
+
end
|
94
|
+
|
95
|
+
# @!visibility private
|
96
|
+
def pattern(options = {})
|
97
|
+
parametric = options.delete(:parametric) || false
|
98
|
+
separator = options.delete(:separator)
|
99
|
+
register_param(options.merge(parametric: parametric, separator: separator))
|
100
|
+
pattern = super(options)
|
101
|
+
pattern = parametric(pattern) if parametric
|
102
|
+
pattern = "#{pattern}(?:#{Regexp.escape(separator)}#{pattern})*" if explode and separator
|
103
|
+
pattern
|
104
|
+
end
|
105
|
+
|
106
|
+
# @!visibility private
|
107
|
+
def parametric(string)
|
108
|
+
"#{Regexp.escape(name)}(?:=#{string})?"
|
109
|
+
end
|
110
|
+
|
111
|
+
# @!visibility private
|
112
|
+
def qualified(string, options = {})
|
113
|
+
prefix ? "#{string}{1,#{prefix}}" : super(string, options)
|
114
|
+
end
|
115
|
+
|
116
|
+
# @!visibility private
|
117
|
+
def default(options = {})
|
118
|
+
allow_reserved = options.delete(:allow_reserved) || false
|
119
|
+
allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]'
|
120
|
+
end
|
121
|
+
|
122
|
+
# @!visibility private
|
123
|
+
def register_param(options = {})
|
124
|
+
parametric = options.has_key?(:parametric) ? options.delete(:parametric) : false
|
125
|
+
split_params = options.delete(:split_params)
|
126
|
+
separator = options.delete(:separator)
|
127
|
+
return unless explode and split_params
|
128
|
+
split_params[name] = { separator: separator, parametric: parametric }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# @return [String] Regular expression for matching the given character in all representations
|
133
|
+
# @!visibility private
|
134
|
+
def encoded(char, options ={})
|
135
|
+
uri_decode = options.fetch(:uri_decode, true)
|
136
|
+
space_matches_plus = options.fetch(:space_matches_plus, true)
|
137
|
+
return Regexp.escape(char) unless uri_decode
|
138
|
+
encoded = escape(char, escape: /./)
|
139
|
+
list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
|
140
|
+
list << encoded('+') if space_matches_plus and char == " "
|
141
|
+
"(?:%s)" % list.join("|")
|
142
|
+
end
|
143
|
+
|
144
|
+
# Compiles an AST to a regular expression.
|
145
|
+
# @param [Mustermann::AST::Node] ast the tree
|
146
|
+
# @return [Regexp] corresponding regular expression.
|
147
|
+
#
|
148
|
+
# @!visibility private
|
149
|
+
def self.compile(ast, options = {})
|
150
|
+
new.compile(ast, options)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Compiles an AST to a regular expression.
|
154
|
+
# @param [Mustermann::AST::Node] ast the tree
|
155
|
+
# @return [Regexp] corresponding regular expression.
|
156
|
+
#
|
157
|
+
# @!visibility private
|
158
|
+
def compile(ast, options = {})
|
159
|
+
except = options.delete(:except)
|
160
|
+
except &&= "(?!#{translate(except, options.merge(no_captures: true))}\\Z)"
|
161
|
+
expression = "\\A#{except}#{translate(ast, options)}\\Z"
|
162
|
+
Regexp.new(expression)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
#private_constant :Compiler
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
require 'mustermann/ast/compiler'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
module AST
|
6
|
+
# Looks at an AST, remembers the important bits of information to do an
|
7
|
+
# ultra fast expansion.
|
8
|
+
#
|
9
|
+
# @!visibility private
|
10
|
+
class Expander < Translator
|
11
|
+
raises ExpandError
|
12
|
+
|
13
|
+
translate Array do
|
14
|
+
inject(t.pattern) do |pattern, element|
|
15
|
+
t.add_to(pattern, t(element))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
translate :capture do
|
20
|
+
t.for_capture(node)
|
21
|
+
end
|
22
|
+
|
23
|
+
translate :named_splat, :splat do
|
24
|
+
t.pattern + t.for_capture(node)
|
25
|
+
end
|
26
|
+
|
27
|
+
translate :root, :group, :expression do
|
28
|
+
t(payload)
|
29
|
+
end
|
30
|
+
|
31
|
+
translate :char do
|
32
|
+
t.pattern(t.escape(payload, also_escape: /[\/\?#\&\=%]/).gsub(?%, "%%"))
|
33
|
+
end
|
34
|
+
|
35
|
+
translate :separator do
|
36
|
+
t.pattern(payload.gsub(?%, "%%"))
|
37
|
+
end
|
38
|
+
|
39
|
+
translate :with_look_ahead do
|
40
|
+
t.add_to(t(head), t(payload))
|
41
|
+
end
|
42
|
+
|
43
|
+
translate :optional do
|
44
|
+
nested = t(payload)
|
45
|
+
nested += t.pattern unless nested.any? { |n| n.first.empty? }
|
46
|
+
nested
|
47
|
+
end
|
48
|
+
|
49
|
+
# helper method for captures
|
50
|
+
# @!visibility private
|
51
|
+
def for_capture(node)
|
52
|
+
name = node.name.to_sym
|
53
|
+
pattern('%s', name, name => /(?!#{pattern_for(node)})./)
|
54
|
+
end
|
55
|
+
|
56
|
+
# maps sorted key list to sprintf patterns and filters
|
57
|
+
# @!visibility private
|
58
|
+
def mappings
|
59
|
+
@mappings ||= {}
|
60
|
+
end
|
61
|
+
|
62
|
+
# all the known keys
|
63
|
+
# @!visibility private
|
64
|
+
def keys
|
65
|
+
@keys ||= []
|
66
|
+
end
|
67
|
+
|
68
|
+
# add a tree for expansion
|
69
|
+
# @!visibility private
|
70
|
+
def add(ast)
|
71
|
+
translate(ast).each do |keys, pattern, filter|
|
72
|
+
self.keys.concat(keys).uniq!
|
73
|
+
mappings[keys.uniq.sort] ||= [keys, pattern, filter]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# helper method for getting a capture's pattern.
|
78
|
+
# @!visibility private
|
79
|
+
def pattern_for(node, options = {})
|
80
|
+
Compiler.new.decorator_for(node).pattern(options)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @see Mustermann::Pattern#expand
|
84
|
+
# @!visibility private
|
85
|
+
def expand(values = {})
|
86
|
+
keys, pattern, filters = mappings.fetch(values.keys.sort) { error_for(values) }
|
87
|
+
filters.each { |key, filter| values[key] &&= escape(values[key], also_escape: filter) }
|
88
|
+
pattern % values.values_at(*keys)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @see Mustermann::Pattern#expandable?
|
92
|
+
# @!visibility private
|
93
|
+
def expandable?(values)
|
94
|
+
values = values.keys if values.respond_to? :keys
|
95
|
+
values = values.sort if values.respond_to? :sort
|
96
|
+
mappings.include? values
|
97
|
+
end
|
98
|
+
|
99
|
+
# @see Mustermann::Expander#with_rest
|
100
|
+
# @!visibility private
|
101
|
+
def expandable_keys(keys)
|
102
|
+
mappings.keys.select { |k| (k - keys).empty? }.max_by(&:size) || keys
|
103
|
+
end
|
104
|
+
|
105
|
+
# helper method for raising an error for unexpandable values
|
106
|
+
# @!visibility private
|
107
|
+
def error_for(values)
|
108
|
+
expansions = mappings.keys.map(&:inspect).join(" or ")
|
109
|
+
raise error_class, "cannot expand with keys %p, possible expansions: %s" % [values.keys.sort, expansions]
|
110
|
+
end
|
111
|
+
|
112
|
+
# @see Mustermann::AST::Translator#expand
|
113
|
+
# @!visibility private
|
114
|
+
def escape(string, *args)
|
115
|
+
# URI::Parser is pretty slow, let's not had every string to it, even if it's unnecessary
|
116
|
+
string =~ /\A\w*\Z/ ? string : super
|
117
|
+
end
|
118
|
+
|
119
|
+
# Turns a sprintf pattern into our secret internal data structure.
|
120
|
+
# @!visibility private
|
121
|
+
def pattern(string = "", *keys)
|
122
|
+
filters = keys.last.is_a?(Hash) ? keys.pop : {}
|
123
|
+
[[keys, string, filters]]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Creates the product of two of our secret internal data structures.
|
127
|
+
# @!visibility private
|
128
|
+
def add_to(list, result)
|
129
|
+
list << [[], ""] if list.empty?
|
130
|
+
list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, f1.merge(f2)] } }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|