cuba 2.0.1 → 2.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -193,6 +193,27 @@ The fourth case, again, reverts to the basic matcher: it generates the string
193
193
  The fifth case is different: it checks if the the parameter supplied is present
194
194
  in the request (via POST or QUERY_STRING) and it pushes the value as a capture.
195
195
 
196
+ Composition
197
+ -----------
198
+
199
+ You can mount a Cuba app, along with middlewares, inside another Cuba app:
200
+
201
+ API = Cuba.build
202
+
203
+ API.use SomeMiddleware
204
+
205
+ API.define do
206
+ on param("url") do |url|
207
+ ...
208
+ end
209
+ end
210
+
211
+ Cuba.define do
212
+ on "api" do
213
+ run API
214
+ end
215
+ end
216
+
196
217
  Testing
197
218
  -------
198
219
 
@@ -1,7 +1,28 @@
1
+ require "rack"
2
+ require "tilt"
1
3
  require "cuba/version"
2
- require "cuba/ron"
3
4
 
4
- module Cuba
5
+ class Rack::Response
6
+ # 301 Moved Permanently
7
+ # 302 Found
8
+ # 303 See Other
9
+ # 307 Temporary Redirect
10
+ def redirect(target, status = 302)
11
+ self.status = status
12
+ self["Location"] = target
13
+ end
14
+ end
15
+
16
+ class Cuba
17
+ class RedefinitionError < StandardError
18
+ end
19
+
20
+ @@methods = []
21
+
22
+ def self.method_added(meth)
23
+ @@methods << meth
24
+ end
25
+
5
26
  def self.reset!
6
27
  @app = nil
7
28
  @prototype = nil
@@ -16,7 +37,11 @@ module Cuba
16
37
  end
17
38
 
18
39
  def self.define(&block)
19
- app.run Cuba::Ron.new(&block)
40
+ app.run Cuba.new(&block)
41
+ end
42
+
43
+ def self.build
44
+ Class.new(self)
20
45
  end
21
46
 
22
47
  def self.prototype
@@ -26,4 +51,255 @@ module Cuba
26
51
  def self.call(env)
27
52
  prototype.call(env)
28
53
  end
29
- end
54
+
55
+ attr :env
56
+ attr :req
57
+ attr :res
58
+ attr :captures
59
+
60
+ def initialize(&blk)
61
+ @blk = blk
62
+ @captures = []
63
+ end
64
+
65
+ def call(env)
66
+ dup._call(env)
67
+ end
68
+
69
+ def _call(env)
70
+ @env = env
71
+ @req = Rack::Request.new(env)
72
+ @res = Rack::Response.new
73
+ @matched = false
74
+
75
+ catch(:ron_run_next_app) do
76
+ instance_eval(&@blk)
77
+
78
+ @res.status = 404 unless @matched || !@res.empty?
79
+
80
+ return @res.finish
81
+ end.call(env)
82
+ end
83
+
84
+ # @private Used internally by #render to cache the
85
+ # Tilt templates.
86
+ def _cache
87
+ Thread.current[:_cache] ||= Tilt::Cache.new
88
+ end
89
+ private :_cache
90
+
91
+ # Render any type of template file supported by Tilt.
92
+ #
93
+ # @example
94
+ #
95
+ # # Renders home, and is assumed to be HAML.
96
+ # render("home.haml")
97
+ #
98
+ # # Renders with some local variables
99
+ # render("home.haml", site_name: "My Site")
100
+ #
101
+ # # Renders with HAML options
102
+ # render("home.haml", {}, ugly: true, format: :html5)
103
+ #
104
+ # # Renders in layout
105
+ # render("layout.haml") { render("home.haml") }
106
+ #
107
+ def render(template, locals = {}, options = {}, &block)
108
+ _cache.fetch(template, locals) {
109
+ Tilt.new(template, 1, options)
110
+ }.render(self, locals, &block)
111
+ end
112
+
113
+ # The heart of the path / verb / any condition matching.
114
+ #
115
+ # @example
116
+ #
117
+ # on get do
118
+ # res.write "GET"
119
+ # end
120
+ #
121
+ # on get, "signup" do
122
+ # res.write "Signup
123
+ # end
124
+ #
125
+ # on "user/:id" do |uid|
126
+ # res.write "User: #{uid}"
127
+ # end
128
+ #
129
+ # on "styles", extension("css") do |file|
130
+ # res.write render("styles/#{file}.sass")
131
+ # end
132
+ #
133
+ def on(*args, &block)
134
+ # No use running any other matchers if we've already found a
135
+ # proper matcher.
136
+ return if @matched
137
+
138
+ try do
139
+ # For every block, we make sure to reset captures so that
140
+ # nesting matchers won't mess with each other's captures.
141
+ @captures = []
142
+
143
+ # We stop evaluation of this entire matcher unless
144
+ # each and every `arg` defined for this matcher evaluates
145
+ # to a non-false value.
146
+ #
147
+ # Short circuit examples:
148
+ # on true, false do
149
+ #
150
+ # # PATH_INFO=/user
151
+ # on true, "signup"
152
+ return unless args.all? { |arg| match(arg) }
153
+
154
+ begin
155
+ # The captures we yield here were generated and assembled
156
+ # by evaluating each of the `arg`s above. Most of these
157
+ # are carried out by #consume.
158
+ yield *captures
159
+
160
+ ensure
161
+ # Regardless of what happens in the `yield`, we should ensure that
162
+ # we successfully set `@matched` to true.
163
+
164
+ # At this point, we've successfully matched with some corresponding
165
+ # matcher, so we can skip all other matchers defined.
166
+ @matched = true
167
+ end
168
+ end
169
+ end
170
+
171
+ # @private Used internally by #on to ensure that SCRIPT_NAME and
172
+ # PATH_INFO are reset to their proper values.
173
+ def try
174
+ script, path = env["SCRIPT_NAME"], env["PATH_INFO"]
175
+
176
+ yield
177
+
178
+ ensure
179
+ env["SCRIPT_NAME"], env["PATH_INFO"] = script, path unless @matched
180
+ end
181
+ private :try
182
+
183
+ def consume(pattern)
184
+ return unless match = env["PATH_INFO"].match(/\A\/(#{pattern})((?:\/|\z))/)
185
+
186
+ path, *vars = match.captures
187
+
188
+ env["SCRIPT_NAME"] += "/#{path}"
189
+ env["PATH_INFO"] = "#{vars.pop}#{match.post_match}"
190
+
191
+ captures.push(*vars)
192
+ end
193
+ private :consume
194
+
195
+ def match(matcher, segment = "([^\\/]+)")
196
+ case matcher
197
+ when String then consume(matcher.gsub(/:\w+/, segment))
198
+ when Regexp then consume(matcher)
199
+ when Symbol then consume(segment)
200
+ when Proc then matcher.call
201
+ else
202
+ matcher
203
+ end
204
+ end
205
+
206
+ # A matcher for files with a certain extension.
207
+ #
208
+ # @example
209
+ # # PATH_INFO=/style/app.css
210
+ # on "style", extension("css") do |file|
211
+ # res.write file # writes app
212
+ # end
213
+ def extension(ext = "\\w+")
214
+ lambda { consume("([^\\/]+?)\.#{ext}\\z") }
215
+ end
216
+
217
+ # Used to ensure that certain request parameters are present. Acts like a
218
+ # precondition / assertion for your route.
219
+ #
220
+ # @example
221
+ # # POST with data like user[fname]=John&user[lname]=Doe
222
+ # on "signup", param("user") do |atts|
223
+ # User.create(atts)
224
+ # end
225
+ def param(key)
226
+ lambda { captures << req[key] unless req[key].to_s.empty? }
227
+ end
228
+
229
+ def header(key)
230
+ lambda { env[key.upcase.tr("-","_")] }
231
+ end
232
+
233
+ # Useful for matching against the request host (i.e. HTTP_HOST).
234
+ #
235
+ # @example
236
+ # on host("account1.example.com"), "api" do
237
+ # res.write "You have reached the API of account1."
238
+ # end
239
+ def host(hostname)
240
+ hostname === req.host
241
+ end
242
+
243
+ # If you want to match against the HTTP_ACCEPT value.
244
+ #
245
+ # @example
246
+ # # HTTP_ACCEPT=application/xml
247
+ # on accept("application/xml") do
248
+ # # automatically set to application/xml.
249
+ # res.write res["Content-Type"]
250
+ # end
251
+ def accept(mimetype)
252
+ lambda do
253
+ String(env["HTTP_ACCEPT"]).split(",").any? { |s| s.strip == mimetype } and
254
+ res["Content-Type"] = mimetype
255
+ end
256
+ end
257
+
258
+ # Syntactic sugar for providing catch-all matches.
259
+ #
260
+ # @example
261
+ # on default do
262
+ # res.write "404"
263
+ # end
264
+ def default
265
+ true
266
+ end
267
+
268
+ # Syntatic sugar for providing HTTP Verb matching.
269
+ #
270
+ # @example
271
+ # on get, "signup" do
272
+ # end
273
+ #
274
+ # on post, "signup" do
275
+ # end
276
+ def get ; req.get? end
277
+ def post ; req.post? end
278
+ def put ; req.put? end
279
+ def delete ; req.delete? end
280
+
281
+ # If you want to halt the processing of an existing handler
282
+ # and continue it via a different handler.
283
+ #
284
+ # @example
285
+ # def redirect(*args)
286
+ # run Cuba.new { on(default) { res.redirect(*args) }}
287
+ # end
288
+ #
289
+ # on "account" do
290
+ # redirect "/login" unless session["uid"]
291
+ #
292
+ # res.write "Super secure account info."
293
+ # end
294
+ def run(app)
295
+ throw :ron_run_next_app, app
296
+ end
297
+
298
+ # In order to prevent people from overriding the standard Cuba
299
+ # methods like `get`, `put`, etc, we add this as a safety measure.
300
+ def self.method_added(meth)
301
+ if @@methods.include?(meth)
302
+ raise RedefinitionError, meth
303
+ end
304
+ end
305
+ end
@@ -1,3 +1,3 @@
1
- module Cuba
2
- VERSION = "2.0.1"
1
+ class Cuba
2
+ VERSION = "2.1.0.rc1"
3
3
  end
@@ -1,7 +1,7 @@
1
1
  require File.expand_path("helper", File.dirname(__FILE__))
2
2
 
3
3
  test "composing on top of a PATH" do
4
- Services = Cuba::Ron.new {
4
+ Services = Cuba.new {
5
5
  on "services/:id" do |id|
6
6
  res.write "View #{id}"
7
7
  end
@@ -0,0 +1,41 @@
1
+ require File.expand_path("helper", File.dirname(__FILE__))
2
+
3
+ class Shrimp
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ status, headers, resp = @app.call(env)
10
+
11
+ [status, headers, resp.body.reverse]
12
+ end
13
+ end
14
+
15
+ test do
16
+ API = Cuba.build
17
+ API.use Shrimp
18
+ API.define do
19
+ on "v1/test" do
20
+ res.write "OK"
21
+ res.write "1"
22
+ res.write "2"
23
+ end
24
+ end
25
+
26
+ Cuba.define do
27
+ on "api" do
28
+ run API
29
+ end
30
+ end
31
+
32
+ _, _, body = Cuba.call({ "PATH_INFO" => "/api/v1/test", "SCRIPT_NAME" => "/" })
33
+
34
+ arr = []
35
+
36
+ body.each do |line|
37
+ arr << line
38
+ end
39
+
40
+ assert_equal ["2", "1", "OK"], arr
41
+ end
@@ -0,0 +1,17 @@
1
+ require File.expand_path("helper", File.dirname(__FILE__))
2
+
3
+ test "adding your new custom helpers is ok" do
4
+ class Cuba
5
+ def foobar
6
+ end
7
+ end
8
+ end
9
+
10
+ test "redefining standard Cuba methods fails" do
11
+ assert_raise Cuba::RedefinitionError do
12
+ class Cuba
13
+ def get
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,7 +3,7 @@ require File.expand_path("helper", File.dirname(__FILE__))
3
3
  test "redirect canonical example" do
4
4
  Cuba.define do
5
5
  def redirect(*args)
6
- run Cuba::Ron.new { on(true) { res.redirect(*args) }}
6
+ run Cuba.new { on(true) { res.redirect(*args) }}
7
7
  end
8
8
 
9
9
  on "account" do
metadata CHANGED
@@ -1,20 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cuba
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
5
- prerelease:
4
+ version: 2.1.0.rc1
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Michel Martens
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-07-19 00:00:00.000000000 -03:00
12
+ date: 2011-08-23 00:00:00.000000000 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
17
- requirement: &2157267480 !ruby/object:Gem::Requirement
17
+ requirement: &2153945360 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: '0'
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2157267480
25
+ version_requirements: *2153945360
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: tilt
28
- requirement: &2157267060 !ruby/object:Gem::Requirement
28
+ requirement: &2153944940 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
- version_requirements: *2157267060
36
+ version_requirements: *2153944940
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: cutest
39
- requirement: &2157266640 !ruby/object:Gem::Requirement
39
+ requirement: &2153944520 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ! '>='
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: '0'
45
45
  type: :development
46
46
  prerelease: false
47
- version_requirements: *2157266640
47
+ version_requirements: *2153944520
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: capybara
50
- requirement: &2157266220 !ruby/object:Gem::Requirement
50
+ requirement: &2153944100 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ! '>='
@@ -55,7 +55,7 @@ dependencies:
55
55
  version: '0'
56
56
  type: :development
57
57
  prerelease: false
58
- version_requirements: *2157266220
58
+ version_requirements: *2153944100
59
59
  description: Cuba is a microframework for web applications.
60
60
  email:
61
61
  - michel@soveran.com
@@ -66,7 +66,6 @@ files:
66
66
  - LICENSE
67
67
  - README.markdown
68
68
  - Rakefile
69
- - lib/cuba/ron.rb
70
69
  - lib/cuba/test.rb
71
70
  - lib/cuba/version.rb
72
71
  - lib/cuba.rb
@@ -80,10 +79,12 @@ files:
80
79
  - test/integration.rb
81
80
  - test/layout.rb
82
81
  - test/match.rb
82
+ - test/middleware.rb
83
83
  - test/number.rb
84
84
  - test/on.rb
85
85
  - test/param.rb
86
86
  - test/path.rb
87
+ - test/redefinition.rb
87
88
  - test/root.rb
88
89
  - test/run.rb
89
90
  - test/segment.rb
@@ -103,9 +104,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
103
104
  required_rubygems_version: !ruby/object:Gem::Requirement
104
105
  none: false
105
106
  requirements:
106
- - - ! '>='
107
+ - - ! '>'
107
108
  - !ruby/object:Gem::Version
108
- version: '0'
109
+ version: 1.3.1
109
110
  requirements: []
110
111
  rubyforge_project:
111
112
  rubygems_version: 1.6.2
@@ -1,260 +0,0 @@
1
- require "rack"
2
- require "tilt"
3
-
4
- class Rack::Response
5
- # 301 Moved Permanently
6
- # 302 Found
7
- # 303 See Other
8
- # 307 Temporary Redirect
9
- def redirect(target, status = 302)
10
- self.status = status
11
- self["Location"] = target
12
- end
13
- end
14
-
15
- module Cuba
16
- class Ron
17
- attr :env
18
- attr :req
19
- attr :res
20
- attr :captures
21
-
22
- def initialize(&blk)
23
- @blk = blk
24
- @captures = []
25
- end
26
-
27
- def call(env)
28
- dup._call(env)
29
- end
30
-
31
- def _call(env)
32
- @env = env
33
- @req = Rack::Request.new(env)
34
- @res = Rack::Response.new
35
- @matched = false
36
-
37
- catch(:ron_run_next_app) do
38
- instance_eval(&@blk)
39
-
40
- @res.status = 404 unless @matched || !@res.empty?
41
-
42
- return @res.finish
43
- end.call(env)
44
- end
45
-
46
- # @private Used internally by #render to cache the
47
- # Tilt templates.
48
- def _cache
49
- Thread.current[:_cache] ||= Tilt::Cache.new
50
- end
51
- private :_cache
52
-
53
- # Render any type of template file supported by Tilt.
54
- #
55
- # @example
56
- #
57
- # # Renders home, and is assumed to be HAML.
58
- # render("home.haml")
59
- #
60
- # # Renders with some local variables
61
- # render("home.haml", site_name: "My Site")
62
- #
63
- # # Renders with HAML options
64
- # render("home.haml", {}, ugly: true, format: :html5)
65
- #
66
- # # Renders in layout
67
- # render("layout.haml") { render("home.haml") }
68
- #
69
- def render(template, locals = {}, options = {}, &block)
70
- _cache.fetch(template, locals) {
71
- Tilt.new(template, 1, options)
72
- }.render(self, locals, &block)
73
- end
74
-
75
- # The heart of the path / verb / any condition matching.
76
- #
77
- # @example
78
- #
79
- # on get do
80
- # res.write "GET"
81
- # end
82
- #
83
- # on get, "signup" do
84
- # res.write "Signup
85
- # end
86
- #
87
- # on "user/:id" do |uid|
88
- # res.write "User: #{uid}"
89
- # end
90
- #
91
- # on "styles", extension("css") do |file|
92
- # res.write render("styles/#{file}.sass")
93
- # end
94
- #
95
- def on(*args, &block)
96
- # No use running any other matchers if we've already found a
97
- # proper matcher.
98
- return if @matched
99
-
100
- try do
101
- # For every block, we make sure to reset captures so that
102
- # nesting matchers won't mess with each other's captures.
103
- @captures = []
104
-
105
- # We stop evaluation of this entire matcher unless
106
- # each and every `arg` defined for this matcher evaluates
107
- # to a non-false value.
108
- #
109
- # Short circuit examples:
110
- # on true, false do
111
- #
112
- # # PATH_INFO=/user
113
- # on true, "signup"
114
- return unless args.all? { |arg| match(arg) }
115
-
116
- begin
117
- # The captures we yield here were generated and assembled
118
- # by evaluating each of the `arg`s above. Most of these
119
- # are carried out by #consume.
120
- yield *captures
121
-
122
- ensure
123
- # Regardless of what happens in the `yield`, we should ensure that
124
- # we successfully set `@matched` to true.
125
-
126
- # At this point, we've successfully matched with some corresponding
127
- # matcher, so we can skip all other matchers defined.
128
- @matched = true
129
- end
130
- end
131
- end
132
-
133
- # @private Used internally by #on to ensure that SCRIPT_NAME and
134
- # PATH_INFO are reset to their proper values.
135
- def try
136
- script, path = env["SCRIPT_NAME"], env["PATH_INFO"]
137
-
138
- yield
139
-
140
- ensure
141
- env["SCRIPT_NAME"], env["PATH_INFO"] = script, path unless @matched
142
- end
143
- private :try
144
-
145
- def consume(pattern)
146
- return unless match = env["PATH_INFO"].match(/\A\/(#{pattern})((?:\/|\z))/)
147
-
148
- path, *vars = match.captures
149
-
150
- env["SCRIPT_NAME"] += "/#{path}"
151
- env["PATH_INFO"] = "#{vars.pop}#{match.post_match}"
152
-
153
- captures.push(*vars)
154
- end
155
- private :consume
156
-
157
- def match(matcher, segment = "([^\\/]+)")
158
- case matcher
159
- when String then consume(matcher.gsub(/:\w+/, segment))
160
- when Regexp then consume(matcher)
161
- when Symbol then consume(segment)
162
- when Proc then matcher.call
163
- else
164
- matcher
165
- end
166
- end
167
-
168
- # A matcher for files with a certain extension.
169
- #
170
- # @example
171
- # # PATH_INFO=/style/app.css
172
- # on "style", extension("css") do |file|
173
- # res.write file # writes app
174
- # end
175
- def extension(ext = "\\w+")
176
- lambda { consume("([^\\/]+?)\.#{ext}\\z") }
177
- end
178
-
179
- # Used to ensure that certain request parameters are present. Acts like a
180
- # precondition / assertion for your route.
181
- #
182
- # @example
183
- # # POST with data like user[fname]=John&user[lname]=Doe
184
- # on "signup", param("user") do |atts|
185
- # User.create(atts)
186
- # end
187
- def param(key)
188
- lambda { captures << req[key] unless req[key].to_s.empty? }
189
- end
190
-
191
- def header(key)
192
- lambda { env[key.upcase.tr("-","_")] }
193
- end
194
-
195
- # Useful for matching against the request host (i.e. HTTP_HOST).
196
- #
197
- # @example
198
- # on host("account1.example.com"), "api" do
199
- # res.write "You have reached the API of account1."
200
- # end
201
- def host(hostname)
202
- hostname === req.host
203
- end
204
-
205
- # If you want to match against the HTTP_ACCEPT value.
206
- #
207
- # @example
208
- # # HTTP_ACCEPT=application/xml
209
- # on accept("application/xml") do
210
- # # automatically set to application/xml.
211
- # res.write res["Content-Type"]
212
- # end
213
- def accept(mimetype)
214
- lambda do
215
- String(env["HTTP_ACCEPT"]).split(",").any? { |s| s.strip == mimetype } and
216
- res["Content-Type"] = mimetype
217
- end
218
- end
219
-
220
- # Syntactic sugar for providing catch-all matches.
221
- #
222
- # @example
223
- # on default do
224
- # res.write "404"
225
- # end
226
- def default
227
- true
228
- end
229
-
230
- # Syntatic sugar for providing HTTP Verb matching.
231
- #
232
- # @example
233
- # on get, "signup" do
234
- # end
235
- #
236
- # on post, "signup" do
237
- # end
238
- def get ; req.get? end
239
- def post ; req.post? end
240
- def put ; req.put? end
241
- def delete ; req.delete? end
242
-
243
- # If you want to halt the processing of an existing handler
244
- # and continue it via a different handler.
245
- #
246
- # @example
247
- # def redirect(*args)
248
- # run Cuba::Ron.new { on(default) { res.redirect(*args) }}
249
- # end
250
- #
251
- # on "account" do
252
- # redirect "/login" unless session["uid"]
253
- #
254
- # res.write "Super secure account info."
255
- # end
256
- def run(app)
257
- throw :ron_run_next_app, app
258
- end
259
- end
260
- end