mustermann-contrib 1.0.0.beta2
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 +7 -0
- data/README.md +1239 -0
- data/examples/highlighting.rb +35 -0
- data/highlighting.png +0 -0
- data/irb.png +0 -0
- data/lib/mustermann/cake.rb +18 -0
- data/lib/mustermann/express.rb +37 -0
- data/lib/mustermann/file_utils.rb +217 -0
- data/lib/mustermann/file_utils/glob_pattern.rb +39 -0
- data/lib/mustermann/fileutils.rb +1 -0
- data/lib/mustermann/flask.rb +198 -0
- data/lib/mustermann/grape.rb +35 -0
- data/lib/mustermann/pyramid.rb +28 -0
- data/lib/mustermann/rails.rb +46 -0
- data/lib/mustermann/shell.rb +56 -0
- data/lib/mustermann/simple.rb +50 -0
- data/lib/mustermann/string_scanner.rb +313 -0
- data/lib/mustermann/strscan.rb +1 -0
- data/lib/mustermann/template.rb +62 -0
- data/lib/mustermann/uri_template.rb +1 -0
- data/lib/mustermann/versions.rb +46 -0
- data/lib/mustermann/visualizer.rb +38 -0
- data/lib/mustermann/visualizer/highlight.rb +137 -0
- data/lib/mustermann/visualizer/highlighter.rb +37 -0
- data/lib/mustermann/visualizer/highlighter/ad_hoc.rb +94 -0
- data/lib/mustermann/visualizer/highlighter/ast.rb +102 -0
- data/lib/mustermann/visualizer/highlighter/composite.rb +45 -0
- data/lib/mustermann/visualizer/highlighter/dummy.rb +18 -0
- data/lib/mustermann/visualizer/highlighter/regular.rb +104 -0
- data/lib/mustermann/visualizer/pattern_extension.rb +68 -0
- data/lib/mustermann/visualizer/renderer/ansi.rb +23 -0
- data/lib/mustermann/visualizer/renderer/generic.rb +46 -0
- data/lib/mustermann/visualizer/renderer/hansi_template.rb +34 -0
- data/lib/mustermann/visualizer/renderer/html.rb +50 -0
- data/lib/mustermann/visualizer/renderer/sexp.rb +37 -0
- data/lib/mustermann/visualizer/tree.rb +63 -0
- data/lib/mustermann/visualizer/tree_renderer.rb +78 -0
- data/mustermann-contrib.gemspec +19 -0
- data/spec/cake_spec.rb +90 -0
- data/spec/express_spec.rb +209 -0
- data/spec/file_utils_spec.rb +119 -0
- data/spec/flask_spec.rb +361 -0
- data/spec/flask_subclass_spec.rb +368 -0
- data/spec/grape_spec.rb +747 -0
- data/spec/pattern_extension_spec.rb +49 -0
- data/spec/pyramid_spec.rb +101 -0
- data/spec/rails_spec.rb +647 -0
- data/spec/shell_spec.rb +147 -0
- data/spec/simple_spec.rb +268 -0
- data/spec/string_scanner_spec.rb +271 -0
- data/spec/template_spec.rb +841 -0
- data/spec/visualizer_spec.rb +199 -0
- data/theme.png +0 -0
- data/tree.png +0 -0
- metadata +126 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'mustermann/visualizer'
|
3
|
+
|
4
|
+
Hansi.mode = ARGV[0].to_i if ARGV.any?
|
5
|
+
|
6
|
+
def self.example(type, *patterns)
|
7
|
+
print Hansi.render(:bold, " #{type}: ".ljust(14))
|
8
|
+
patterns.each do |pattern|
|
9
|
+
pattern = Mustermann.new(pattern, type: type)
|
10
|
+
space_after = pattern.to_s.size > 24 ? " " : " " * (25 - pattern.to_s.size)
|
11
|
+
highlight = Mustermann::Visualizer.highlight(pattern, inspect: true)
|
12
|
+
print highlight.to_ansi + space_after
|
13
|
+
end
|
14
|
+
puts
|
15
|
+
end
|
16
|
+
|
17
|
+
puts
|
18
|
+
example(:cake, '/:prefix/**')
|
19
|
+
example(:express, '/:prefix+/:id(\d+)', '/:page/:slug+')
|
20
|
+
example(:flask, '/<prefix>/<int:id>', '/user/<int(min=0):id>')
|
21
|
+
example(:identity, '/image.png')
|
22
|
+
example(:pyramid, '/{prefix:.*}/{id}', '/{page}/*slug')
|
23
|
+
example(:rails, '/:slug(.:ext)')
|
24
|
+
example(:regexp, '/(?<slug>[^/]+)', '/(?:page|user)/(\d+)')
|
25
|
+
example(:shell, '/**/*', '/\{a,b\}/{a,b}')
|
26
|
+
example(:simple, '/:page/*slug')
|
27
|
+
example(:sinatra, '/:page/*slug', '/users/{id}?')
|
28
|
+
example(:template, '/{+pre}/{page}{?q,p}', '/users/{id}?')
|
29
|
+
puts
|
30
|
+
|
31
|
+
example(:composition)
|
32
|
+
composite = Mustermann.new("/{a}", "/{b}/{c}")
|
33
|
+
puts " " + composite.to_ansi
|
34
|
+
puts " " + (Mustermann.new("/") ^ composite).to_ansi
|
35
|
+
puts
|
data/highlighting.png
ADDED
Binary file
|
data/irb.png
ADDED
Binary file
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/ast/pattern'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# CakePHP style pattern implementation.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Mustermann.new('/:foo', type: :cake) === '/bar' # => true
|
9
|
+
#
|
10
|
+
# @see Mustermann::Pattern
|
11
|
+
# @see file:README.md#cake Syntax description in the README
|
12
|
+
class Cake < AST::Pattern
|
13
|
+
register :cake
|
14
|
+
|
15
|
+
on(?:) { |c| node(:capture) { scan(/\w+/) } }
|
16
|
+
on(?*) { |c| node(:splat, convert: (-> e { e.split('/') } unless scan(?*))) }
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/ast/pattern'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Express style pattern implementation.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Mustermann.new('/:foo', type: :express) === '/bar' # => true
|
9
|
+
#
|
10
|
+
# @see Mustermann::Pattern
|
11
|
+
# @see file:README.md#flask Syntax description in the README
|
12
|
+
class Express < AST::Pattern
|
13
|
+
register :express
|
14
|
+
|
15
|
+
on(nil, ??, ?+, ?*, ?)) { |c| unexpected(c) }
|
16
|
+
on(?:) { |c| node(:capture) { scan(/\w+/) } }
|
17
|
+
on(?() { |c| node(:splat, constraint: read_brackets(?(, ?))) }
|
18
|
+
|
19
|
+
suffix ??, after: :capture do |char, element|
|
20
|
+
unexpected(char) unless element.is_a? :capture
|
21
|
+
node(:optional, element)
|
22
|
+
end
|
23
|
+
|
24
|
+
suffix ?*, after: :capture do |match, element|
|
25
|
+
node(:named_splat, element.name)
|
26
|
+
end
|
27
|
+
|
28
|
+
suffix ?+, after: :capture do |match, element|
|
29
|
+
node(:named_splat, element.name, constraint: ".+")
|
30
|
+
end
|
31
|
+
|
32
|
+
suffix ?(, after: :capture do |match, element|
|
33
|
+
element.constraint = read_brackets(?(, ?))
|
34
|
+
element
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/file_utils/glob_pattern'
|
3
|
+
require 'mustermann/mapper'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Mustermann
|
7
|
+
# Implements handy file operations using patterns.
|
8
|
+
module FileUtils
|
9
|
+
extend self
|
10
|
+
|
11
|
+
# Turn a Mustermann pattern into glob pattern.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# require 'mustermann/file_utils'
|
15
|
+
#
|
16
|
+
# Mustermann::FileUtils.glob_pattern('/:name') # => '/*'
|
17
|
+
# Mustermann::FileUtils.glob_pattern('src/:path/:file.(js|rb)') # => 'src/**/*/*.{js,rb}'
|
18
|
+
# Mustermann::FileUtils.glob_pattern('{a,b}/*', type: :shell) # => '{a,b}/*'
|
19
|
+
#
|
20
|
+
# pattern = Mustermann.new('/foo/:page', '/bar/:page') # => #<Mustermann::Composite:...>
|
21
|
+
# Mustermann::FileUtils.glob_pattern(pattern) # => "{/foo/*,/bar/*}"
|
22
|
+
#
|
23
|
+
# @param [Object] pattern the object to turn into a glob pattern.
|
24
|
+
# @return [String] the glob pattern
|
25
|
+
def glob_pattern(*pattern, **options)
|
26
|
+
pattern_with_glob_pattern(*pattern, **options).last
|
27
|
+
end
|
28
|
+
|
29
|
+
# Uses the given pattern(s) to search for files and directories.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# require 'mustermann/file_utils'
|
33
|
+
# Mustermann::FileUtils.glob(':base.:ext') # => ['example.txt']
|
34
|
+
#
|
35
|
+
# Mustermann::FileUtils.glob(':base.:ext') do |file, params|
|
36
|
+
# file # => "example.txt"
|
37
|
+
# params # => {"base"=>"example", "ext"=>"txt"}
|
38
|
+
# end
|
39
|
+
def glob(*pattern, **options, &block)
|
40
|
+
raise ArgumentError, "no pattern given" if pattern.empty?
|
41
|
+
pattern, glob_pattern = pattern_with_glob_pattern(*pattern, **options)
|
42
|
+
results = [] unless block
|
43
|
+
Dir.glob(glob_pattern) do |result|
|
44
|
+
next unless params = pattern.params(result)
|
45
|
+
block ? block[result, params] : results << result
|
46
|
+
end
|
47
|
+
results
|
48
|
+
end
|
49
|
+
|
50
|
+
# Allows to search for files an map these onto other strings.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# require 'mustermann/file_utils'
|
54
|
+
#
|
55
|
+
# Mustermann::FileUtils.glob_map(':base.:ext' => ':base.bak.:ext') # => {'example.txt' => 'example.bak.txt'}
|
56
|
+
# Mustermann::FileUtils.glob_map(':base.:ext' => :base) { |file, mapped| mapped } # => ['example']
|
57
|
+
#
|
58
|
+
# @see Mustermann::Mapper
|
59
|
+
def glob_map(map = {}, **options, &block)
|
60
|
+
map = Mapper === map ? map : Mapper.new(map, **options)
|
61
|
+
mapped = glob(*map.to_h.keys).map { |f| [f, unescape(map[f])] }
|
62
|
+
block ? mapped.map(&block) : Hash[mapped]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Copies files based on a pattern mapping.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# require 'mustermann/file_utils'
|
69
|
+
#
|
70
|
+
# # copies example.txt to example.bak.txt
|
71
|
+
# Mustermann::FileUtils.cp(':base.:ext' => ':base.bak.:ext')
|
72
|
+
#
|
73
|
+
# @see #glob_map
|
74
|
+
def cp(map = {}, recursive: false, **options)
|
75
|
+
utils_opts, opts = split_options(:preserve, :dereference_root, :remove_destination, **options)
|
76
|
+
cp_method = recursive ? :cp_r : :cp
|
77
|
+
glob_map(map, **opts) { |o,n| f.send(cp_method, o, n, **utils_opts) }
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
# Copies files based on a pattern mapping, recursively.
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# require 'mustermann/file_utils'
|
85
|
+
#
|
86
|
+
# # copies Foo.app/example.txt to Foo.back.app/example.txt
|
87
|
+
# Mustermann::FileUtils.cp_r(':base.:ext' => ':base.bak.:ext')
|
88
|
+
#
|
89
|
+
# @see #glob_map
|
90
|
+
def cp_r(map = {}, **options)
|
91
|
+
cp(map, recursive: true, **options)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Moves files based on a pattern mapping.
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# require 'mustermann/file_utils'
|
98
|
+
#
|
99
|
+
# # moves example.txt to example.bak.txt
|
100
|
+
# Mustermann::FileUtils.mv(':base.:ext' => ':base.bak.:ext')
|
101
|
+
#
|
102
|
+
# @see #glob_map
|
103
|
+
def mv(map = {}, **options)
|
104
|
+
utils_opts, opts = split_options(**options)
|
105
|
+
glob_map(map, **opts) { |o,n| f.mv(o, n, **utils_opts) }
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
# Creates links based on a pattern mapping.
|
110
|
+
#
|
111
|
+
# @example
|
112
|
+
# require 'mustermann/file_utils'
|
113
|
+
#
|
114
|
+
# # creates a link from bin/example to lib/example.rb
|
115
|
+
# Mustermann::FileUtils.ln('lib/:name.rb' => 'bin/:name')
|
116
|
+
#
|
117
|
+
# @see #glob_map
|
118
|
+
def ln(map = {}, symbolic: false, **options)
|
119
|
+
utils_opts, opts = split_options(**options)
|
120
|
+
link_method = symbolic ? :ln_s : :ln
|
121
|
+
glob_map(map, **opts) { |o,n| f.send(link_method, o, n, **utils_opts) }
|
122
|
+
end
|
123
|
+
|
124
|
+
# Creates symbolic links based on a pattern mapping.
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# require 'mustermann/file_utils'
|
128
|
+
#
|
129
|
+
# # creates a symbolic link from bin/example to lib/example.rb
|
130
|
+
# Mustermann::FileUtils.ln_s('lib/:name.rb' => 'bin/:name')
|
131
|
+
#
|
132
|
+
# @see #glob_map
|
133
|
+
def ln_s(map = {}, **options)
|
134
|
+
ln(map, symbolic: true, **options)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Creates symbolic links based on a pattern mapping.
|
138
|
+
# Overrides potentailly existing files.
|
139
|
+
#
|
140
|
+
# @example
|
141
|
+
# require 'mustermann/file_utils'
|
142
|
+
#
|
143
|
+
# # creates a symbolic link from bin/example to lib/example.rb
|
144
|
+
# Mustermann::FileUtils.ln_sf('lib/:name.rb' => 'bin/:name')
|
145
|
+
#
|
146
|
+
# @see #glob_map
|
147
|
+
def ln_sf(map = {}, **options)
|
148
|
+
ln(map, symbolic: true, force: true, **options)
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
# Splits options into those meant for Mustermann and those
|
153
|
+
# meant for ::FileUtils.
|
154
|
+
#
|
155
|
+
# @!visibility private
|
156
|
+
def split_options(*utils_option_names, **options)
|
157
|
+
utils_options, pattern_options = {}, {}
|
158
|
+
utils_option_names += %i[force noop verbose]
|
159
|
+
|
160
|
+
options.each do |key, value|
|
161
|
+
list = utils_option_names.include?(key) ? utils_options : pattern_options
|
162
|
+
list[key] = value
|
163
|
+
end
|
164
|
+
|
165
|
+
[utils_options, pattern_options]
|
166
|
+
end
|
167
|
+
|
168
|
+
# Create a Mustermann pattern from whatever the input is and turn it into
|
169
|
+
# a glob pattern.
|
170
|
+
#
|
171
|
+
# @!visibility private
|
172
|
+
def pattern_with_glob_pattern(*pattern, **options)
|
173
|
+
options[:uri_decode] ||= false
|
174
|
+
pattern = Mustermann.new(*pattern.flatten, **options)
|
175
|
+
@glob_patterns ||= {}
|
176
|
+
@glob_patterns[pattern] ||= GlobPattern.generate(pattern)
|
177
|
+
[pattern, @glob_patterns[pattern]]
|
178
|
+
end
|
179
|
+
|
180
|
+
# The FileUtils method to use.
|
181
|
+
# @!visibility private
|
182
|
+
def f
|
183
|
+
::FileUtils
|
184
|
+
end
|
185
|
+
|
186
|
+
# Unescape an URI escaped string.
|
187
|
+
# @!visibility private
|
188
|
+
def unescape(string)
|
189
|
+
@uri ||= URI::Parser.new
|
190
|
+
@uri.unescape(string)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Create a new version of Mustermann::FileUtils using a different ::FileUtils module.
|
194
|
+
# @see DryRun
|
195
|
+
# @!visibility private
|
196
|
+
def with_file_utils(&block)
|
197
|
+
Module.new do
|
198
|
+
include Mustermann::FileUtils
|
199
|
+
define_method(:f, &block)
|
200
|
+
private(:f)
|
201
|
+
extend self
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
private :pattern_with_glob_pattern, :split_options, :f, :unescape
|
206
|
+
|
207
|
+
alias_method :copy, :cp
|
208
|
+
alias_method :move, :mv
|
209
|
+
alias_method :link, :ln
|
210
|
+
alias_method :symlink, :ln_s
|
211
|
+
alias_method :[], :glob
|
212
|
+
|
213
|
+
DryRun ||= with_file_utils { ::FileUtils::DryRun }
|
214
|
+
NoWrite ||= with_file_utils { ::FileUtils::NoWrite }
|
215
|
+
Verbose ||= with_file_utils { ::FileUtils::Verbose }
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module FileUtils
|
5
|
+
# AST Translator to turn Mustermann patterns into glob patterns.
|
6
|
+
# @!visibility private
|
7
|
+
class GlobPattern < Mustermann::AST::Translator
|
8
|
+
# Character that need to be escaped in glob patterns.
|
9
|
+
# @!visibility private
|
10
|
+
ESCAPE = %w([ ] { } * ** \\)
|
11
|
+
|
12
|
+
# Turn a Mustermann pattern into glob pattern.
|
13
|
+
# @param [#to_glob, #to_ast, Object] pattern the object to turn into a glob pattern.
|
14
|
+
# @return [String] the glob pattern
|
15
|
+
# @!visibility private
|
16
|
+
def self.generate(pattern)
|
17
|
+
return pattern.to_glob if pattern.respond_to? :to_glob
|
18
|
+
return new.translate(pattern.to_ast) if pattern.respond_to? :to_ast
|
19
|
+
return "**/*" unless pattern.is_a? Mustermann::Composite
|
20
|
+
"{#{pattern.patterns.map { |p| generate(p) }.join(',')}}"
|
21
|
+
end
|
22
|
+
|
23
|
+
translate(:root, :group, :expression) { t(payload) || "" }
|
24
|
+
translate(:separator, :char) { t.escape(payload) }
|
25
|
+
translate(:capture) { constraint ? "**/*" : "*" }
|
26
|
+
translate(:optional) { "{#{t(payload)},}" }
|
27
|
+
translate(:named_splat, :splat) { "**/*" }
|
28
|
+
translate(:with_look_ahead) { t(head) + t(payload) }
|
29
|
+
translate(:union) { "{#{payload.map { |e| t(e) }.join(',')}}" }
|
30
|
+
translate(Array) { map { |e| t(e) }.join }
|
31
|
+
|
32
|
+
# Escape with a slash rather than URI escaping.
|
33
|
+
# @!visibility private
|
34
|
+
def escape(char)
|
35
|
+
ESCAPE.include?(char) ? "\\#{char}" : char
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'mustermann/file_utils'
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/ast/pattern'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Flask style pattern implementation.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Mustermann.new('/<foo>', type: :flask) === '/bar' # => true
|
9
|
+
#
|
10
|
+
# @see Mustermann::Pattern
|
11
|
+
# @see file:README.md#flask Syntax description in the README
|
12
|
+
class Flask < AST::Pattern
|
13
|
+
include Concat::Native
|
14
|
+
register :flask
|
15
|
+
|
16
|
+
on(nil, ?>, ?:) { |c| unexpected(c) }
|
17
|
+
|
18
|
+
on(?<) do |char|
|
19
|
+
converter_name = expect(/\w+/, char: char)
|
20
|
+
args, opts = scan(?() ? read_args(?=, ?)) : [[], {}]
|
21
|
+
|
22
|
+
if scan(?:)
|
23
|
+
name = read_escaped(?>)
|
24
|
+
else
|
25
|
+
converter_name, name = 'default', converter_name
|
26
|
+
expect(?>)
|
27
|
+
end
|
28
|
+
|
29
|
+
converter = pattern.converters.fetch(converter_name) { unexpected("converter %p" % converter_name) }
|
30
|
+
converter = converter.new(*args, **opts) if converter.respond_to? :new
|
31
|
+
constraint = converter.constraint if converter.respond_to? :constraint
|
32
|
+
convert = converter.convert if converter.respond_to? :convert
|
33
|
+
qualifier = converter.qualifier if converter.respond_to? :qualifier
|
34
|
+
node_type = converter.node_type if converter.respond_to? :node_type
|
35
|
+
node_type ||= :capture
|
36
|
+
|
37
|
+
node(node_type, name, convert: convert, constraint: constraint, qualifier: qualifier)
|
38
|
+
end
|
39
|
+
|
40
|
+
# A class for easy creating of converters.
|
41
|
+
# @see Mustermann::Flask#register_converter
|
42
|
+
class Converter
|
43
|
+
# Constraint on the format used for the capture.
|
44
|
+
# Should be a regexp (or a string corresponding to a regexp)
|
45
|
+
# @see Mustermann::Flask#register_converter
|
46
|
+
attr_accessor :constraint
|
47
|
+
|
48
|
+
# Callback
|
49
|
+
# Should be a Proc.
|
50
|
+
# @see Mustermann::Flask#register_converter
|
51
|
+
attr_accessor :convert
|
52
|
+
|
53
|
+
# Constraint on the format used for the capture.
|
54
|
+
# Should be a regexp (or a string corresponding to a regexp)
|
55
|
+
# @see Mustermann::Flask#register_converter
|
56
|
+
# @!visibility private
|
57
|
+
attr_accessor :node_type
|
58
|
+
|
59
|
+
# Constraint on the format used for the capture.
|
60
|
+
# Should be a regexp (or a string corresponding to a regexp)
|
61
|
+
# @see Mustermann::Flask#register_converter
|
62
|
+
# @!visibility private
|
63
|
+
attr_accessor :qualifier
|
64
|
+
|
65
|
+
# @!visibility private
|
66
|
+
def self.create(&block)
|
67
|
+
Class.new(self) do
|
68
|
+
define_method(:initialize) { |*a, **o| block[self, *a, **o] }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Makes sure a given value falls inbetween a min and a max.
|
73
|
+
# Uses the passed block to convert the value from a string to whatever
|
74
|
+
# format you'd expect.
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# require 'mustermann/flask'
|
78
|
+
#
|
79
|
+
# class MyPattern < Mustermann::Flask
|
80
|
+
# register_converter(:x) { between(5, 15, &:to_i) }
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# pattern = MyPattern.new('<x:id>')
|
84
|
+
# pattern.params('/12') # => { 'id' => 12 }
|
85
|
+
# pattern.params('/16') # => { 'id' => 15 }
|
86
|
+
#
|
87
|
+
# @see Mustermann::Flask#register_converter
|
88
|
+
def between(min, max)
|
89
|
+
self.convert = proc do |input|
|
90
|
+
value = yield(input)
|
91
|
+
value = yield(min) if min and value < yield(min)
|
92
|
+
value = yield(max) if max and value > yield(max)
|
93
|
+
value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Generally available converters.
|
99
|
+
# @!visibility private
|
100
|
+
def self.converters(inherited = true)
|
101
|
+
return @converters ||= {} unless inherited
|
102
|
+
defaults = superclass.respond_to?(:converters) ? superclass.converters : {}
|
103
|
+
defaults.merge(converters(false))
|
104
|
+
end
|
105
|
+
|
106
|
+
# Allows you to register your own converters.
|
107
|
+
#
|
108
|
+
# It is reommended to use this on a subclass, so to not influence other subsystems
|
109
|
+
# using flask templates.
|
110
|
+
#
|
111
|
+
# The object passed in as converter can implement #convert and/or #constraint.
|
112
|
+
#
|
113
|
+
# It can also instead implement #new, which will then return an object responding
|
114
|
+
# to some of these methods. Arguments from the flask pattern will be passed to #new.
|
115
|
+
#
|
116
|
+
# If passed a block, it will be yielded to with a {Mustermann::Flask::Converter}
|
117
|
+
# instance and any arguments in the flask pattern.
|
118
|
+
#
|
119
|
+
# @example with simple object
|
120
|
+
# require 'mustermann/flask'
|
121
|
+
#
|
122
|
+
# MyPattern = Class.new(Mustermann::Flask)
|
123
|
+
# up_converter = Struct.new(:convert).new(:upcase.to_proc)
|
124
|
+
# MyPattern.register_converter(:upper, up_converter)
|
125
|
+
#
|
126
|
+
# MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
|
127
|
+
#
|
128
|
+
# @example with block
|
129
|
+
# require 'mustermann/flask'
|
130
|
+
#
|
131
|
+
# MyPattern = Class.new(Mustermann::Flask)
|
132
|
+
# MyPattern.register_converter(:upper) { |c| c.convert = :upcase.to_proc }
|
133
|
+
#
|
134
|
+
# MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
|
135
|
+
#
|
136
|
+
# @example with converter class
|
137
|
+
# require 'mustermann/flasl'
|
138
|
+
#
|
139
|
+
# class MyPattern < Mustermann::Flask
|
140
|
+
# class Converter
|
141
|
+
# attr_reader :convert
|
142
|
+
# def initialize(send: :to_s)
|
143
|
+
# @convert = send.to_sym.to_proc
|
144
|
+
# end
|
145
|
+
# end
|
146
|
+
#
|
147
|
+
# register_converter(:t, Converter)
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# MyPattern.new("/<t(send=upcase):name>").params('/Foo') # => { "name" => "FOO" }
|
151
|
+
# MyPattern.new("/<t(send=downcase):name>").params('/Foo') # => { "name" => "foo" }
|
152
|
+
#
|
153
|
+
# @param [#to_s] name converter name
|
154
|
+
# @param [#new, #convert, #constraint, nil] converter
|
155
|
+
def self.register_converter(name, converter = nil, &block)
|
156
|
+
converter ||= Converter.create(&block)
|
157
|
+
converters(false)[name.to_s] = converter
|
158
|
+
end
|
159
|
+
|
160
|
+
register_converter(:string) do |converter, minlength: nil, maxlength: nil, length: nil|
|
161
|
+
converter.qualifier = "{%s,%s}" % [minlength || 1, maxlength] if minlength or maxlength
|
162
|
+
converter.qualifier = "{%s}" % length if length
|
163
|
+
end
|
164
|
+
|
165
|
+
register_converter(:int) do |converter, min: nil, max: nil, fixed_digits: false|
|
166
|
+
converter.constraint = /\d/
|
167
|
+
converter.qualifier = "{#{fixed_digits}}" if fixed_digits
|
168
|
+
converter.between(min, max) { |string| Integer(string) }
|
169
|
+
end
|
170
|
+
|
171
|
+
register_converter(:float) do |converter, min: nil, max: nil|
|
172
|
+
converter.constraint = /\d*\.?\d+/
|
173
|
+
converter.qualifier = ""
|
174
|
+
converter.between(min, max) { |string| Float(string) }
|
175
|
+
end
|
176
|
+
|
177
|
+
register_converter(:path) do |converter|
|
178
|
+
converter.node_type = :named_splat
|
179
|
+
end
|
180
|
+
|
181
|
+
register_converter(:any) do |converter, *strings|
|
182
|
+
strings = strings.map { |s| Regexp.escape(s) unless s == {} }.compact
|
183
|
+
converter.qualifier = ""
|
184
|
+
converter.constraint = Regexp.union(*strings)
|
185
|
+
end
|
186
|
+
|
187
|
+
register_converter(:default, converters['string'])
|
188
|
+
|
189
|
+
supported_options :converters
|
190
|
+
attr_reader :converters
|
191
|
+
|
192
|
+
def initialize(input, converters: {}, **options)
|
193
|
+
@converters = self.class.converters.dup
|
194
|
+
converters.each { |k,v| @converters[k.to_s] = v } if converters
|
195
|
+
super(input, **options)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|