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/pattern'
module Mustermann
# Flask style pattern implementation.
#
# @example
# Mustermann.new('/<foo>', type: :flask) === '/bar' # => true
#
# @see Mustermann::Pattern
# @see file:README.md#flask Syntax description in the README
class Flask < AST::Pattern
register :flask
on(nil, ?>, ?:) { |c| unexpected(c) }
on(?<) do |char|
converter_name = expect(/\w+/, char: char)
args, opts = scan(?() ? read_args(?=, ?)) : [[], {}]
if scan(?:)
name = read_escaped(?>)
else
converter_name, name = 'default', converter_name
expect(?>)
end
converter = pattern.converters.fetch(converter_name) { unexpected("converter %p" % converter_name) }
converter = converter.new(*args, opts) if converter.respond_to? :new
constraint = converter.constraint if converter.respond_to? :constraint
convert = converter.convert if converter.respond_to? :convert
qualifier = converter.qualifier if converter.respond_to? :qualifier
node_type = converter.node_type if converter.respond_to? :node_type
node_type ||= :capture
node(node_type, name, convert: convert, constraint: constraint, qualifier: qualifier)
end
# A class for easy creating of converters.
# @see Mustermann::Flask#register_converter
class Converter
# Constraint on the format used for the capture.
# Should be a regexp (or a string corresponding to a regexp)
# @see Mustermann::Flask#register_converter
attr_accessor :constraint
# Callback
# Should be a Proc.
# @see Mustermann::Flask#register_converter
attr_accessor :convert
# Constraint on the format used for the capture.
# Should be a regexp (or a string corresponding to a regexp)
# @see Mustermann::Flask#register_converter
# @!visibility private
attr_accessor :node_type
# Constraint on the format used for the capture.
# Should be a regexp (or a string corresponding to a regexp)
# @see Mustermann::Flask#register_converter
# @!visibility private
attr_accessor :qualifier
# @!visibility private
def self.create(&block)
Class.new(self) do
define_method(:initialize) { |*a| o = a.last.kind_of?(Hash) ? a.pop : {}; block[self, *a, o] }
end
end
# Makes sure a given value falls inbetween a min and a max.
# Uses the passed block to convert the value from a string to whatever
# format you'd expect.
#
# @example
# require 'mustermann/flask'
#
# class MyPattern < Mustermann::Flask
# register_converter(:x) { between(5, 15, &:to_i) }
# end
#
# pattern = MyPattern.new('<x:id>')
# pattern.params('/12') # => { 'id' => 12 }
# pattern.params('/16') # => { 'id' => 15 }
#
# @see Mustermann::Flask#register_converter
def between(min, max)
self.convert = proc do |input|
value = yield(input)
value = yield(min) if min and value < yield(min)
value = yield(max) if max and value > yield(max)
value
end
end
end
# Generally available converters.
# @!visibility private
def self.converters(inherited = true)
return @converters ||= {} unless inherited
defaults = superclass.respond_to?(:converters) ? superclass.converters : {}
defaults.merge(converters(false))
end
# Allows you to register your own converters.
#
# It is reommended to use this on a subclass, so to not influence other subsystems
# using flask templates.
#
# The object passed in as converter can implement #convert and/or #constraint.
#
# It can also instead implement #new, which will then return an object responding
# to some of these methods. Arguments from the flask pattern will be passed to #new.
#
# If passed a block, it will be yielded to with a {Mustermann::Flask::Converter}
# instance and any arguments in the flask pattern.
#
# @example with simple object
# require 'mustermann/flask'
#
# MyPattern = Class.new(Mustermann::Flask)
# up_converter = Struct.new(:convert).new(:upcase.to_proc)
# MyPattern.register_converter(:upper, up_converter)
#
# MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
#
# @example with block
# require 'mustermann/flask'
#
# MyPattern = Class.new(Mustermann::Flask)
# MyPattern.register_converter(:upper) { |c| c.convert = :upcase.to_proc }
#
# MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
#
# @example with converter class
# require 'mustermann/flasl'
#
# class MyPattern < Mustermann::Flask
# class Converter
# attr_reader :convert
# def initialize(send: :to_s)
# @convert = send.to_sym.to_proc
# end
# end
#
# register_converter(:t, Converter)
# end
#
# MyPattern.new("/<t(send=upcase):name>").params('/Foo') # => { "name" => "FOO" }
# MyPattern.new("/<t(send=downcase):name>").params('/Foo') # => { "name" => "foo" }
#
# @param [#to_s] name converter name
# @param [#new, #convert, #constraint, nil] converter
def self.register_converter(name, converter = nil, &block)
converter ||= Converter.create(&block)
converters(false)[name.to_s] = converter
end
register_converter(:string) do |converter, options = {}|
minlength = options.delete(:minlength)
maxlength = options.delete(:maxlength)
length = options.delete(:length)
converter.qualifier = "{%s,%s}" % [minlength || 1, maxlength] if minlength or maxlength
converter.qualifier = "{%s}" % length if length
end
register_converter(:int) do |converter, options = {}|
min = options.delete(:min)
max = options.delete(:max)
fixed_digits = options.fetch(:fixed_digits, false)
options.delete(:fixed_digits)
converter.constraint = /\d/
converter.qualifier = "{#{fixed_digits}}" if fixed_digits
converter.between(min, max) { |string| Integer(string) }
end
register_converter(:float) do |converter, options = {}|
min = options.delete(:min)
max = options.delete(:max)
converter.constraint = /\d*\.?\d+/
converter.qualifier = ""
converter.between(min, max) { |string| Float(string) }
end
register_converter(:path) do |converter|
converter.node_type = :named_splat
end
register_converter(:any) do |converter, *strings|
strings = strings.map { |s| Regexp.escape(s) unless s == {} }.compact
converter.qualifier = ""
converter.constraint = Regexp.union(*strings)
end
register_converter(:default, converters['string'])
supported_options :converters
attr_reader :converters
def initialize(input, options = {})
converters = options[:converters] || {}
@converters = self.class.converters.dup
converters.each { |k,v| @converters[k.to_s] = v } if converters
super(input, options)
end
end
end
|