mustermann19 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/.yardopts +1 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +1081 -0
- data/Rakefile +6 -0
- data/bench/capturing.rb +57 -0
- data/bench/regexp.rb +21 -0
- data/bench/simple_vs_sinatra.rb +23 -0
- data/bench/template_vs_addressable.rb +26 -0
- data/internals.md +64 -0
- data/lib/mustermann.rb +61 -0
- data/lib/mustermann/ast/compiler.rb +168 -0
- data/lib/mustermann/ast/expander.rb +134 -0
- data/lib/mustermann/ast/node.rb +160 -0
- data/lib/mustermann/ast/parser.rb +137 -0
- data/lib/mustermann/ast/pattern.rb +84 -0
- data/lib/mustermann/ast/transformer.rb +129 -0
- data/lib/mustermann/ast/translator.rb +108 -0
- data/lib/mustermann/ast/tree_renderer.rb +29 -0
- data/lib/mustermann/ast/validation.rb +43 -0
- data/lib/mustermann/caster.rb +117 -0
- data/lib/mustermann/equality_map.rb +48 -0
- data/lib/mustermann/error.rb +6 -0
- data/lib/mustermann/expander.rb +206 -0
- data/lib/mustermann/extension.rb +52 -0
- data/lib/mustermann/identity.rb +19 -0
- data/lib/mustermann/mapper.rb +98 -0
- data/lib/mustermann/pattern.rb +182 -0
- data/lib/mustermann/rails.rb +17 -0
- data/lib/mustermann/regexp_based.rb +30 -0
- data/lib/mustermann/regular.rb +26 -0
- data/lib/mustermann/router.rb +9 -0
- data/lib/mustermann/router/rack.rb +50 -0
- data/lib/mustermann/router/simple.rb +144 -0
- data/lib/mustermann/shell.rb +29 -0
- data/lib/mustermann/simple.rb +38 -0
- data/lib/mustermann/simple_match.rb +30 -0
- data/lib/mustermann/sinatra.rb +22 -0
- data/lib/mustermann/template.rb +48 -0
- data/lib/mustermann/to_pattern.rb +45 -0
- data/lib/mustermann/version.rb +3 -0
- data/mustermann.gemspec +31 -0
- data/spec/expander_spec.rb +105 -0
- data/spec/extension_spec.rb +296 -0
- data/spec/identity_spec.rb +83 -0
- data/spec/mapper_spec.rb +83 -0
- data/spec/mustermann_spec.rb +65 -0
- data/spec/pattern_spec.rb +49 -0
- data/spec/rails_spec.rb +522 -0
- data/spec/regexp_based_spec.rb +8 -0
- data/spec/regular_spec.rb +36 -0
- data/spec/router/rack_spec.rb +39 -0
- data/spec/router/simple_spec.rb +32 -0
- data/spec/shell_spec.rb +109 -0
- data/spec/simple_match_spec.rb +10 -0
- data/spec/simple_spec.rb +237 -0
- data/spec/sinatra_spec.rb +574 -0
- data/spec/support.rb +5 -0
- data/spec/support/coverage.rb +16 -0
- data/spec/support/env.rb +15 -0
- data/spec/support/expand_matcher.rb +27 -0
- data/spec/support/match_matcher.rb +39 -0
- data/spec/support/pattern.rb +39 -0
- data/spec/template_spec.rb +815 -0
- data/spec/to_pattern_spec.rb +20 -0
- 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
|