renee-url-generation 0.4.0.pre1

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.
@@ -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