1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
|
require 'mustermann/ast/expander'
require 'mustermann/caster'
require 'mustermann'
module Mustermann
# Allows fine-grained control over pattern expansion.
#
# @example
# expander = Mustermann::Expander.new(additional_values: :append)
# expander << "/users/:user_id"
# expander << "/pages/:page_id"
#
# expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5"
class Expander
attr_reader :patterns, :additional_values, :caster
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}.
# @param [Symbol] additional_values behavior when encountering additional values, see {#expand}.
# @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}.
def initialize(*patterns)
options = patterns.last.is_a?(Hash) ? patterns.pop : {}
additional_values = options.delete(:additional_values) || :raise
unless additional_values == :raise or additional_values == :ignore or additional_values == :append
raise ArgumentError, "Illegal value %p for additional_values" % additional_values
end
@patterns = []
@api_expander = AST::Expander.new
@additional_values = additional_values
@options = options
@caster = Caster.new(Caster::Nil)
add(*patterns)
end
# Add patterns to expand.
#
# @example
# expander = Mustermann::Expander.new
# expander.add("/:a.jpg", "/:b.png")
# expander.expand(a: "pony") # => "/pony.jpg"
#
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of to add for expansion, Strings will be compiled to patterns.
# @return [Mustermann::Expander] the expander
def add(*patterns)
patterns.each do |pattern|
pattern = Mustermann.new(pattern.to_str, @options) if pattern.respond_to? :to_str
raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
@api_expander.add(pattern.to_ast)
@patterns << pattern
end
self
end
alias_method :<<, :add
# Register a block as simple hash transformation that runs before expanding the pattern.
# @return [Mustermann::Expander] the expander
#
# @overload cast
# Register a block as simple hash transformation that runs before expanding the pattern for all entries.
#
# @example casting everything that implements to_param to param
# expander.cast { |o| o.to_param if o.respond_to? :to_param }
#
# @yield every key/value pair
# @yieldparam key [Symbol] omitted if block takes less than 2
# @yieldparam value [Object] omitted if block takes no arguments
# @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
# @yieldreturn [nil, false] will keep key/value pair in hash
# @yieldreturn [Object] will replace value with returned object
#
# @overload cast(*type_matchers)
# Register a block as simple hash transformation that runs before expanding the pattern for certain entries.
#
# @example convert user to user_id
# expander = Mustermann::Expander.new('/users/:user_id')
# expand.cast(:user) { |user| { user_id: user.id } }
#
# expand.expand(user: User.current) # => "/users/42"
#
# @example convert user, page, image to user_id, page_id, image_id
# expander = Mustermann::Expander.new('/users/:user_id', '/pages/:page_id', '/:image_id.jpg')
# expand.cast(:user, :page, :image) { |key, value| { "#{key}_id".to_sym => value.id } }
#
# expand.expand(user: User.current) # => "/users/42"
#
# @example casting to multiple key/value pairs
# expander = Mustermann::Expander.new('/users/:user_id/:image_id.:format')
# expander.cast(:image) { |i| { user_id: i.owner.id, image_id: i.id, format: i.format } }
#
# expander.expander(image: User.current.avatar) # => "/users/42/avatar.jpg"
#
# @example casting all ActiveRecord objects to param
# expander.cast(ActiveRecord::Base, &:to_param)
#
# @param [Array<Symbol, Regexp, #===>] type_matchers
# To identify key/value pairs to match against.
# Regexps and Symbols match against key, everything else matches against value.
#
# @yield every key/value pair
# @yieldparam key [Symbol] omitted if block takes less than 2
# @yieldparam value [Object] omitted if block takes no arguments
# @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
# @yieldreturn [nil, false] will keep key/value pair in hash
# @yieldreturn [Object] will replace value with returned object
#
# @overload cast(*cast_objects)
#
# @param [Array<#cast>] cast_objects
# Before expanding, will call #cast on these objects for each key/value pair.
# Return value will be treated same as block return values described above.
def cast(*types, &block)
caster.register(*types, &block)
self
end
# @example Expanding a pattern
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
# pattern.expand(name: 'hello') # => "/hello"
# pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
#
# @example Handling additional values
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
# pattern.expand(:ignore, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png"
# pattern.expand(:append, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
# pattern.expand(:raise, name: 'hello', ext: 'png', scale: '2x') # raises Mustermann::ExpandError
#
# @example Setting additional values behavior for the expander object
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext', additional_values: :append)
# pattern.expand(name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
#
# @param [Symbol] behavior
# What to do with additional key/value pairs not present in the values hash.
# Possible options: :raise, :ignore, :append.
#
# @param [Hash{Symbol: #to_s, Array<#to_s>}] values
# Values to use for expansion.
#
# @return [String] expanded string
# @raise [NotImplementedError] raised if expand is not supported.
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
def expand(behavior = nil, values = {})
behavior, values = nil, behavior if behavior.is_a? Hash
values = map_values(values)
case behavior || additional_values
when :raise then @api_expander.expand(values)
when :ignore then with_rest(values) { |uri, rest| uri }
when :append then with_rest(values) { |uri, rest| append(uri, rest) }
else raise ArgumentError, "unknown behavior %p" % behavior
end
end
# @see Object#==
def ==(other)
return false unless other.class == self.class
other.patterns == patterns and other.additional_values == additional_values
end
# @see Object#eql?
def eql?(other)
return false unless other.class == self.class
other.patterns.eql? patterns and other.additional_values.eql? additional_values
end
# @see Object#hash
def hash
patterns.hash + additional_values.hash
end
def expandable?(values)
return false unless values
expandable, _ = split_values(map_values(values))
@api_expander.expandable? expandable
end
def with_rest(values)
expandable, non_expandable = split_values(values)
yield expand(:raise, slice(values, expandable)), slice(values, non_expandable)
end
def split_values(values)
expandable = @api_expander.expandable_keys(values.keys)
non_expandable = values.keys - expandable
[expandable, non_expandable]
end
def slice(hash, keys)
Hash[keys.map { |k| [k, hash[k]] }]
end
def append(uri, values)
return uri unless values and values.any?
entries = values.map { |pair| pair.map { |e| @api_expander.escape(e, also_escape: /[\/\?#\&\=%]/) }.join(?=) }
"#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }"
end
def map_values(values)
values = values.dup
@api_expander.keys.each { |key| values[key] ||= values.delete(key.to_s) if values.include? key.to_s }
caster.cast(values)
end
private :with_rest, :slice, :append, :caster, :map_values, :split_values
end
end
|