renee-url-generation 0.4.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,319 @@
1
+ module Renee
2
+ class Core
3
+ # Collection of useful methods for routing within a {Renee::Core} app.
4
+ module Routing
5
+ include Chaining
6
+
7
+ # Allow continued routing if a routing block fails to match
8
+ #
9
+ # @param [Boolean] val
10
+ # indicate if continued routing should be allowed.
11
+ #
12
+ # @api public
13
+ def continue_routing
14
+ if block_given?
15
+ original_env = @env.dup
16
+ begin
17
+ yield
18
+ rescue NotMatchedError
19
+ @env = original_env
20
+ end
21
+ else
22
+ create_chain_proxy(:continue_routing)
23
+ end
24
+ end
25
+ chainable :continue_routing
26
+
27
+ # Match a path to respond to.
28
+ #
29
+ # @param [String] p
30
+ # path to match.
31
+ # @param [Proc] blk
32
+ # block to yield
33
+ #
34
+ # @example
35
+ # path('/') { ... } #=> '/'
36
+ # path('test') { ... } #=> '/test'
37
+ #
38
+ # path 'foo' do
39
+ # path('bar') { ... } #=> '/foo/bar'
40
+ # end
41
+ #
42
+ # @api public
43
+ def path(p, &blk)
44
+ if blk
45
+ p = p[1, p.size] if p[0] == ?/
46
+ extension_part = detected_extension ? "|\\.#{Regexp.quote(detected_extension)}" : ""
47
+ part(/^\/#{Regexp.quote(p)}(?=\/|$#{extension_part})/, &blk)
48
+ else
49
+ create_chain_proxy(:path, p)
50
+ end
51
+ end
52
+ chainable :path
53
+
54
+ # Like #path, but doesn't look for leading slashes.
55
+ def part(p)
56
+ if block_given?
57
+ p = /^\/?#{Regexp.quote(p)}/ if p.is_a?(String)
58
+ if match = env['PATH_INFO'][p]
59
+ with_path_part(match) { yield }
60
+ end
61
+ else
62
+ create_chain_proxy(:part, p)
63
+ end
64
+ end
65
+
66
+ # Match parts off the path as variables. The parts matcher can conform to either a regular expression, or be an Integer, or
67
+ # simply a String.
68
+ # @param[Object] type the type of object to match for. If you supply Integer, this will only match integers in addition to casting your variable for you.
69
+ # @param[Object] default the default value to use if your param cannot be successfully matched.
70
+ #
71
+ # @example
72
+ # path '/' do
73
+ # variable { |id| halt [200, {}, id] }
74
+ # end
75
+ # GET /hey #=> [200, {}, 'hey']
76
+ #
77
+ # @example
78
+ # path '/' do
79
+ # variable(:integer) { |id| halt [200, {}, "This is a numeric id: #{id}"] }
80
+ # end
81
+ # GET /123 #=> [200, {}, 'This is a numeric id: 123']
82
+ #
83
+ # @example
84
+ # path '/test' do
85
+ # variable { |foo, bar| halt [200, {}, "#{foo}-#{bar}"] }
86
+ # end
87
+ # GET /test/hey/there #=> [200, {}, 'hey-there']
88
+ #
89
+ # @api public
90
+ def variable(type = nil, &blk)
91
+ blk ? complex_variable(type, '/', 1, &blk) : create_chain_proxy(:variable, type)
92
+ end
93
+ alias_method :var, :variable
94
+ chainable :variable, :var
95
+
96
+ def optional_variable(type = nil, &blk)
97
+ blk ? complex_variable(type, '/', 0..1) { |vars| blk[vars.first] } : create_chain_proxy(:variable, type)
98
+ end
99
+ alias_method :optional, :optional_variable
100
+ chainable :optional, :optional_variable
101
+
102
+ # Same as variable except you can match multiple variables with the same type.
103
+ # @param [Range, Integer] count The number of parameters to capture.
104
+ # @param [Symbol] type The type to use for match.
105
+ def multi_variable(count, type = nil, &blk)
106
+ blk ? complex_variable(type, '/', count, &blk) : create_chain_proxy(:multi_variable, count, type)
107
+ end
108
+ alias_method :multi_var, :multi_variable
109
+ alias_method :mvar, :multi_variable
110
+ chainable :multi_variable, :multi_var, :mvar
111
+
112
+ # Same as variable except it matches indefinitely.
113
+ # @param [Symbol] type The type to use for match.
114
+ def repeating_variable(type = nil, &blk)
115
+ blk ? complex_variable(type, '/', nil, &blk) : create_chain_proxy(:repeating_variable, type)
116
+ end
117
+ alias_method :glob, :repeating_variable
118
+ chainable :repeating_variable, :glob
119
+
120
+ # Match parts off the path as variables without a leading slash.
121
+ # @see #variable
122
+ # @api public
123
+ def partial_variable(type = nil, &blk)
124
+ blk ? complex_variable(type, nil, 1, &blk) : create_chain_proxy(:partial_variable, type)
125
+ end
126
+ alias_method :part_var, :partial_variable
127
+ chainable :partial_variable, :part_var
128
+
129
+ # Returns the matched extension. If no extension is present, returns `nil`.
130
+ #
131
+ # @example
132
+ # halt [200, {}, path] if extension == 'html'
133
+ #
134
+ # @api public
135
+ def extension
136
+ detected_extension
137
+ end
138
+ alias_method :ext, :extension
139
+
140
+ # Match no extension.
141
+ #
142
+ # @example
143
+ # no_extension { |path| halt [200, {}, path] }
144
+ #
145
+ # @api public
146
+ def no_extension(&blk)
147
+ blk.call unless detected_extension
148
+ end
149
+
150
+ # Match any remaining path.
151
+ #
152
+ # @example
153
+ # remainder { |path| halt [200, {}, path] }
154
+ #
155
+ # @api public
156
+ def remainder(&blk)
157
+ blk ? with_path_part(env['PATH_INFO']) { |var| blk.call(var) } : create_chain_proxy(:remainder)
158
+ end
159
+ alias_method :catchall, :remainder
160
+ chainable :remainder, :catchall
161
+
162
+ # Respond to a GET request and yield the block.
163
+ #
164
+ # @example
165
+ # get { halt [200, {}, "hello world"] }
166
+ #
167
+ # @api public
168
+ def get(&blk)
169
+ blk ? request_method('GET', &blk) : create_chain_proxy(:get)
170
+ end
171
+ chainable :get
172
+
173
+ # Respond to a POST request and yield the block.
174
+ #
175
+ # @example
176
+ # post { halt [200, {}, "hello world"] }
177
+ #
178
+ # @api public
179
+ def post(&blk)
180
+ blk ? request_method('POST', &blk) : create_chain_proxy(:post)
181
+ end
182
+ chainable :post
183
+
184
+ # Respond to a PUT request and yield the block.
185
+ #
186
+ # @example
187
+ # put { halt [200, {}, "hello world"] }
188
+ #
189
+ # @api public
190
+ def put(&blk)
191
+ blk ? request_method('PUT', &blk) : create_chain_proxy(:put)
192
+ end
193
+ chainable :put
194
+
195
+ # Respond to a DELETE request and yield the block.
196
+ #
197
+ # @example
198
+ # delete { halt [200, {}, "hello world"] }
199
+ #
200
+ # @api public
201
+ def delete(&blk)
202
+ blk ? request_method('DELETE', &blk) : create_chain_proxy(:delete)
203
+ end
204
+ chainable :delete
205
+
206
+ # Match only when the path is either '' or '/'.
207
+ #
208
+ # @example
209
+ # complete { halt [200, {}, "hello world"] }
210
+ #
211
+ # @api public
212
+ def complete(&blk)
213
+ if blk
214
+ with_path_part(env['PATH_INFO']) { blk.call } if complete?
215
+ else
216
+ create_chain_proxy(:complete)
217
+ end
218
+ end
219
+ chainable :complete
220
+
221
+ # Test if the path has been consumed
222
+ #
223
+ # @example
224
+ # if complete?
225
+ # halt "Hey, the path is done"
226
+ # end
227
+ #
228
+ # @api public
229
+ def complete?
230
+ (detected_extension and env['PATH_INFO'] =~ /^\/?(\.#{Regexp.quote(detected_extension)}\/?)?$/) || (detected_extension.nil? and env['PATH_INFO'] =~ /^\/?$/)
231
+ end
232
+
233
+ # Match only when the path is ''.
234
+ #
235
+ # @example
236
+ # empty { halt [200, {}, "hello world"] }
237
+ #
238
+ # @api public
239
+ def empty(&blk)
240
+ if blk
241
+ if env['PATH_INFO'] == ''
242
+ with_path_part(env['PATH_INFO']) { blk.call }
243
+ end
244
+ else
245
+ create_chain_proxy(:empty)
246
+ end
247
+ end
248
+ chainable :empty
249
+
250
+ private
251
+ def complex_variable(type, prefix, count)
252
+ matcher = variable_matcher_for_type(type)
253
+ path = env['PATH_INFO'].dup
254
+ vals = []
255
+ var_index = 0
256
+ variable_matching_loop(count) do
257
+ path.start_with?(prefix) ? path.slice!(0, prefix.size) : break if prefix
258
+ if match = matcher[path]
259
+ path.slice!(0, match.first.size)
260
+ vals << match.last
261
+ end
262
+ end
263
+ return unless count.nil? || count === vals.size
264
+ with_path_part(env['PATH_INFO'][0, env['PATH_INFO'].size - path.size]) do
265
+ if count == 1
266
+ yield(vals.first)
267
+ else
268
+ yield(vals)
269
+ end
270
+ end
271
+ end
272
+
273
+ def variable_matching_loop(count)
274
+ case count
275
+ when Range then count.max.times { break unless yield }
276
+ when nil then loop { break unless yield }
277
+ else count.times { break unless yield }
278
+ end
279
+ end
280
+
281
+ def variable_matcher_for_type(type)
282
+ if self.class.variable_types.key?(type)
283
+ self.class.variable_types[type]
284
+ else
285
+ regexp = case type
286
+ when nil, String
287
+ detected_extension ?
288
+ /(([^\/](?!#{Regexp.quote(detected_extension)}$))+)(?=$|\/|\.#{Regexp.quote(detected_extension)})/ :
289
+ /([^\/]+)(?=$|\/)/
290
+ when Regexp
291
+ type
292
+ else
293
+ raise "Unexpected variable type #{type.inspect}"
294
+ end
295
+ proc do |path|
296
+ if match = /^#{regexp.to_s}/.match(path)
297
+ [match[0]]
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ def with_path_part(part)
304
+ script_part = env['PATH_INFO'][0, part.size]
305
+ env['PATH_INFO'] = env['PATH_INFO'].slice(part.size, env['PATH_INFO'].size)
306
+ env['SCRIPT_NAME'] += script_part
307
+ yield script_part
308
+ raise NotMatchedError
309
+ end
310
+
311
+ def request_method(method)
312
+ if env['REQUEST_METHOD'] == method && complete?
313
+ yield
314
+ raise NotMatchedError
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,18 @@
1
+ module Renee
2
+ class Core
3
+ # Module used for transforming arbitrary values using the registerd variable types.
4
+ # @see #register_variable_name.
5
+ #
6
+ module Transform
7
+ # Transforms a value according to the rules specified by #register_variable_name.
8
+ # @param [Symbol] name The name of the variable type.
9
+ # @param [String] value The value to transform.
10
+ # @return The transformed value or nil.
11
+ def transform(type, value)
12
+ if self.class.variable_types.key?(type) and m = self.class.variable_types[type][value]
13
+ m.first == value ? m.last : nil
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/renee/core.rb ADDED
@@ -0,0 +1,98 @@
1
+ require 'rack'
2
+
3
+ require 'renee/version'
4
+ require 'renee/core/matcher'
5
+ require 'renee/core/chaining'
6
+ require 'renee/core/response'
7
+ require 'renee/core/exceptions'
8
+ require 'renee/core/rack_interaction'
9
+ require 'renee/core/request_context'
10
+ require 'renee/core/transform'
11
+ require 'renee/core/routing'
12
+ require 'renee/core/responding'
13
+ require 'renee/core/env_accessors'
14
+ require 'renee/core/plugins'
15
+
16
+ # Top-level Renee constant
17
+ module Renee
18
+ # @example
19
+ # Renee.core { path('/hello') { halt :ok } }
20
+ def self.core(&blk)
21
+ cls = Class.new(Renee::Core)
22
+ cls.app(&blk) if blk
23
+ cls
24
+ end
25
+
26
+ # The top-level class for creating core application.
27
+ # For convience you can also used a method named #Renee
28
+ # for decalaring new instances.
29
+ class Core
30
+ # Current version of Renee::Core
31
+ VERSION = Renee::VERSION
32
+
33
+ # Error raised if routing fails. Use #continue_routing to continue routing.
34
+ NotMatchedError = Class.new(RuntimeError)
35
+
36
+ # Class methods that are included in new instances of {Core}
37
+ module ClassMethods
38
+ include Plugins
39
+
40
+ # The application block used to create your application.
41
+ attr_reader :application_block
42
+
43
+ # Provides a rack interface compliant call method. This method creates a new instance of your class and calls
44
+ # #call on it.
45
+ def call(env)
46
+ new.call(env)
47
+ end
48
+
49
+ # Allows you to set the #application_block on your class.
50
+ # @yield The application block
51
+ def app(&app)
52
+ @application_block = app
53
+ setup do
54
+ register_variable_type :integer, IntegerMatcher
55
+ register_variable_type :int, :integer
56
+ end
57
+ end
58
+
59
+ # Runs class methods on your application.
60
+ def setup(&blk)
61
+ instance_eval(&blk)
62
+ self
63
+ end
64
+
65
+ # The currently available variable types you've defined.
66
+ def variable_types
67
+ @variable_types ||= {}
68
+ end
69
+
70
+ # Registers a new variable type for use within {Renee::Core::Routing#variable} and others.
71
+ # @param [Symbol] name The name of the variable.
72
+ # @param [Regexp] matcher A regexp describing what part of an arbitrary string to capture.
73
+ # @return [Renee::Core::Matcher] A matcher
74
+ def register_variable_type(name, matcher)
75
+ matcher = case matcher
76
+ when Matcher then matcher
77
+ when Array then Matcher.new(matcher.map{|m| variable_types[m]})
78
+ when Symbol then variable_types[matcher]
79
+ else Matcher.new(matcher)
80
+ end
81
+ matcher.name = name
82
+ variable_types[name] = matcher
83
+ end
84
+ end
85
+
86
+ include Chaining
87
+ include RequestContext
88
+ include Routing
89
+ include Responding
90
+ include RackInteraction
91
+ include Transform
92
+ include EnvAccessors
93
+
94
+ class << self
95
+ include ClassMethods
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Route chaining" do
4
+
5
+ it "should chaining" do
6
+ type = { 'Content-Type' => 'text/plain' }
7
+ mock_app do
8
+ path('/').get { halt [200,type,['foo']] }
9
+ continue_routing.path('bar').put { halt [200,type,['bar']] }
10
+ continue_routing.path('bar').var.put { |id| halt [200,type,[id]] }
11
+ continue_routing.path('bar').var.get.halt { |id| "wow, nice to meet you " }
12
+ end
13
+ get '/'
14
+ assert_equal 200, response.status
15
+ assert_equal 'foo', response.body
16
+ put '/bar'
17
+ assert_equal 200, response.status
18
+ assert_equal 'bar', response.body
19
+ put '/bar/asd'
20
+ assert_equal 200, response.status
21
+ assert_equal 'asd', response.body
22
+ end
23
+
24
+ it "should chain and halt with a non-routing method" do
25
+ type = { 'Content-Type' => 'text/plain' }
26
+ mock_app do
27
+ path('/').get.halt "hi"
28
+ end
29
+ get '/'
30
+ assert_equal 200, response.status
31
+ assert_equal 'hi', response.body
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require File.expand_path('../test_helper', __FILE__)
4
+
5
+ describe Renee::Core::EnvAccessors do
6
+ it "should allow accessing the env" do
7
+ @app = Renee.core {
8
+ self.test = 'hello'
9
+ path('test').get do
10
+ halt "test is #{test}"
11
+ end
12
+ }.setup {
13
+ env_accessor :test
14
+ }
15
+ get '/test'
16
+ assert_equal 200, response.status
17
+ assert_equal 'test is hello', response.body
18
+ end
19
+
20
+ it "should raise when you try to access weird env keys" do
21
+ assert_raises(Renee::Core::EnvAccessors::InvalidEnvNameError) {
22
+ @app = Renee.core {
23
+ self.test_test = 'hello'
24
+ }.setup {
25
+ env_accessor "test.test"
26
+ }
27
+ }
28
+ end
29
+
30
+ it "should allow weird env keys if you map them" do
31
+ @app = Renee.core {
32
+ self.test_test = 'hello'
33
+ path('test').get do
34
+ halt "test is #{test_test}"
35
+ end
36
+ }.setup {
37
+ env_accessor "test.test" => :test_test
38
+ }
39
+ get '/test'
40
+ assert_equal 200, response.status
41
+ assert_equal 'test is hello', response.body
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Route::Settings#include" do
4
+ it "should allow the inclusion of arbitrary modules" do
5
+ type = { 'Content-Type' => 'text/plain' }
6
+ @app = Renee.core {
7
+ halt :ok if respond_to?(:hi)
8
+ }.setup {
9
+ include Module.new { def hi; end }
10
+ }
11
+ get '/'
12
+ assert_equal 200, response.status
13
+ end
14
+ end
@@ -0,0 +1,70 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class AddHelloThereMiddleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if env['hello']
10
+ env['hello'] << "there"
11
+ else
12
+ env['hello'] = 'hello'
13
+ end
14
+ @app.call(env)
15
+ end
16
+ end
17
+
18
+ class AddWhatsThatMiddleware
19
+ def initialize(app)
20
+ @app = app
21
+ end
22
+
23
+ def call(env)
24
+ if env['hello']
25
+ env['hello'] << "that"
26
+ else
27
+ env['hello'] = 'whats'
28
+ end
29
+ @app.call(env)
30
+ end
31
+ end
32
+
33
+ describe "Route::Core::RequestContext#use" do
34
+ it "should allow the inclusion of arbitrary middlewares" do
35
+ type = { 'Content-Type' => 'text/plain' }
36
+ @app = Renee.core {
37
+ halt env['hello']
38
+ }.setup {
39
+ use AddHelloThereMiddleware
40
+ }
41
+ get '/'
42
+ assert_equal 200, response.status
43
+ assert_equal 'hello', response.body
44
+ end
45
+
46
+ it "should call middlewares in sequence (1)" do
47
+ type = { 'Content-Type' => 'text/plain' }
48
+ @app = Renee.core {
49
+ halt env['hello']
50
+ }.setup {
51
+ use AddHelloThereMiddleware
52
+ use AddWhatsThatMiddleware
53
+ }
54
+ get '/'
55
+ assert_equal 200, response.status
56
+ assert_equal 'hellothat', response.body
57
+ end
58
+
59
+ it "should call middlewares in sequence (2)" do
60
+ @app = Renee.core {
61
+ halt env['hello']
62
+ }.setup {
63
+ use AddWhatsThatMiddleware
64
+ use AddHelloThereMiddleware
65
+ }
66
+ get '/'
67
+ assert_equal 200, response.status
68
+ assert_equal 'whatsthere', response.body
69
+ end
70
+ end