mustermann 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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