mustermann 1.0.1 → 1.0.2.rc1

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