mustermann 0.1.0 → 0.2.0
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.
- 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
|