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,108 @@
1
+ require 'mustermann/ast/node'
2
+ require 'mustermann/error'
3
+ require 'delegate'
4
+
5
+ module Mustermann
6
+ module AST
7
+ # Implements translator pattern
8
+ #
9
+ # @abstract
10
+ # @!visibility private
11
+ class Translator
12
+ # Encapsulates a single node translation
13
+ # @!visibility private
14
+ class NodeTranslator < DelegateClass(Node)
15
+ # @param [Array<Symbol, Class>] types list of types to register for.
16
+ # @!visibility private
17
+ def self.register(*types)
18
+ types.each do |type|
19
+ type = Node.constant_name(type) if type.is_a? Symbol
20
+ translator.dispatch_table[type.to_s] = self
21
+ end
22
+ end
23
+
24
+ # @param node [Mustermann::AST::Node, Object]
25
+ # @param translator [Mustermann::AST::Translator]
26
+ #
27
+ # @!visibility private
28
+ def initialize(node, translator)
29
+ @translator = translator
30
+ super(node)
31
+ end
32
+
33
+ # @!visibility private
34
+ attr_reader :translator
35
+
36
+ # shorthand for translating a nested object
37
+ # @!visibility private
38
+ def t(*args, &block)
39
+ return translator unless args.any?
40
+ translator.translate(*args, &block)
41
+ end
42
+
43
+ # @!visibility private
44
+ alias_method :node, :__getobj__
45
+ end
46
+
47
+ # maps types to translations
48
+ # @!visibility private
49
+ def self.dispatch_table
50
+ @dispatch_table ||= {}
51
+ end
52
+
53
+ # some magic sauce so {NodeTranslator}s know whom to talk to for {#register}
54
+ # @!visibility private
55
+ def self.inherited(subclass)
56
+ node_translator = Class.new(NodeTranslator)
57
+ node_translator.define_singleton_method(:translator) { subclass }
58
+ subclass.const_set(:NodeTranslator, node_translator)
59
+ super
60
+ end
61
+
62
+ # DSL-ish method for specifying the exception class to use.
63
+ # @!visibility private
64
+ def self.raises(error)
65
+ define_method(:error_class) { error }
66
+ end
67
+
68
+ # DSL method for defining single method translations.
69
+ # @!visibility private
70
+ def self.translate(*types, &block)
71
+ Class.new(const_get(:NodeTranslator)) do
72
+ register(*types)
73
+ define_method(:translate, &block)
74
+ end
75
+ end
76
+
77
+ raises Mustermann::Error
78
+
79
+ # @param [Mustermann::AST::Node, Object] node to translate
80
+ # @return decorator encapsulating translation
81
+ #
82
+ # @!visibility private
83
+ def decorator_for(node)
84
+ factory = node.class.ancestors.inject(nil) { |d,a| d || self.class.dispatch_table[a.name] }
85
+ raise error_class, "#{self.class}: Cannot translate #{node.class}" unless factory
86
+ factory.new(node, self)
87
+ end
88
+
89
+ # Start the translation dance for a (sub)tree.
90
+ # @!visibility private
91
+ def translate(node, *args, &block)
92
+ result = decorator_for(node).translate(*args, &block)
93
+ result = result.node while result.is_a? NodeTranslator
94
+ result
95
+ end
96
+
97
+ # @return [String] escaped character
98
+ # @!visibility private
99
+ def escape(char, options = {})
100
+ parser = options[:parser] || URI::DEFAULT_PARSER
101
+ escape = options[:escape] || parser.regexp[:UNSAFE]
102
+ also_escape = options[:also_escape]
103
+ escape = Regexp.union(also_escape, escape) if also_escape
104
+ char =~ escape ? parser.escape(char, Regexp.union(*escape)) : char
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,29 @@
1
+ require 'mustermann/ast/translator'
2
+
3
+ module Mustermann
4
+ module AST
5
+ # Turns an AST into a human readable string.
6
+ # @!visibility private
7
+ class TreeRenderer < Translator
8
+ # @example
9
+ # Mustermann::AST::TreeRenderer.render Mustermann::Sinatra::Parser.parse('/foo')
10
+ #
11
+ # @!visibility private
12
+ def self.render(ast)
13
+ new.translate(ast)
14
+ end
15
+
16
+ translate(Object) { inspect }
17
+ translate(Array) { map { |e| "\n" << t(e) }.join.gsub("\n", "\n ") }
18
+ translate(:node) { "#{t.type(node)} #{t(payload)}" }
19
+ translate(:with_look_ahead) { "#{t.type(node)} #{t(head)} #{t(payload)}" }
20
+
21
+ # Turns a class name into a node identifier.
22
+ #
23
+ # @!visibility private
24
+ def type(node)
25
+ node.class.name[/[^:]+$/].split(/(?<=.)(?=[A-Z])/).map(&:downcase).join(?_)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,43 @@
1
+ require 'mustermann/ast/translator'
2
+
3
+ module Mustermann
4
+ module AST
5
+ # Checks the AST for certain validations, like correct capture names.
6
+ #
7
+ # Internally a poor man's visitor (abusing translator to not have to implement a visitor).
8
+ # @!visibility private
9
+ class Validation < Translator
10
+ # Runs validations.
11
+ #
12
+ # @param [Mustermann::AST::Node] ast to be validated
13
+ # @return [Mustermann::AST::Node] the validated ast
14
+ # @raise [Mustermann::AST::CompileError] if validation fails
15
+ # @!visibility private
16
+ def self.validate(ast)
17
+ new.translate(ast)
18
+ ast
19
+ end
20
+
21
+ translate(Object, :splat) {}
22
+ translate(:node) { t(payload) }
23
+ translate(Array) { each { |p| t(p)} }
24
+ translate(:capture, :variable, :named_splat) { t.check_name(name) }
25
+
26
+ # @raise [Mustermann::CompileError] if name is not acceptable
27
+ # @!visibility private
28
+ def check_name(name)
29
+ raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
30
+ raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
31
+ raise CompileError, "capture name can't be #{name}" if name == "splat" or name == "captures"
32
+ raise CompileError, "can't use the same capture name twice" if names.include? name
33
+ names << name
34
+ end
35
+
36
+ # @return [Array<String>] list of capture names in tree
37
+ # @!visibility private
38
+ def names
39
+ @names ||= []
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,117 @@
1
+ require 'enumerable/lazy' unless Enumerable.method_defined?(:lazy)
2
+ require 'delegate'
3
+
4
+ module Mustermann
5
+ # Class for defining and running simple Hash transformations.
6
+ #
7
+ # @example
8
+ # caster = Mustermann::Caster.new
9
+ # caster.register(:foo) { |value| { bar: value.upcase } }
10
+ # caster.cast(foo: "hello", baz: "world") # => { bar: "HELLO", baz: "world" }
11
+ #
12
+ # @see Mustermann::Expander#cast
13
+ #
14
+ # @!visibility private
15
+ class Caster < DelegateClass(Array)
16
+ # @param (see #register)
17
+ # @!visibility private
18
+ def initialize(*types, &block)
19
+ super([])
20
+ register(*types, &block)
21
+ end
22
+
23
+ # @param [Array<Symbol, Regexp, #cast, #===>] types identifier for cast type (some need block)
24
+ # @!visibility private
25
+ def register(*types, &block)
26
+ types << Any.new(&block) if types.empty?
27
+ types.each { |type| self << caster_for(type, &block) }
28
+ end
29
+
30
+ # @param [Symbol, Regexp, #cast, #===] type identifier for cast type (some need block)
31
+ # @return [#cast] specific cast operation
32
+ # @!visibility private
33
+ def caster_for(type, &block)
34
+ case type
35
+ when Symbol, Regexp then Key.new(type, &block)
36
+ else type.respond_to?(:cast) ? type : Value.new(type, &block)
37
+ end
38
+ end
39
+
40
+ # Transforms a Hash.
41
+ # @param [Hash] hash pre-transform Hash
42
+ # @return [Hash] post-transform Hash
43
+ # @!visibility private
44
+ def cast(hash)
45
+ merge = {}
46
+ hash.delete_if do |key, value|
47
+ next unless casted = lazy.map { |e| e.cast(key, value) }.detect { |e| e }
48
+ casted = { key => casted } unless casted.respond_to? :to_hash
49
+ merge.update(casted.to_hash)
50
+ end
51
+ hash.update(merge)
52
+ end
53
+
54
+ # Specific cast for remove nil values.
55
+ # @!visibility private
56
+ module Nil
57
+ # @see Mustermann::Caster#cast
58
+ # @!visibility private
59
+ def self.cast(key, value)
60
+ {} if value.nil?
61
+ end
62
+ end
63
+
64
+ # Class for block based casts that are triggered for every key/value pair.
65
+ # @!visibility private
66
+ class Any
67
+ # @!visibility private
68
+ def initialize(&block)
69
+ @block = block
70
+ end
71
+
72
+ # @see Mustermann::Caster#cast
73
+ # @!visibility private
74
+ def cast(key, value)
75
+ case @block.arity
76
+ when 0 then @block.call
77
+ when 1 then @block.call(value)
78
+ else @block.call(key, value)
79
+ end
80
+ end
81
+ end
82
+
83
+ # Class for block based casts that are triggered for key/value pairs with a matching value.
84
+ # @!visibility private
85
+ class Value < Any
86
+ # @param [#===] type used for matching values
87
+ # @!visibility private
88
+ def initialize(type, &block)
89
+ @type = type
90
+ super(&block)
91
+ end
92
+
93
+ # @see Mustermann::Caster#cast
94
+ # @!visibility private
95
+ def cast(key, value)
96
+ super if @type === value
97
+ end
98
+ end
99
+
100
+ # Class for block based casts that are triggered for key/value pairs with a matching key.
101
+ # @!visibility private
102
+ class Key < Any
103
+ # @param [#===] type used for matching keys
104
+ # @!visibility private
105
+ def initialize(type, &block)
106
+ @type = type
107
+ super(&block)
108
+ end
109
+
110
+ # @see Mustermann::Caster#cast
111
+ # @!visibility private
112
+ def cast(key, value)
113
+ super if @type === key
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,48 @@
1
+ module Mustermann
2
+ # A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity.
3
+ # Used for caching.
4
+ #
5
+ # @see #fetch
6
+ # @!visibility private
7
+ class EqualityMap
8
+ MAP_CLASS = defined?(ObjectSpace::WeakMap) ? ObjectSpace::WeakMap : Hash
9
+
10
+ # @!visibility private
11
+ def initialize
12
+ @keys = {}
13
+ @map = MAP_CLASS.new
14
+ end
15
+
16
+ # @param [Array<#hash>] key for caching
17
+ # @yield block that will be called to populate entry if missing
18
+ # @return value stored in map or result of block
19
+ # @!visibility private
20
+ def fetch(*key)
21
+ identity = @keys[key.hash]
22
+ key = identity == key ? identity : key
23
+
24
+ # it is ok that this is not thread-safe, worst case it has double cost in
25
+ # generating, object equality is not guaranteed anyways
26
+ @map[key] ||= track(key, yield)
27
+ end
28
+
29
+ # @param [#hash] key for identifying the object
30
+ # @param [Object] object to be stored
31
+ # @return [Object] same as the second parameter
32
+ def track(key, object)
33
+ ObjectSpace.define_finalizer(object, finalizer(key.hash))
34
+ @keys[key.hash] = key
35
+ object
36
+ end
37
+
38
+ # Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object.
39
+ #
40
+ # @param [Fixnum] hash for key
41
+ # @return [Proc] finalizer callback
42
+ def finalizer(hash)
43
+ proc { @keys.delete(hash) }
44
+ end
45
+
46
+ private :track, :finalizer
47
+ end
48
+ end
@@ -0,0 +1,6 @@
1
+ module Mustermann
2
+ Error ||= Class.new(StandardError) # Raised if anything goes wrong while generating a {Pattern}.
3
+ CompileError ||= Class.new(Error) # Raised if anything goes wrong while compiling a {Pattern}.
4
+ ParseError ||= Class.new(Error) # Raised if anything goes wrong while parsing a {Pattern}.
5
+ ExpandError ||= Class.new(Error) # Raised if anything goes wrong while expanding a {Pattern}.
6
+ end
@@ -0,0 +1,206 @@
1
+ require 'mustermann/ast/expander'
2
+ require 'mustermann/caster'
3
+ require 'mustermann'
4
+
5
+ module Mustermann
6
+ # Allows fine-grained control over pattern expansion.
7
+ #
8
+ # @example
9
+ # expander = Mustermann::Expander.new(additional_values: :append)
10
+ # expander << "/users/:user_id"
11
+ # expander << "/pages/:page_id"
12
+ #
13
+ # expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5"
14
+ class Expander
15
+ attr_reader :patterns, :additional_values, :caster
16
+
17
+ # @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}.
18
+ # @param [Symbol] additional_values behavior when encountering additional values, see {#expand}.
19
+ # @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}.
20
+ def initialize(*patterns)
21
+ options = patterns.last.is_a?(Hash) ? patterns.pop : {}
22
+ additional_values = options.delete(:additional_values) || :raise
23
+ unless additional_values == :raise or additional_values == :ignore or additional_values == :append
24
+ raise ArgumentError, "Illegal value %p for additional_values" % additional_values
25
+ end
26
+
27
+ @patterns = []
28
+ @api_expander = AST::Expander.new
29
+ @additional_values = additional_values
30
+ @options = options
31
+ @caster = Caster.new(Caster::Nil)
32
+ add(*patterns)
33
+ end
34
+
35
+ # Add patterns to expand.
36
+ #
37
+ # @example
38
+ # expander = Mustermann::Expander.new
39
+ # expander.add("/:a.jpg", "/:b.png")
40
+ # expander.expand(a: "pony") # => "/pony.jpg"
41
+ #
42
+ # @param [Array<#to_str, Mustermann::Pattern>] patterns list of to add for expansion, Strings will be compiled to patterns.
43
+ # @return [Mustermann::Expander] the expander
44
+ def add(*patterns)
45
+ patterns.each do |pattern|
46
+ pattern = Mustermann.new(pattern.to_str, @options) if pattern.respond_to? :to_str
47
+ raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
48
+ @api_expander.add(pattern.to_ast)
49
+ @patterns << pattern
50
+ end
51
+ self
52
+ end
53
+
54
+ alias_method :<<, :add
55
+
56
+ # Register a block as simple hash transformation that runs before expanding the pattern.
57
+ # @return [Mustermann::Expander] the expander
58
+ #
59
+ # @overload cast
60
+ # Register a block as simple hash transformation that runs before expanding the pattern for all entries.
61
+ #
62
+ # @example casting everything that implements to_param to param
63
+ # expander.cast { |o| o.to_param if o.respond_to? :to_param }
64
+ #
65
+ # @yield every key/value pair
66
+ # @yieldparam key [Symbol] omitted if block takes less than 2
67
+ # @yieldparam value [Object] omitted if block takes no arguments
68
+ # @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
69
+ # @yieldreturn [nil, false] will keep key/value pair in hash
70
+ # @yieldreturn [Object] will replace value with returned object
71
+ #
72
+ # @overload cast(*type_matchers)
73
+ # Register a block as simple hash transformation that runs before expanding the pattern for certain entries.
74
+ #
75
+ # @example convert user to user_id
76
+ # expander = Mustermann::Expander.new('/users/:user_id')
77
+ # expand.cast(:user) { |user| { user_id: user.id } }
78
+ #
79
+ # expand.expand(user: User.current) # => "/users/42"
80
+ #
81
+ # @example convert user, page, image to user_id, page_id, image_id
82
+ # expander = Mustermann::Expander.new('/users/:user_id', '/pages/:page_id', '/:image_id.jpg')
83
+ # expand.cast(:user, :page, :image) { |key, value| { "#{key}_id".to_sym => value.id } }
84
+ #
85
+ # expand.expand(user: User.current) # => "/users/42"
86
+ #
87
+ # @example casting to multiple key/value pairs
88
+ # expander = Mustermann::Expander.new('/users/:user_id/:image_id.:format')
89
+ # expander.cast(:image) { |i| { user_id: i.owner.id, image_id: i.id, format: i.format } }
90
+ #
91
+ # expander.expander(image: User.current.avatar) # => "/users/42/avatar.jpg"
92
+ #
93
+ # @example casting all ActiveRecord objects to param
94
+ # expander.cast(ActiveRecord::Base, &:to_param)
95
+ #
96
+ # @param [Array<Symbol, Regexp, #===>] type_matchers
97
+ # To identify key/value pairs to match against.
98
+ # Regexps and Symbols match against key, everything else matches against value.
99
+ #
100
+ # @yield every key/value pair
101
+ # @yieldparam key [Symbol] omitted if block takes less than 2
102
+ # @yieldparam value [Object] omitted if block takes no arguments
103
+ # @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
104
+ # @yieldreturn [nil, false] will keep key/value pair in hash
105
+ # @yieldreturn [Object] will replace value with returned object
106
+ #
107
+ # @overload cast(*cast_objects)
108
+ #
109
+ # @param [Array<#cast>] cast_objects
110
+ # Before expanding, will call #cast on these objects for each key/value pair.
111
+ # Return value will be treated same as block return values described above.
112
+ def cast(*types, &block)
113
+ caster.register(*types, &block)
114
+ self
115
+ end
116
+
117
+ # @example Expanding a pattern
118
+ # pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
119
+ # pattern.expand(name: 'hello') # => "/hello"
120
+ # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
121
+ #
122
+ # @example Handling additional values
123
+ # pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
124
+ # pattern.expand(:ignore, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png"
125
+ # pattern.expand(:append, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
126
+ # pattern.expand(:raise, name: 'hello', ext: 'png', scale: '2x') # raises Mustermann::ExpandError
127
+ #
128
+ # @example Setting additional values behavior for the expander object
129
+ # pattern = Mustermann::Expander.new('/:name', '/:name.:ext', additional_values: :append)
130
+ # pattern.expand(name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
131
+ #
132
+ # @param [Symbol] behavior
133
+ # What to do with additional key/value pairs not present in the values hash.
134
+ # Possible options: :raise, :ignore, :append.
135
+ #
136
+ # @param [Hash{Symbol: #to_s, Array<#to_s>}] values
137
+ # Values to use for expansion.
138
+ #
139
+ # @return [String] expanded string
140
+ # @raise [NotImplementedError] raised if expand is not supported.
141
+ # @raise [Mustermann::ExpandError] raised if a value is missing or unknown
142
+ def expand(behavior = nil, values = {})
143
+ values, behavior = behavior, nil if behavior.is_a?(Hash)
144
+ values = map_values(values)
145
+
146
+ case behavior || additional_values
147
+ when :raise then @api_expander.expand(values)
148
+ when :ignore then with_rest(values) { |uri, rest| uri }
149
+ when :append then with_rest(values) { |uri, rest| append(uri, rest) }
150
+ else raise ArgumentError, "unknown behavior %p" % behavior
151
+ end
152
+ end
153
+
154
+ # @see Object#==
155
+ def ==(other)
156
+ return false unless other.class == self.class
157
+ other.patterns == patterns and other.additional_values == additional_values
158
+ end
159
+
160
+ # @see Object#eql?
161
+ def eql?(other)
162
+ return false unless other.class == self.class
163
+ other.patterns.eql? patterns and other.additional_values.eql? additional_values
164
+ end
165
+
166
+ # @see Object#hash
167
+ def hash
168
+ patterns.hash + additional_values.hash
169
+ end
170
+
171
+ def expandable?(values)
172
+ return false unless values
173
+ expandable, _ = split_values(map_values(values))
174
+ @api_expander.expandable? expandable
175
+ end
176
+
177
+ def with_rest(values)
178
+ expandable, non_expandable = split_values(values)
179
+ yield expand(:raise, slice(values, expandable)), slice(values, non_expandable)
180
+ end
181
+
182
+ def split_values(values)
183
+ expandable = @api_expander.expandable_keys(values.keys)
184
+ non_expandable = values.keys - expandable
185
+ [expandable, non_expandable]
186
+ end
187
+
188
+ def slice(hash, keys)
189
+ Hash[keys.map { |k| [k, hash[k]] }]
190
+ end
191
+
192
+ def append(uri, values)
193
+ return uri unless values and values.any?
194
+ entries = values.map { |pair| pair.map { |e| @api_expander.escape(e, also_escape: /[\/\?#\&\=%]/) }.join(?=) }
195
+ "#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }"
196
+ end
197
+
198
+ def map_values(values)
199
+ values = values.dup
200
+ @api_expander.keys.each { |key| values[key] ||= values.delete(key.to_s) if values.include? key.to_s }
201
+ caster.cast(values)
202
+ end
203
+
204
+ private :with_rest, :slice, :append, :caster, :map_values, :split_values
205
+ end
206
+ end