mustermann 0.0.1 → 0.1.0

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -3
  4. data/.yardopts +1 -0
  5. data/README.md +53 -10
  6. data/Rakefile +4 -1
  7. data/bench/capturing.rb +42 -9
  8. data/bench/template_vs_addressable.rb +3 -0
  9. data/internals.md +64 -0
  10. data/lib/mustermann.rb +14 -5
  11. data/lib/mustermann/ast/compiler.rb +150 -0
  12. data/lib/mustermann/ast/expander.rb +112 -0
  13. data/lib/mustermann/ast/node.rb +155 -0
  14. data/lib/mustermann/ast/parser.rb +136 -0
  15. data/lib/mustermann/ast/pattern.rb +89 -0
  16. data/lib/mustermann/ast/transformer.rb +121 -0
  17. data/lib/mustermann/ast/translator.rb +111 -0
  18. data/lib/mustermann/ast/tree_renderer.rb +29 -0
  19. data/lib/mustermann/ast/validation.rb +40 -0
  20. data/lib/mustermann/error.rb +4 -12
  21. data/lib/mustermann/extension.rb +3 -6
  22. data/lib/mustermann/identity.rb +4 -4
  23. data/lib/mustermann/pattern.rb +34 -5
  24. data/lib/mustermann/rails.rb +7 -16
  25. data/lib/mustermann/regexp_based.rb +4 -4
  26. data/lib/mustermann/shell.rb +4 -4
  27. data/lib/mustermann/simple.rb +1 -1
  28. data/lib/mustermann/simple_match.rb +2 -2
  29. data/lib/mustermann/sinatra.rb +10 -20
  30. data/lib/mustermann/template.rb +11 -104
  31. data/lib/mustermann/version.rb +1 -1
  32. data/mustermann.gemspec +1 -1
  33. data/spec/extension_spec.rb +143 -0
  34. data/spec/mustermann_spec.rb +41 -0
  35. data/spec/pattern_spec.rb +16 -6
  36. data/spec/rails_spec.rb +77 -9
  37. data/spec/sinatra_spec.rb +6 -0
  38. data/spec/support.rb +5 -78
  39. data/spec/support/coverage.rb +18 -0
  40. data/spec/support/env.rb +6 -0
  41. data/spec/support/expand_matcher.rb +27 -0
  42. data/spec/support/match_matcher.rb +39 -0
  43. data/spec/support/pattern.rb +28 -0
  44. metadata +43 -43
  45. data/.test_queue_stats +0 -0
  46. data/lib/mustermann/ast.rb +0 -403
  47. data/spec/ast_spec.rb +0 -8
@@ -3,16 +3,16 @@ require 'forwardable'
3
3
 
4
4
  module Mustermann
5
5
  # Superclass for patterns that internally compile to a regular expression.
6
- # @see Pattern
6
+ # @see Mustermann::Pattern
7
7
  # @abstract
8
8
  class RegexpBased < Pattern
9
9
  # @return [Regexp] regular expression equivalent to the pattern.
10
10
  attr_reader :regexp
11
11
  alias_method :to_regexp, :regexp
12
12
 
13
- # @param (see Pattern#initialize)
14
- # @return (see Pattern#initialize)
15
- # @see (see Pattern#initialize)
13
+ # @param (see Mustermann::Pattern#initialize)
14
+ # @return (see Mustermann::Pattern#initialize)
15
+ # @see (see Mustermann::Pattern#initialize)
16
16
  def initialize(string, **options)
17
17
  @regexp = compile(string, **options)
18
18
  super
@@ -7,14 +7,14 @@ module Mustermann
7
7
  # @example
8
8
  # Mustermann.new('/*.*', type: :shell) === '/bar' # => false
9
9
  #
10
- # @see Pattern
10
+ # @see Mustermann::Pattern
11
11
  # @see file:README.md#shell Syntax description in the README
12
12
  class Shell < Pattern
13
13
  FLAGS ||= File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
14
14
 
15
- # @param (see Pattern#===)
16
- # @return (see Pattern#===)
17
- # @see (see Pattern#===)
15
+ # @param (see Mustermann::Pattern#===)
16
+ # @return (see Mustermann::Pattern#===)
17
+ # @see (see Mustermann::Pattern#===)
18
18
  def ===(string)
19
19
  File.fnmatch? @string, unescape(string), FLAGS
20
20
  end
@@ -6,7 +6,7 @@ module Mustermann
6
6
  # @example
7
7
  # Mustermann.new('/:foo', type: :simple) === '/bar' # => true
8
8
  #
9
- # @see Pattern
9
+ # @see Mustermann::Pattern
10
10
  # @see file:README.md#simple Syntax description in the README
11
11
  class Simple < RegexpBased
12
12
  supported_options :greedy, :space_matches_plus
@@ -12,12 +12,12 @@ module Mustermann
12
12
  @string.dup
13
13
  end
14
14
 
15
- # @return [Array<String>] empty array for immitating MatchData interface
15
+ # @return [Array<String>] empty array for imitating MatchData interface
16
16
  def names
17
17
  []
18
18
  end
19
19
 
20
- # @return [Array<String>] empty array for immitating MatchData interface
20
+ # @return [Array<String>] empty array for imitating MatchData interface
21
21
  def captures
22
22
  []
23
23
  end
@@ -1,4 +1,4 @@
1
- require 'mustermann/ast'
1
+ require 'mustermann/ast/pattern'
2
2
 
3
3
  module Mustermann
4
4
  # Sinatra 2.0 style pattern implementation.
@@ -6,27 +6,17 @@ module Mustermann
6
6
  # @example
7
7
  # Mustermann.new('/:foo') === '/bar' # => true
8
8
  #
9
- # @see Pattern
9
+ # @see Mustermann::Pattern
10
10
  # @see file:README.md#sinatra Syntax description in the README
11
- class Sinatra < AST
12
- def parse_element(buffer)
13
- case char = buffer.getch
14
- when nil, ?? then unexpected(char)
15
- when ?) then unexpected(char, exception: UnexpectedClosingGroup)
16
- when ?* then Splat.new
17
- when ?/ then Separator.new(char)
18
- when ?( then Group.parse { parse_buffer(buffer) }
19
- when ?: then Capture.parse { buffer.scan(/\w+/) }
20
- when ?\\ then Char.new expect(buffer, /./)
21
- else Char.new(char)
22
- end
23
- end
11
+ class Sinatra < AST::Pattern
12
+ on(nil, ??, ?)) { |c| unexpected(c) }
13
+ on(?*) { |c| node(:splat) }
14
+ on(?() { |c| node(:group) { read unless scan(?)) } }
15
+ on(?:) { |c| node(:capture) { scan(/\w+/) } }
16
+ on(?\\) { |c| node(:char, expect(/./)) }
24
17
 
25
- def parse_suffix(element, buffer)
26
- return element unless buffer.scan(/\?/)
27
- Optional.new(element)
18
+ suffix ?? do |char, element|
19
+ node(:optional, element)
28
20
  end
29
-
30
- private :parse_element, :parse_suffix
31
21
  end
32
22
  end
@@ -1,4 +1,4 @@
1
- require 'mustermann/ast'
1
+ require 'mustermann/ast/pattern'
2
2
 
3
3
  module Mustermann
4
4
  # URI template pattern implementation.
@@ -6,115 +6,22 @@ module Mustermann
6
6
  # @example
7
7
  # Mustermann.new('/{foo}') === '/bar' # => true
8
8
  #
9
- # @see Pattern
9
+ # @see Mustermann::Pattern
10
10
  # @see file:README.md#template Syntax description in the README
11
11
  # @see http://tools.ietf.org/html/rfc6570 RFC 6570
12
- class Template < AST
13
- Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
14
- OPERATORS ||= {
15
- nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
16
- ?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
17
- ?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
18
- ?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
19
- }
20
-
21
- # AST node for template expressions.
22
- # @!visibility private
23
- class Expression < Group
24
- # @!visibility private
25
- attr_accessor :operator
26
-
27
- # makes sure we have the proper surrounding characters for the operator
28
- # @!visibility private
29
- def transform
30
- self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
31
- new_payload = payload.inject { |list, element| Array(list) << separator << element }
32
- @payload = Array(new_payload).map!(&:transform)
33
- payload.unshift separator(operator.prefix) if operator.prefix
34
- self
35
- end
36
-
37
- # @!visibility private
38
- def compile(greedy: true, **options)
39
- super(allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
40
- parametric: operator.parametric, separator: operator.separator, **options)
41
- end
42
-
43
- # @!visibility private
44
- def separator(char = operator.separator)
45
- AST.const_get(:Separator).new(char) # uhm
46
- end
47
- end
48
-
49
- # AST node for template variables.
50
- # @!visibility private
51
- class Variable < Capture
52
- # @!visibility private
53
- attr_accessor :prefix, :explode
54
-
55
- # @!visibility private
56
- def compile(**options)
57
- return super(**options) if explode or not options[:parametric]
58
- parametric super(parametric: false, **options)
59
- end
60
-
61
- # @!visibility private
62
- def pattern(parametric: false, **options)
63
- register_param(parametric: parametric, **options)
64
- pattern = super(**options)
65
- pattern = parametric(pattern) if parametric
66
- pattern = "#{pattern}(?:#{Regexp.escape(options.fetch(:separator))}#{pattern})*" if explode
67
- pattern
68
- end
69
-
70
- # @!visibility private
71
- def parametric(string)
72
- "#{Regexp.escape(name)}(?:=#{string})?"
12
+ class Template < AST::Pattern
13
+ on ?{ do |char|
14
+ variable = proc do
15
+ match = expect(/(?<name>\w+)(?:\:(?<prefix>\d{1,4})|(?<explode>\*))?/)
16
+ node(:variable, match[:name], prefix: match[:prefix], explode: match[:explode])
73
17
  end
74
18
 
75
- # @!visibility private
76
- def qualified(string, **options)
77
- prefix ? "#{string}{1,#{prefix}}" : super(string, **options)
78
- end
79
-
80
- # @!visibility private
81
- def default(allow_reserved: false, **options)
82
- allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]'
83
- end
84
-
85
- # @!visibility private
86
- def register_param(parametric: false, split_params: nil, separator: nil, **options)
87
- return unless explode and split_params
88
- split_params[name] = { separator: separator, parametric: parametric }
89
- end
90
- end
91
-
92
- # @!visibility private
93
- def parse_element(buffer)
94
- parse_expression(buffer) || parse_literal(buffer)
95
- end
96
-
97
- # @!visibility private
98
- def parse_expression(buffer)
99
- return unless buffer.scan(/\{/)
100
19
  operator = buffer.scan(/[\+\#\.\/;\?\&\=\,\!\@\|]/)
101
- expression = Expression.new(parse_variable(buffer), operator: operator)
102
- expression.parse { parse_variable(buffer) if buffer.scan(/,/) }
103
- expression if expect(buffer, ?})
104
- end
105
-
106
- # @!visibility private
107
- def parse_variable(buffer)
108
- match = expect(buffer, /(?<name>\w+)(?:\:(?<prefix>\d{1,4})|(?<explode>\*))?/)
109
- Variable.new(match[:name], prefix: match[:prefix], explode: match[:explode])
20
+ expression = node(:expression, [variable[]], operator: operator) { variable[] if scan(?,) }
21
+ expression if expect(?})
110
22
  end
111
23
 
112
- # @!visibility private
113
- def parse_literal(buffer)
114
- return unless char = buffer.getch
115
- raise unexpected(?}) if char == ?}
116
- char == ?/ ? Separator.new('/') : Char.new(char)
117
- end
24
+ on(?}) { |c| unexpected(c) }
118
25
 
119
26
  # @!visibility private
120
27
  def compile(*args, **options)
@@ -135,6 +42,6 @@ module Mustermann
135
42
  @split_params.include? key
136
43
  end
137
44
 
138
- private :parse_element, :parse_expression, :parse_literal, :parse_variable, :map_param, :always_array?
45
+ private :compile, :map_param, :always_array?
139
46
  end
140
47
  end
@@ -1,3 +1,3 @@
1
1
  module Mustermann
2
- VERSION ||= '0.0.1'
2
+ VERSION ||= '0.1.0'
3
3
  end
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
13
13
  s.files = `git ls-files`.split("\n")
14
14
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
15
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
- s.extra_rdoc_files = %w[README.md]
16
+ s.extra_rdoc_files = %w[README.md internals.md]
17
17
  s.require_path = 'lib'
18
18
  s.required_ruby_version = '>= 2.0.0'
19
19
 
@@ -44,6 +44,7 @@ describe Mustermann::Extension do
44
44
 
45
45
  context 'route local' do
46
46
  before do
47
+ app.set(:pattern, nil)
47
48
  app.get('/:id', capture: /\d+/) { 'ok' }
48
49
  end
49
50
 
@@ -84,6 +85,148 @@ describe Mustermann::Extension do
84
85
  end
85
86
  end
86
87
 
88
+ describe :pattern do
89
+ describe :except do
90
+ before { app.get('/auth/*', pattern: { except: '/auth/login' }) { 'ok' } }
91
+ example { get('/auth/dunno').should be_ok }
92
+ example { get('/auth/login').should_not be_ok }
93
+ end
94
+
95
+ describe :capture do
96
+ context 'route local' do
97
+ before do
98
+ app.set(:pattern, nil)
99
+ app.get('/:id', pattern: { capture: /\d+/ }) { 'ok' }
100
+ end
101
+
102
+ example { get('/42').should be_ok }
103
+ example { get('/foo').should_not be_ok }
104
+ end
105
+
106
+ context 'global and route local' do
107
+ context 'global is a hash' do
108
+ before do
109
+ app.set(:pattern, capture: { id: /\d+/ })
110
+ app.get('/:id(.:ext)?', pattern: { capture: { ext: 'png' }}) { ?a }
111
+ app.get('/:id', pattern: { capture: { id: 'foo' }}) { ?b }
112
+ app.get('/:id', pattern: { capture: :alpha }) { ?c }
113
+ end
114
+
115
+ example { get('/20') .body.should be == ?a }
116
+ example { get('/20.png') .body.should be == ?a }
117
+ example { get('/foo') .body.should be == ?b }
118
+ example { get('/bar') .body.should be == ?c }
119
+ end
120
+
121
+ context 'global is not a hash' do
122
+ before do
123
+ app.set(:pattern, capture: /\d+/)
124
+ app.get('/:slug(.:ext)?', pattern: { capture: { ext: 'png' }}) { params[:slug] }
125
+ app.get('/:slug', pattern: { capture: :alpha }) { 'ok' }
126
+ end
127
+
128
+ example { get('/20.png').should be_ok }
129
+ example { get('/foo.png').should_not be_ok }
130
+ example { get('/foo').should be_ok }
131
+
132
+ example { get('/20.png') .body.should be == '20' }
133
+ example { get('/42') .body.should be == '42' }
134
+ example { get('/foo') .body.should be == 'ok' }
135
+ end
136
+ end
137
+ end
138
+
139
+ describe :greedy do
140
+ context 'default' do
141
+ before { app.get('/:name.:ext') { params[:name] }}
142
+ example { get('/foo.bar') .body.should be == 'foo' }
143
+ example { get('/foo.bar.baz') .body.should be == 'foo.bar' }
144
+ end
145
+
146
+ context 'enabled' do
147
+ before { app.get('/:name.:ext', pattern: { greedy: true }) { params[:name] }}
148
+ example { get('/foo.bar') .body.should be == 'foo' }
149
+ example { get('/foo.bar.baz') .body.should be == 'foo.bar' }
150
+ end
151
+
152
+ context 'disabled' do
153
+ before { app.get('/:name.:ext', pattern: { greedy: false }) { params[:name] }}
154
+ example { get('/foo.bar') .body.should be == 'foo' }
155
+ example { get('/foo.bar.baz') .body.should be == 'foo' }
156
+ end
157
+
158
+ context 'global' do
159
+ before do
160
+ app.set(:pattern, greedy: false)
161
+ app.get('/:name.:ext') { params[:name] }
162
+ end
163
+
164
+ example { get('/foo.bar') .body.should be == 'foo' }
165
+ example { get('/foo.bar.baz') .body.should be == 'foo' }
166
+ end
167
+ end
168
+
169
+ describe :space_matches_plus do
170
+ context 'default' do
171
+ before { app.get('/foo bar') { 'ok' }}
172
+ example { get('/foo%20bar') .should be_ok }
173
+ example { get('/foo+bar') .should be_ok }
174
+ end
175
+
176
+ context 'enabled' do
177
+ before { app.get('/foo bar', pattern: { space_matches_plus: true }) { 'ok' }}
178
+ example { get('/foo%20bar') .should be_ok }
179
+ example { get('/foo+bar') .should be_ok }
180
+ end
181
+
182
+ context 'disabled' do
183
+ before { app.get('/foo bar', pattern: { space_matches_plus: false }) { 'ok' }}
184
+ example { get('/foo%20bar') .should be_ok }
185
+ example { get('/foo+bar') .should_not be_ok }
186
+ end
187
+
188
+ context 'global' do
189
+ before do
190
+ app.set(:pattern, space_matches_plus: false)
191
+ app.get('/foo bar') { 'ok' }
192
+ end
193
+
194
+ example { get('/foo%20bar') .should be_ok }
195
+ example { get('/foo+bar') .should_not be_ok }
196
+ end
197
+ end
198
+
199
+ describe :uri_decode do
200
+ context 'default' do
201
+ before { app.get('/&') { 'ok' }}
202
+ example { get('/&') .should be_ok }
203
+ example { get('/%26') .should be_ok }
204
+ end
205
+
206
+ context 'enabled' do
207
+ before { app.get('/&', pattern: { uri_decode: true }) { 'ok' }}
208
+ example { get('/&') .should be_ok }
209
+ example { get('/%26') .should be_ok }
210
+ end
211
+
212
+ context 'disabled' do
213
+ before { app.get('/&', pattern: { uri_decode: false }) { 'ok' }}
214
+ example { get('/&') .should be_ok }
215
+ example { get('/%26') .should_not be_ok }
216
+ end
217
+
218
+ context 'global' do
219
+ before do
220
+ app.set(:pattern, uri_decode: false)
221
+ app.get('/&') { 'ok' }
222
+ end
223
+
224
+ example { get('/&') .should be_ok }
225
+ example { get('/%26') .should_not be_ok }
226
+ end
227
+ end
228
+ end
229
+
87
230
  describe :type do
88
231
  describe :identity do
89
232
  before do
@@ -0,0 +1,41 @@
1
+ require 'support'
2
+ require 'mustermann'
3
+ require 'mustermann/extension'
4
+ require 'sinatra/base'
5
+
6
+ describe Mustermann do
7
+ describe :new do
8
+ example { Mustermann.new('') .should be_a(Mustermann::Sinatra) }
9
+ example { Mustermann.new('', type: :identity) .should be_a(Mustermann::Identity) }
10
+ example { Mustermann.new('', type: :rails) .should be_a(Mustermann::Rails) }
11
+ example { Mustermann.new('', type: :shell) .should be_a(Mustermann::Shell) }
12
+ example { Mustermann.new('', type: :sinatra) .should be_a(Mustermann::Sinatra) }
13
+ example { Mustermann.new('', type: :simple) .should be_a(Mustermann::Simple) }
14
+ example { Mustermann.new('', type: :template) .should be_a(Mustermann::Template) }
15
+
16
+ example { expect { Mustermann.new('', foo: :bar) }.to raise_error(ArgumentError, "unsupported option :foo for Mustermann::Sinatra") }
17
+ example { expect { Mustermann.new('', type: :ast) }.to raise_error(ArgumentError, "unsupported type :ast") }
18
+ end
19
+
20
+ describe :[] do
21
+ example { Mustermann[:identity] .should be == Mustermann::Identity }
22
+ example { Mustermann[:rails] .should be == Mustermann::Rails }
23
+ example { Mustermann[:shell] .should be == Mustermann::Shell }
24
+ example { Mustermann[:sinatra] .should be == Mustermann::Sinatra }
25
+ example { Mustermann[:simple] .should be == Mustermann::Simple }
26
+ example { Mustermann[:template] .should be == Mustermann::Template }
27
+
28
+ example { expect { Mustermann[:ast] }.to raise_error(ArgumentError, "unsupported type :ast") }
29
+ end
30
+
31
+ describe :extend_object do
32
+ context 'special behavior for Sinatra only' do
33
+ example { Object .new.extend(Mustermann).should be_a(Mustermann) }
34
+ example { Object .new.extend(Mustermann).should_not be_a(Mustermann::Extension) }
35
+ example { Class .new.extend(Mustermann).should be_a(Mustermann) }
36
+ example { Class .new.extend(Mustermann).should_not be_a(Mustermann::Extension) }
37
+ example { Sinatra .new.extend(Mustermann).should_not be_a(Mustermann) }
38
+ example { Sinatra .new.extend(Mustermann).should be_a(Mustermann::Extension) }
39
+ end
40
+ end
41
+ end