mustermann19 0.3.1

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