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,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