mustermann 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +166 -16
- data/Rakefile +1 -1
- data/bench/capturing.rb +4 -4
- data/bench/regexp.rb +21 -0
- data/lib/mustermann/ast/expander.rb +9 -3
- data/lib/mustermann/ast/node.rb +0 -1
- data/lib/mustermann/ast/parser.rb +5 -6
- data/lib/mustermann/ast/pattern.rb +17 -21
- data/lib/mustermann/ast/transformer.rb +31 -23
- data/lib/mustermann/ast/translator.rb +2 -2
- data/lib/mustermann/ast/validation.rb +8 -5
- data/lib/mustermann/caster.rb +116 -0
- data/lib/mustermann/equality_map.rb +46 -0
- data/lib/mustermann/expander.rb +169 -0
- data/lib/mustermann/pattern.rb +7 -4
- data/lib/mustermann/regexp_based.rb +2 -2
- data/lib/mustermann/router.rb +9 -0
- data/lib/mustermann/router/rack.rb +47 -0
- data/lib/mustermann/router/simple.rb +142 -0
- data/lib/mustermann/shell.rb +9 -2
- data/lib/mustermann/simple.rb +2 -2
- data/lib/mustermann/version.rb +1 -1
- data/mustermann.gemspec +1 -0
- data/spec/expander_spec.rb +77 -0
- data/spec/router/rack_spec.rb +39 -0
- data/spec/router/simple_spec.rb +30 -0
- data/spec/support/coverage.rb +12 -14
- data/spec/support/pattern.rb +10 -3
- metadata +30 -3
@@ -5,43 +5,51 @@ module Mustermann
|
|
5
5
|
# Takes a tree, turns it into an even better tree.
|
6
6
|
# @!visibility private
|
7
7
|
class Transformer < Translator
|
8
|
-
# @!visibility private
|
9
|
-
Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
|
10
|
-
|
11
|
-
# Operators available for expressions.
|
12
|
-
# @!visibility private
|
13
|
-
OPERATORS ||= {
|
14
|
-
nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
|
15
|
-
?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
|
16
|
-
?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
|
17
|
-
?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
|
18
|
-
}
|
19
8
|
|
20
9
|
# Transforms a tree.
|
21
10
|
# @note might mutate handed in tree instead of creating a new one
|
22
11
|
# @param [Mustermann::AST::Node] tree to be transformed
|
23
12
|
# @return [Mustermann::AST::Node] transformed tree
|
24
13
|
# @!visibility private
|
25
|
-
def self.transform(
|
26
|
-
new.translate(
|
14
|
+
def self.transform(tree)
|
15
|
+
new.translate(tree)
|
27
16
|
end
|
28
17
|
|
29
18
|
translate(:node) { self }
|
30
|
-
|
31
|
-
translate(:expression) do
|
32
|
-
self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
|
33
|
-
separator = Node[:separator].new(operator.separator)
|
34
|
-
prefix = Node[:separator].new(operator.prefix)
|
35
|
-
self.payload = Array(payload.inject { |list, element| Array(list) << t(separator) << t(element) })
|
36
|
-
payload.unshift(prefix) if operator.prefix
|
37
|
-
self
|
38
|
-
end
|
39
|
-
|
40
19
|
translate(:group, :root) do
|
41
20
|
self.payload = t(payload)
|
42
21
|
self
|
43
22
|
end
|
44
23
|
|
24
|
+
# URI expression transformations depending on operator
|
25
|
+
# @!visibility private
|
26
|
+
class ExpressionTransform < NodeTranslator
|
27
|
+
register :expression
|
28
|
+
|
29
|
+
# @!visibility private
|
30
|
+
Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
|
31
|
+
|
32
|
+
# Operators available for expressions.
|
33
|
+
# @!visibility private
|
34
|
+
OPERATORS ||= {
|
35
|
+
nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
|
36
|
+
?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
|
37
|
+
?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
|
38
|
+
?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
|
39
|
+
}
|
40
|
+
|
41
|
+
# Sets operator and inserts separators in between variables.
|
42
|
+
# @!visibility private
|
43
|
+
def translate
|
44
|
+
self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
|
45
|
+
separator = Node[:separator].new(operator.separator)
|
46
|
+
prefix = Node[:separator].new(operator.prefix)
|
47
|
+
self.payload = Array(payload.inject { |list, element| Array(list) << t(separator) << t(element) })
|
48
|
+
payload.unshift(prefix) if operator.prefix
|
49
|
+
self
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
45
53
|
# Inserts with_look_ahead nodes wherever appropriate
|
46
54
|
# @!visibility private
|
47
55
|
class ArrayTransform < NodeTranslator
|
@@ -12,7 +12,7 @@ module Mustermann
|
|
12
12
|
# Encapsulates a single node translation
|
13
13
|
# @!visibility private
|
14
14
|
class NodeTranslator < DelegateClass(Node)
|
15
|
-
# @param [Array<Symbol, Class>] list of types to register for.
|
15
|
+
# @param [Array<Symbol, Class>] types list of types to register for.
|
16
16
|
# @!visibility private
|
17
17
|
def self.register(*types)
|
18
18
|
types.each do |type|
|
@@ -76,7 +76,7 @@ module Mustermann
|
|
76
76
|
|
77
77
|
raises Mustermann::Error
|
78
78
|
|
79
|
-
# @param [Mustermann::AST::Node, Object]
|
79
|
+
# @param [Mustermann::AST::Node, Object] node to translate
|
80
80
|
# @return decorator encapsulating translation
|
81
81
|
#
|
82
82
|
# @!visibility private
|
@@ -4,14 +4,14 @@ module Mustermann
|
|
4
4
|
module AST
|
5
5
|
# Checks the AST for certain validations, like correct capture names.
|
6
6
|
#
|
7
|
-
# Internally a poor man's visitor (abusing translator to not have to
|
7
|
+
# Internally a poor man's visitor (abusing translator to not have to implement a visitor).
|
8
8
|
# @!visibility private
|
9
9
|
class Validation < Translator
|
10
10
|
# Runs validations.
|
11
11
|
#
|
12
12
|
# @param [Mustermann::AST::Node] ast to be validated
|
13
13
|
# @return [Mustermann::AST::Node] the validated ast
|
14
|
-
# @
|
14
|
+
# @raise [Mustermann::AST::CompileError] if validation fails
|
15
15
|
# @!visibility private
|
16
16
|
def self.validate(ast)
|
17
17
|
new.translate(ast)
|
@@ -21,13 +21,16 @@ module Mustermann
|
|
21
21
|
translate(Object, :splat) {}
|
22
22
|
translate(:node) { t(payload) }
|
23
23
|
translate(Array) { each { |p| t(p)} }
|
24
|
+
translate(:capture, :variable, :named_splat) { t.check_name(name) }
|
24
25
|
|
25
|
-
|
26
|
+
# @raise [Mustermann::CompileError] if name is not acceptable
|
27
|
+
# @!visibility private
|
28
|
+
def check_name(name)
|
26
29
|
raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
|
27
30
|
raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
|
28
31
|
raise CompileError, "capture name can't be #{name}" if name == "splat" or name == "captures"
|
29
|
-
raise CompileError, "can't use the same capture name twice" if
|
30
|
-
|
32
|
+
raise CompileError, "can't use the same capture name twice" if names.include? name
|
33
|
+
names << name
|
31
34
|
end
|
32
35
|
|
33
36
|
# @return [Array<String>] list of capture names in tree
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# Class for defining and running simple Hash transformations.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# caster = Mustermann::Caster.new
|
8
|
+
# caster.register(:foo) { |value| { bar: value.upcase } }
|
9
|
+
# caster.cast(foo: "hello", baz: "world") # => { bar: "HELLO", baz: "world" }
|
10
|
+
#
|
11
|
+
# @see Mustermann::Expander#cast
|
12
|
+
#
|
13
|
+
# @!visibility private
|
14
|
+
class Caster < DelegateClass(Array)
|
15
|
+
# @param (see #register)
|
16
|
+
# @!visibility private
|
17
|
+
def initialize(*types, &block)
|
18
|
+
super([])
|
19
|
+
register(*types, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Array<Symbol, Regexp, #cast, #===>] types identifier for cast type (some need block)
|
23
|
+
# @!visibility private
|
24
|
+
def register(*types, &block)
|
25
|
+
types << Any.new(&block) if types.empty?
|
26
|
+
types.each { |type| self << caster_for(type, &block) }
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [Symbol, Regexp, #cast, #===] type identifier for cast type (some need block)
|
30
|
+
# @return [#cast] specific cast operation
|
31
|
+
# @!visibility private
|
32
|
+
def caster_for(type, &block)
|
33
|
+
case type
|
34
|
+
when Symbol, Regexp then Key.new(type, &block)
|
35
|
+
else type.respond_to?(:cast) ? type : Value.new(type, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Transforms a Hash.
|
40
|
+
# @param [Hash] hash pre-transform Hash
|
41
|
+
# @return [Hash] post-transform Hash
|
42
|
+
# @!visibility private
|
43
|
+
def cast(hash)
|
44
|
+
merge = {}
|
45
|
+
hash.delete_if do |key, value|
|
46
|
+
next unless casted = lazy.map { |e| e.cast(key, value) }.detect { |e| e }
|
47
|
+
casted = { key => casted } unless casted.respond_to? :to_hash
|
48
|
+
merge.update(casted.to_hash)
|
49
|
+
end
|
50
|
+
hash.update(merge)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Specific cast for remove nil values.
|
54
|
+
# @!visibility private
|
55
|
+
module Nil
|
56
|
+
# @see Mustermann::Caster#cast
|
57
|
+
# @!visibility private
|
58
|
+
def self.cast(key, value)
|
59
|
+
{} if value.nil?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Class for block based casts that are triggered for every key/value pair.
|
64
|
+
# @!visibility private
|
65
|
+
class Any
|
66
|
+
# @!visibility private
|
67
|
+
def initialize(&block)
|
68
|
+
@block = block
|
69
|
+
end
|
70
|
+
|
71
|
+
# @see Mustermann::Caster#cast
|
72
|
+
# @!visibility private
|
73
|
+
def cast(key, value)
|
74
|
+
case @block.arity
|
75
|
+
when 0 then @block.call
|
76
|
+
when 1 then @block.call(value)
|
77
|
+
else @block.call(key, value)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Class for block based casts that are triggered for key/value pairs with a matching value.
|
83
|
+
# @!visibility private
|
84
|
+
class Value < Any
|
85
|
+
# @param [#===] type used for matching values
|
86
|
+
# @!visibility private
|
87
|
+
def initialize(type, &block)
|
88
|
+
@type = type
|
89
|
+
super(&block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @see Mustermann::Caster#cast
|
93
|
+
# @!visibility private
|
94
|
+
def cast(key, value)
|
95
|
+
super if @type === value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Class for block based casts that are triggered for key/value pairs with a matching key.
|
100
|
+
# @!visibility private
|
101
|
+
class Key < Any
|
102
|
+
# @param [#===] type used for matching keys
|
103
|
+
# @!visibility private
|
104
|
+
def initialize(type, &block)
|
105
|
+
@type = type
|
106
|
+
super(&block)
|
107
|
+
end
|
108
|
+
|
109
|
+
# @see Mustermann::Caster#cast
|
110
|
+
# @!visibility private
|
111
|
+
def cast(key, value)
|
112
|
+
super if @type === key
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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
|
+
# @!visibility private
|
9
|
+
def initialize
|
10
|
+
@keys = {}
|
11
|
+
@map = ObjectSpace::WeakMap.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [Array<#hash>] key for caching
|
15
|
+
# @yield block that will be called to populate entry if missing
|
16
|
+
# @return value stored in map or result of block
|
17
|
+
# @!visibility private
|
18
|
+
def fetch(*key)
|
19
|
+
identity = @keys[key.hash]
|
20
|
+
key = identity == key ? identity : key
|
21
|
+
|
22
|
+
# it is ok that this is not thread-safe, worst case it has double cost in
|
23
|
+
# generating, object equality is not guaranteed anyways
|
24
|
+
@map[key] ||= track(key, yield)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [#hash] key for identifying the object
|
28
|
+
# @param [Object] object to be stored
|
29
|
+
# @return [Object] same as the second parameter
|
30
|
+
def track(key, object)
|
31
|
+
ObjectSpace.define_finalizer(object, finalizer(hash))
|
32
|
+
@keys[key.hash] = key
|
33
|
+
object
|
34
|
+
end
|
35
|
+
|
36
|
+
# Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object.
|
37
|
+
#
|
38
|
+
# @param [Fixnum] hash for key
|
39
|
+
# @return [Proc] finalizer callback
|
40
|
+
def finalizer(hash)
|
41
|
+
proc { @keys.delete(hash) }
|
42
|
+
end
|
43
|
+
|
44
|
+
private :track, :finalizer
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,169 @@
|
|
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, additional_values: :raise, **options)
|
21
|
+
unless additional_values == :raise or additional_values == :ignore or additional_values == :append
|
22
|
+
raise ArgumentError, "Illegal value %p for additional_values" % additional_values
|
23
|
+
end
|
24
|
+
|
25
|
+
@patterns = []
|
26
|
+
@api_expander = AST::Expander.new
|
27
|
+
@additional_values = additional_values
|
28
|
+
@options = options
|
29
|
+
@caster = Caster.new(Caster::Nil)
|
30
|
+
add(*patterns)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add patterns to expand.
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# expander = Mustermann::Expander.new
|
37
|
+
# expander.add("/:a.jpg", "/:b.png")
|
38
|
+
# expander.expand(a: "pony") # => "/pony.jpg"
|
39
|
+
#
|
40
|
+
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of to add for expansion, Strings will be compiled to patterns.
|
41
|
+
# @return [Mustermann::Expander] the expander
|
42
|
+
def add(*patterns)
|
43
|
+
patterns.each do |pattern|
|
44
|
+
pattern = Mustermann.new(pattern.to_str, **@options) if pattern.respond_to? :to_str
|
45
|
+
raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
|
46
|
+
@api_expander.add(pattern.to_ast)
|
47
|
+
@patterns << pattern
|
48
|
+
end
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
alias_method :<<, :add
|
53
|
+
|
54
|
+
# Register a block as simple hash transformation that runs before expanding the pattern.
|
55
|
+
# @return [Mustermann::Expander] the expander
|
56
|
+
#
|
57
|
+
# @overload cast
|
58
|
+
# Register a block as simple hash transformation that runs before expanding the pattern for all entries.
|
59
|
+
#
|
60
|
+
# @example casting everything that implements to_param to param
|
61
|
+
# expander.cast { |o| o.to_param if o.respond_to? :to_param }
|
62
|
+
#
|
63
|
+
# @yield every key/value pair
|
64
|
+
# @yieldparam key [Symbol] omitted if block takes less than 2
|
65
|
+
# @yieldparam value [Object] omitted if block takes no arguments
|
66
|
+
# @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
|
67
|
+
# @yieldreturn [nil, false] will keep key/value pair in hash
|
68
|
+
# @yieldreturn [Object] will replace value with returned object
|
69
|
+
#
|
70
|
+
# @overload cast(*type_matchers)
|
71
|
+
# Register a block as simple hash transformation that runs before expanding the pattern for certain entries.
|
72
|
+
#
|
73
|
+
# @example convert user to user_id
|
74
|
+
# expander = Mustermann::Expander.new('/users/:user_id')
|
75
|
+
# expand.cast(:user) { |user| { user_id: user.id } }
|
76
|
+
#
|
77
|
+
# expand.expand(user: User.current) # => "/users/42"
|
78
|
+
#
|
79
|
+
# @example convert user, page, image to user_id, page_id, image_id
|
80
|
+
# expander = Mustermann::Expander.new('/users/:user_id', '/pages/:page_id', '/:image_id.jpg')
|
81
|
+
# expand.cast(:user, :page, :image) { |key, value| { "#{key}_id".to_sym => value.id } }
|
82
|
+
#
|
83
|
+
# expand.expand(user: User.current) # => "/users/42"
|
84
|
+
#
|
85
|
+
# @example casting to multiple key/value pairs
|
86
|
+
# expander = Mustermann::Expander.new('/users/:user_id/:image_id.:format')
|
87
|
+
# expander.cast(:image) { |i| { user_id: i.owner.id, image_id: i.id, format: i.format } }
|
88
|
+
#
|
89
|
+
# expander.expander(image: User.current.avatar) # => "/users/42/avatar.jpg"
|
90
|
+
#
|
91
|
+
# @example casting all ActiveRecord objects to param
|
92
|
+
# expander.cast(ActiveRecord::Base, &:to_param)
|
93
|
+
#
|
94
|
+
# @param [Array<Symbol, Regexp, #===>] type_matchers
|
95
|
+
# To identify key/value pairs to match against.
|
96
|
+
# Regexps and Symbols match againg key, everything else matches against value.
|
97
|
+
#
|
98
|
+
# @yield every key/value pair
|
99
|
+
# @yieldparam key [Symbol] omitted if block takes less than 2
|
100
|
+
# @yieldparam value [Object] omitted if block takes no arguments
|
101
|
+
# @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
|
102
|
+
# @yieldreturn [nil, false] will keep key/value pair in hash
|
103
|
+
# @yieldreturn [Object] will replace value with returned object
|
104
|
+
#
|
105
|
+
# @overload cast(*cast_objects)
|
106
|
+
#
|
107
|
+
# @param [Array<#cast>] cast_objects
|
108
|
+
# Before expanding, will call #cast on these objects for each key/value pair.
|
109
|
+
# Return value will be treated same as block return values described above.
|
110
|
+
def cast(*types, &block)
|
111
|
+
caster.register(*types, &block)
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# @example Expanding a pattern
|
116
|
+
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
|
117
|
+
# pattern.expand(name: 'hello') # => "/hello"
|
118
|
+
# pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
|
119
|
+
#
|
120
|
+
# @example Handling additional values
|
121
|
+
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
|
122
|
+
# pattern.expand(:ignore, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png"
|
123
|
+
# pattern.expand(:append, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
|
124
|
+
# pattern.expand(:raise, name: 'hello', ext: 'png', scale: '2x') # raises Mustermann::ExpandError
|
125
|
+
#
|
126
|
+
# @example Setting additional values behavior for the expander object
|
127
|
+
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext', additional_values: :append)
|
128
|
+
# pattern.expand(name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
|
129
|
+
#
|
130
|
+
# @param [Symbol] behavior
|
131
|
+
# What to do with additional key/value pairs not present in the values hash.
|
132
|
+
# Possible options: :raise, :ignore, :append.
|
133
|
+
#
|
134
|
+
# @param [Hash{Symbol: #to_s, Array<#to_s>}] values
|
135
|
+
# Values to use for expansion.
|
136
|
+
#
|
137
|
+
# @return [String] expanded string
|
138
|
+
# @raise [NotImplementedError] raised if expand is not supported.
|
139
|
+
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
140
|
+
def expand(behavior = nil, **values)
|
141
|
+
values = caster.cast(values)
|
142
|
+
|
143
|
+
case behavior || additional_values
|
144
|
+
when :raise then @api_expander.expand(values)
|
145
|
+
when :ignore then with_rest(values) { |uri, rest| uri }
|
146
|
+
when :append then with_rest(values) { |uri, rest| append(uri, rest) }
|
147
|
+
else raise ArgumentError, "unknown behavior %p" % behavior
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def with_rest(values)
|
152
|
+
expandable = @api_expander.expandable_keys(values.keys)
|
153
|
+
non_expandable = values.keys - expandable
|
154
|
+
yield expand(:raise, slice(values, expandable)), slice(values, non_expandable)
|
155
|
+
end
|
156
|
+
|
157
|
+
def slice(hash, keys)
|
158
|
+
Hash[keys.map { |k| [k, hash[k]] }]
|
159
|
+
end
|
160
|
+
|
161
|
+
def append(uri, values)
|
162
|
+
return uri unless values and values.any?
|
163
|
+
entries = values.map { |pair| pair.map { |e| @api_expander.escape(e, also_escape: /[\/\?#\&\=%]/) }.join(?=) }
|
164
|
+
"#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }"
|
165
|
+
end
|
166
|
+
|
167
|
+
private :with_rest, :slice, :append, :caster
|
168
|
+
end
|
169
|
+
end
|