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