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