cuba 2.0.1 → 2.1.0.rc1

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