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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +1239 -0
  3. data/examples/highlighting.rb +35 -0
  4. data/highlighting.png +0 -0
  5. data/irb.png +0 -0
  6. data/lib/mustermann/cake.rb +18 -0
  7. data/lib/mustermann/express.rb +37 -0
  8. data/lib/mustermann/file_utils.rb +217 -0
  9. data/lib/mustermann/file_utils/glob_pattern.rb +39 -0
  10. data/lib/mustermann/fileutils.rb +1 -0
  11. data/lib/mustermann/flask.rb +198 -0
  12. data/lib/mustermann/grape.rb +35 -0
  13. data/lib/mustermann/pyramid.rb +28 -0
  14. data/lib/mustermann/rails.rb +46 -0
  15. data/lib/mustermann/shell.rb +56 -0
  16. data/lib/mustermann/simple.rb +50 -0
  17. data/lib/mustermann/string_scanner.rb +313 -0
  18. data/lib/mustermann/strscan.rb +1 -0
  19. data/lib/mustermann/template.rb +62 -0
  20. data/lib/mustermann/uri_template.rb +1 -0
  21. data/lib/mustermann/versions.rb +46 -0
  22. data/lib/mustermann/visualizer.rb +38 -0
  23. data/lib/mustermann/visualizer/highlight.rb +137 -0
  24. data/lib/mustermann/visualizer/highlighter.rb +37 -0
  25. data/lib/mustermann/visualizer/highlighter/ad_hoc.rb +94 -0
  26. data/lib/mustermann/visualizer/highlighter/ast.rb +102 -0
  27. data/lib/mustermann/visualizer/highlighter/composite.rb +45 -0
  28. data/lib/mustermann/visualizer/highlighter/dummy.rb +18 -0
  29. data/lib/mustermann/visualizer/highlighter/regular.rb +104 -0
  30. data/lib/mustermann/visualizer/pattern_extension.rb +68 -0
  31. data/lib/mustermann/visualizer/renderer/ansi.rb +23 -0
  32. data/lib/mustermann/visualizer/renderer/generic.rb +46 -0
  33. data/lib/mustermann/visualizer/renderer/hansi_template.rb +34 -0
  34. data/lib/mustermann/visualizer/renderer/html.rb +50 -0
  35. data/lib/mustermann/visualizer/renderer/sexp.rb +37 -0
  36. data/lib/mustermann/visualizer/tree.rb +63 -0
  37. data/lib/mustermann/visualizer/tree_renderer.rb +78 -0
  38. data/mustermann-contrib.gemspec +19 -0
  39. data/spec/cake_spec.rb +90 -0
  40. data/spec/express_spec.rb +209 -0
  41. data/spec/file_utils_spec.rb +119 -0
  42. data/spec/flask_spec.rb +361 -0
  43. data/spec/flask_subclass_spec.rb +368 -0
  44. data/spec/grape_spec.rb +747 -0
  45. data/spec/pattern_extension_spec.rb +49 -0
  46. data/spec/pyramid_spec.rb +101 -0
  47. data/spec/rails_spec.rb +647 -0
  48. data/spec/shell_spec.rb +147 -0
  49. data/spec/simple_spec.rb +268 -0
  50. data/spec/string_scanner_spec.rb +271 -0
  51. data/spec/template_spec.rb +841 -0
  52. data/spec/visualizer_spec.rb +199 -0
  53. data/theme.png +0 -0
  54. data/tree.png +0 -0
  55. 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