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.
Files changed (69) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +10 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +22 -0
  7. data/README.md +1081 -0
  8. data/Rakefile +6 -0
  9. data/bench/capturing.rb +57 -0
  10. data/bench/regexp.rb +21 -0
  11. data/bench/simple_vs_sinatra.rb +23 -0
  12. data/bench/template_vs_addressable.rb +26 -0
  13. data/internals.md +64 -0
  14. data/lib/mustermann.rb +61 -0
  15. data/lib/mustermann/ast/compiler.rb +168 -0
  16. data/lib/mustermann/ast/expander.rb +134 -0
  17. data/lib/mustermann/ast/node.rb +160 -0
  18. data/lib/mustermann/ast/parser.rb +137 -0
  19. data/lib/mustermann/ast/pattern.rb +84 -0
  20. data/lib/mustermann/ast/transformer.rb +129 -0
  21. data/lib/mustermann/ast/translator.rb +108 -0
  22. data/lib/mustermann/ast/tree_renderer.rb +29 -0
  23. data/lib/mustermann/ast/validation.rb +43 -0
  24. data/lib/mustermann/caster.rb +117 -0
  25. data/lib/mustermann/equality_map.rb +48 -0
  26. data/lib/mustermann/error.rb +6 -0
  27. data/lib/mustermann/expander.rb +206 -0
  28. data/lib/mustermann/extension.rb +52 -0
  29. data/lib/mustermann/identity.rb +19 -0
  30. data/lib/mustermann/mapper.rb +98 -0
  31. data/lib/mustermann/pattern.rb +182 -0
  32. data/lib/mustermann/rails.rb +17 -0
  33. data/lib/mustermann/regexp_based.rb +30 -0
  34. data/lib/mustermann/regular.rb +26 -0
  35. data/lib/mustermann/router.rb +9 -0
  36. data/lib/mustermann/router/rack.rb +50 -0
  37. data/lib/mustermann/router/simple.rb +144 -0
  38. data/lib/mustermann/shell.rb +29 -0
  39. data/lib/mustermann/simple.rb +38 -0
  40. data/lib/mustermann/simple_match.rb +30 -0
  41. data/lib/mustermann/sinatra.rb +22 -0
  42. data/lib/mustermann/template.rb +48 -0
  43. data/lib/mustermann/to_pattern.rb +45 -0
  44. data/lib/mustermann/version.rb +3 -0
  45. data/mustermann.gemspec +31 -0
  46. data/spec/expander_spec.rb +105 -0
  47. data/spec/extension_spec.rb +296 -0
  48. data/spec/identity_spec.rb +83 -0
  49. data/spec/mapper_spec.rb +83 -0
  50. data/spec/mustermann_spec.rb +65 -0
  51. data/spec/pattern_spec.rb +49 -0
  52. data/spec/rails_spec.rb +522 -0
  53. data/spec/regexp_based_spec.rb +8 -0
  54. data/spec/regular_spec.rb +36 -0
  55. data/spec/router/rack_spec.rb +39 -0
  56. data/spec/router/simple_spec.rb +32 -0
  57. data/spec/shell_spec.rb +109 -0
  58. data/spec/simple_match_spec.rb +10 -0
  59. data/spec/simple_spec.rb +237 -0
  60. data/spec/sinatra_spec.rb +574 -0
  61. data/spec/support.rb +5 -0
  62. data/spec/support/coverage.rb +16 -0
  63. data/spec/support/env.rb +15 -0
  64. data/spec/support/expand_matcher.rb +27 -0
  65. data/spec/support/match_matcher.rb +39 -0
  66. data/spec/support/pattern.rb +39 -0
  67. data/spec/template_spec.rb +815 -0
  68. data/spec/to_pattern_spec.rb +20 -0
  69. metadata +301 -0
@@ -0,0 +1,6 @@
1
+ ENV['JRUBY_OPTS'] = '--2.0 -X-C'
2
+ ENV['RBXOPT'] = '-X20'
3
+
4
+ task(:spec) { ruby '-w -S rspec' }
5
+ task(:doc_stats) { ruby '-S yard stats' }
6
+ task(default: [:spec, :doc_stats])
@@ -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
@@ -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
@@ -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
+ ...
@@ -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