2 |
--------------------------------------------------------------------------------
/test/views/content-yield.erb:
--------------------------------------------------------------------------------
1 | This is the actual content.
2 |
--------------------------------------------------------------------------------
/test/views/home.str:
--------------------------------------------------------------------------------
1 |
"]
78 | end
79 | end
80 |
81 | test "caching behavior" do
82 | Thread.current[:_cache] = nil
83 |
84 | Cuba.plugin Cuba::Render
85 | Cuba.settings[:render][:views] = "./test/views"
86 |
87 | Cuba.define do
88 | on "foo/:i" do |i|
89 | res.write partial("test", title: i)
90 | end
91 | end
92 |
93 | 10.times do |i|
94 | _, _, resp = Cuba.call({ "PATH_INFO" => "/foo/#{i}", "SCRIPT_NAME" => "" })
95 | end
96 |
97 | assert_equal 1, Thread.current[:_cache].instance_variable_get(:@cache).size
98 | end
99 |
100 | test "overrides layout" do
101 | Cuba.plugin Cuba::Render
102 | Cuba.settings[:render][:views] = "./test/views"
103 |
104 | Cuba.define do
105 | on true do
106 | res.write view("home", { name: "Agent Smith", title: "Home" }, "layout-alternative")
107 | end
108 | end
109 |
110 | _, _, body = Cuba.call({})
111 |
112 | assert_response body, ["Alternative Layout: Home\n
Home
\n
Hello Agent Smith
"]
113 | end
114 |
115 | test "ensures content-type header is set" do
116 | Cuba.plugin(Cuba::Render)
117 |
118 | Cuba.define do
119 | on default do
120 | res.status = 403
121 | render("about", title: "Hello Cuba")
122 | end
123 | end
124 |
125 | _, headers, _ = Cuba.call({})
126 |
127 | assert_equal("text/html; charset=utf-8", headers["content-type"])
128 | end
129 |
--------------------------------------------------------------------------------
/test/captures.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("helper", File.dirname(__FILE__))
2 |
3 | test "doesn't yield HOST" do
4 | Cuba.define do
5 | on host("example.com") do |*args|
6 | res.write args.size
7 | end
8 | end
9 |
10 | env = { "HTTP_HOST" => "example.com" }
11 |
12 | _, _, resp = Cuba.call(env)
13 |
14 | assert_response resp, ["0"]
15 | end
16 |
17 | test "doesn't yield the verb" do
18 | Cuba.define do
19 | on get do |*args|
20 | res.write args.size
21 | end
22 | end
23 |
24 | env = { "REQUEST_METHOD" => "GET" }
25 |
26 | _, _, resp = Cuba.call(env)
27 |
28 | assert_response resp, ["0"]
29 | end
30 |
31 | test "doesn't yield the path" do
32 | Cuba.define do
33 | on get, "home" do |*args|
34 | res.write args.size
35 | end
36 | end
37 |
38 | env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/home",
39 | "SCRIPT_NAME" => "/" }
40 |
41 | _, _, resp = Cuba.call(env)
42 |
43 | assert_response resp, ["0"]
44 | end
45 |
46 | test "yields the segment" do
47 | Cuba.define do
48 | on get, "user", :id do |id|
49 | res.write id
50 | end
51 | end
52 |
53 | env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/user/johndoe",
54 | "SCRIPT_NAME" => "/" }
55 |
56 | _, _, resp = Cuba.call(env)
57 |
58 | assert_response resp, ["johndoe"]
59 | end
60 |
61 | test "yields a number" do
62 | Cuba.define do
63 | on get, "user", :id do |id|
64 | res.write id
65 | end
66 | end
67 |
68 | env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/user/101",
69 | "SCRIPT_NAME" => "/" }
70 |
71 | _, _, resp = Cuba.call(env)
72 |
73 | assert_response resp, ["101"]
74 | end
75 |
76 | test "yield a file name with a matching extension" do
77 | Cuba.define do
78 | on get, "css", extension("css") do |file|
79 | res.write file
80 | end
81 | end
82 |
83 | env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/css/app.css",
84 | "SCRIPT_NAME" => "/" }
85 |
86 | _, _, resp = Cuba.call(env)
87 |
88 | assert_response resp, ["app"]
89 | end
90 |
91 | test "yields a segment per nested block" do
92 | Cuba.define do
93 | on :one do |one|
94 | on :two do |two|
95 | on :three do |three|
96 | res.write one
97 | res.write two
98 | res.write three
99 | end
100 | end
101 | end
102 | end
103 |
104 | env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/one/two/three",
105 | "SCRIPT_NAME" => "/" }
106 |
107 | _, _, resp = Cuba.call(env)
108 |
109 | assert_response resp, ["one", "two", "three"]
110 | end
111 |
112 | test "consumes a slash if needed" do
113 | Cuba.define do
114 | on get, "(.+\\.css)" do |file|
115 | res.write file
116 | end
117 | end
118 |
119 | env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/foo/bar.css",
120 | "SCRIPT_NAME" => "/" }
121 |
122 | _, _, resp = Cuba.call(env)
123 |
124 | assert_response resp, ["foo/bar.css"]
125 | end
126 |
127 | test "regex captures in string format" do
128 | Cuba.define do
129 | on get, "posts/(\\d+)-(.*)" do |id, slug|
130 | res.write id
131 | res.write slug
132 | end
133 | end
134 |
135 |
136 | env = { "REQUEST_METHOD" => "GET",
137 | "PATH_INFO" => "/posts/123-postal-service",
138 | "SCRIPT_NAME" => "/" }
139 |
140 | _, _, resp = Cuba.call(env)
141 |
142 |
143 | assert_response resp, ["123", "postal-service"]
144 | end
145 |
146 | test "regex captures in regex format" do
147 | Cuba.define do
148 | on get, %r{posts/(\d+)-(.*)} do |id, slug|
149 | res.write id
150 | res.write slug
151 | end
152 | end
153 |
154 | env = { "REQUEST_METHOD" => "GET",
155 | "PATH_INFO" => "/posts/123-postal-service",
156 | "SCRIPT_NAME" => "/" }
157 |
158 | _, _, resp = Cuba.call(env)
159 |
160 |
161 | assert_response resp, ["123", "postal-service"]
162 | end
163 |
--------------------------------------------------------------------------------
/lib/cuba.rb:
--------------------------------------------------------------------------------
1 | require "delegate"
2 | require "rack"
3 | require "rack/session"
4 |
5 | class Cuba
6 | SLASH = "/".freeze
7 | EMPTY = "".freeze
8 | SEGMENT = "([^\\/]+)".freeze
9 | DEFAULT = "text/html; charset=utf-8".freeze
10 | REGEXES = Hash.new { |h, pattern| h[pattern] = /\A\/(#{pattern})(\/|\z)/ }
11 |
12 | class Response
13 | LOCATION = "location".freeze
14 |
15 | module ContentType
16 | HTML = "text/html".freeze # :nodoc:
17 | TEXT = "text/plain".freeze # :nodoc:
18 | JSON = "application/json".freeze # :nodoc:
19 | end
20 |
21 | attr_accessor :status
22 |
23 | attr :body
24 | attr :headers
25 |
26 | def initialize(headers = {})
27 | @status = nil
28 | @headers = headers
29 | @body = []
30 | @length = 0
31 | end
32 |
33 | def [](key)
34 | @headers[key]
35 | end
36 |
37 | def []=(key, value)
38 | @headers[key] = value
39 | end
40 |
41 | def write(str)
42 | s = str.to_s
43 |
44 | @length += s.bytesize
45 | @headers[Rack::CONTENT_LENGTH] = @length.to_s
46 | @body << s
47 | end
48 |
49 | # Write response body as text/plain
50 | def text(str)
51 | @headers[Rack::CONTENT_TYPE] = ContentType::TEXT
52 | write(str)
53 | end
54 |
55 | # Write response body as text/html
56 | def html(str)
57 | @headers[Rack::CONTENT_TYPE] = ContentType::HTML
58 | write(str)
59 | end
60 |
61 | # Write response body as application/json
62 | def json(str)
63 | @headers[Rack::CONTENT_TYPE] = ContentType::JSON
64 | write(str)
65 | end
66 |
67 | def redirect(path, status = 302)
68 | @headers[LOCATION] = path
69 | @status = status
70 | end
71 |
72 | def finish
73 | [@status, @headers, @body]
74 | end
75 |
76 | def set_cookie(key, value)
77 | Rack::Utils.set_cookie_header!(@headers, key, value)
78 | end
79 |
80 | def delete_cookie(key, value = {})
81 | Rack::Utils.delete_cookie_header!(@headers, key, value)
82 | end
83 | end
84 |
85 | def self.reset!
86 | @app = nil
87 | @prototype = nil
88 | end
89 |
90 | def self.app
91 | @app ||= Rack::Builder.new
92 | end
93 |
94 | def self.use(middleware, *args, **kwargs, &block)
95 | app.use(middleware, *args, **kwargs, &block)
96 | end
97 |
98 | def self.define(&block)
99 | app.run new(&block)
100 | end
101 |
102 | def self.prototype
103 | @prototype ||= app.to_app
104 | end
105 |
106 | def self.call(env)
107 | prototype.call(env)
108 | end
109 |
110 | def self.plugin(mixin)
111 | include mixin
112 | extend mixin::ClassMethods if defined?(mixin::ClassMethods)
113 |
114 | mixin.setup(self) if mixin.respond_to?(:setup)
115 | end
116 |
117 | def self.settings
118 | @settings ||= {}
119 | end
120 |
121 | def self.deepclone(obj)
122 | Marshal.load(Marshal.dump(obj))
123 | end
124 |
125 | def self.inherited(child)
126 | child.settings.replace(deepclone(settings))
127 | end
128 |
129 | attr :env
130 | attr :req
131 | attr :res
132 | attr :captures
133 |
134 | def initialize(&blk)
135 | @blk = blk
136 | @captures = []
137 | end
138 |
139 | def settings
140 | self.class.settings
141 | end
142 |
143 | def call(env)
144 | dup.call!(env)
145 | end
146 |
147 | def call!(env)
148 | @env = env
149 | @req = settings[:req].new(env)
150 | @res = settings[:res].new(settings[:default_headers].dup)
151 |
152 | # This `catch` statement will either receive a
153 | # rack response tuple via a `halt`, or will
154 | # fall back to issuing a 404.
155 | #
156 | # When it `catch`es a throw, the return value
157 | # of this whole `call!` method will be the
158 | # rack response tuple, which is exactly what we want.
159 | catch(:halt) do
160 | instance_eval(&@blk)
161 |
162 | not_found
163 | res.finish
164 | end
165 | end
166 |
167 | def session
168 | env["rack.session"] || raise(RuntimeError,
169 | "You're missing a session handler. You can get started " +
170 | "by adding Cuba.use Rack::Session::Cookie")
171 | end
172 |
173 | # The heart of the path / verb / any condition matching.
174 | #
175 | # @example
176 | #
177 | # on get do
178 | # res.write "GET"
179 | # end
180 | #
181 | # on get, "signup" do
182 | # res.write "Signup"
183 | # end
184 | #
185 | # on "user/:id" do |uid|
186 | # res.write "User: #{uid}"
187 | # end
188 | #
189 | # on "styles", extension("css") do |file|
190 | # res.write render("styles/#{file}.sass")
191 | # end
192 | #
193 | def on(*args, &block)
194 | try do
195 | # For every block, we make sure to reset captures so that
196 | # nesting matchers won't mess with each other's captures.
197 | @captures = []
198 |
199 | # We stop evaluation of this entire matcher unless
200 | # each and every `arg` defined for this matcher evaluates
201 | # to a non-false value.
202 | #
203 | # Short circuit examples:
204 | # on true, false do
205 | #
206 | # # PATH_INFO=/user
207 | # on true, "signup"
208 | return unless args.all? { |arg| match(arg) }
209 |
210 | # The captures we yield here were generated and assembled
211 | # by evaluating each of the `arg`s above. Most of these
212 | # are carried out by #consume.
213 | yield(*captures)
214 |
215 | if res.status.nil?
216 | if res.body.empty?
217 | not_found
218 | else
219 | res.headers[Rack::CONTENT_TYPE] ||= DEFAULT
220 | res.status = 200
221 | end
222 | end
223 |
224 | halt(res.finish)
225 | end
226 | end
227 |
228 | # @private Used internally by #on to ensure that SCRIPT_NAME and
229 | # PATH_INFO are reset to their proper values.
230 | def try
231 | script, path = env[Rack::SCRIPT_NAME], env[Rack::PATH_INFO]
232 |
233 | yield
234 |
235 | ensure
236 | env[Rack::SCRIPT_NAME], env[Rack::PATH_INFO] = script, path
237 | end
238 | private :try
239 |
240 | def consume(pattern)
241 | matchdata = env[Rack::PATH_INFO].match(REGEXES[pattern])
242 |
243 | return false unless matchdata
244 |
245 | path, *vars = matchdata.captures
246 |
247 | env[Rack::SCRIPT_NAME] += "/#{path}"
248 | env[Rack::PATH_INFO] = "#{vars.pop}#{matchdata.post_match}"
249 |
250 | captures.push(*vars)
251 | end
252 | private :consume
253 |
254 | def match(matcher, segment = SEGMENT)
255 | case matcher
256 | when String then consume(matcher.gsub(/:\w+/, segment))
257 | when Regexp then consume(matcher)
258 | when Symbol then consume(segment)
259 | when Proc then matcher.call
260 | else
261 | matcher
262 | end
263 | end
264 |
265 | # A matcher for files with a certain extension.
266 | #
267 | # @example
268 | # # PATH_INFO=/style/app.css
269 | # on "style", extension("css") do |file|
270 | # res.write file # writes app
271 | # end
272 | def extension(ext = "\\w+")
273 | lambda { consume("([^\\/]+?)\.#{ext}\\z") }
274 | end
275 |
276 | # Ensures that certain request parameters are present. Acts like a
277 | # precondition / assertion for your route. A default value can be
278 | # provided as a second argument. In that case, it always matches
279 | # and the result is either the parameter or the default value.
280 | #
281 | # @example
282 | # # POST with data like user[fname]=John&user[lname]=Doe
283 | # on "signup", param("user") do |atts|
284 | # User.create(atts)
285 | # end
286 | #
287 | # on "login", param("username", "guest") do |username|
288 | # # If not provided, username == "guest"
289 | # end
290 | def param(key, default = nil)
291 | value = req.params[key.to_s] || default
292 |
293 | lambda { captures << value unless value.to_s.empty? }
294 | end
295 |
296 | # Useful for matching against the request host (i.e. HTTP_HOST).
297 | #
298 | # @example
299 | # on host("account1.example.com"), "api" do
300 | # res.write "You have reached the API of account1."
301 | # end
302 | def host(hostname)
303 | hostname === req.host
304 | end
305 |
306 | # If you want to match against the HTTP_ACCEPT value.
307 | #
308 | # @example
309 | # # HTTP_ACCEPT=application/xml
310 | # on accept("application/xml") do
311 | # # automatically set to application/xml.
312 | # res.write res["Content-Type"]
313 | # end
314 | def accept(mimetype)
315 | lambda do
316 | accept = String(env["HTTP_ACCEPT"]).split(",")
317 |
318 | if accept.any? { |s| s.strip == mimetype }
319 | res[Rack::CONTENT_TYPE] = mimetype
320 | end
321 | end
322 | end
323 |
324 | # Syntactic sugar for providing catch-all matches.
325 | #
326 | # @example
327 | # on default do
328 | # res.write "404"
329 | # end
330 | def default
331 | true
332 | end
333 |
334 | # Access the root of the application.
335 | #
336 | # @example
337 | #
338 | # # GET /
339 | # on root do
340 | # res.write "Home"
341 | # end
342 | def root
343 | env[Rack::PATH_INFO] == SLASH || env[Rack::PATH_INFO] == EMPTY
344 | end
345 |
346 | # Syntatic sugar for providing HTTP Verb matching.
347 | #
348 | # @example
349 | # on get, "signup" do
350 | # end
351 | #
352 | # on post, "signup" do
353 | # end
354 | def get; req.get? end
355 | def post; req.post? end
356 | def put; req.put? end
357 | def patch; req.patch? end
358 | def delete; req.delete? end
359 | def head; req.head? end
360 | def options; req.options? end
361 | def link; req.link? end
362 | def unlink; req.unlink? end
363 | def trace; req.trace? end
364 |
365 | # If you want to halt the processing of an existing handler
366 | # and continue it via a different handler.
367 | #
368 | # @example
369 | # def redirect(*args)
370 | # run Cuba.new { on(default) { res.redirect(*args) }}
371 | # end
372 | #
373 | # on "account" do
374 | # redirect "/login" unless session["uid"]
375 | #
376 | # res.write "Super secure account info."
377 | # end
378 | def run(app)
379 | halt app.call(req.env)
380 | end
381 |
382 | def halt(response)
383 | throw :halt, response
384 | end
385 |
386 | # Adds ability to pass information to a nested Cuba application.
387 | # It receives two parameters: a hash that represents the passed
388 | # information and a block. The #vars method is used to retrieve
389 | # a hash with the passed information.
390 | #
391 | # class Platforms < Cuba
392 | # define do
393 | # platform = vars[:platform]
394 | #
395 | # on default do
396 | # res.write(platform) # => "heroku" or "salesforce"
397 | # end
398 | # end
399 | # end
400 | #
401 | # Cuba.define do
402 | # on "(heroku|salesforce)" do |platform|
403 | # with(platform: platform) do
404 | # run(Platforms)
405 | # end
406 | # end
407 | # end
408 | #
409 | def with(dict = {})
410 | old, env["cuba.vars"] = vars, vars.merge(dict)
411 | yield
412 | ensure
413 | env["cuba.vars"] = old
414 | end
415 |
416 | # Returns a hash with the information set by the #with method.
417 | #
418 | # with(role: "admin", site: "main") do
419 | # on default do
420 | # res.write(vars.inspect)
421 | # end
422 | # end
423 | # # => '{:role=>"admin", :site=>"main"}'
424 | #
425 | def vars
426 | env["cuba.vars"] ||= {}
427 | end
428 |
429 | def not_found
430 | res.status = 404
431 | end
432 | end
433 |
434 | Cuba.settings[:req] = Rack::Request
435 | Cuba.settings[:res] = Cuba::Response
436 | Cuba.settings[:default_headers] = {}
437 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Cuba
2 | ====
3 |
4 | _n_. a microframework for web development.
5 |
6 | 
7 |
8 | Community
9 | ---------
10 |
11 | Meet us on IRC: [#cuba.rb][irc] on [freenode.net][freenode].
12 |
13 | [irc]: irc://chat.freenode.net/#cuba.rb
14 | [freenode]: http://freenode.net/
15 |
16 | Description
17 | -----------
18 |
19 | Cuba is a microframework for web development originally inspired
20 | by [Rum][rum], a tiny but powerful mapper for [Rack][rack]
21 | applications.
22 |
23 | It integrates many templates via [Tilt][tilt], and testing via
24 | [Cutest][cutest] and [Capybara][capybara].
25 |
26 | [rum]: http://github.com/chneukirchen/rum
27 | [rack]: http://github.com/rack/rack
28 | [tilt]: http://github.com/rtomayko/tilt
29 | [cutest]: http://github.com/djanowski/cutest
30 | [capybara]: http://github.com/jnicklas/capybara
31 | [rack-test]: https://github.com/brynary/rack-test
32 |
33 | Installation
34 | ------------
35 |
36 | ``` console
37 | $ gem install cuba
38 | ```
39 |
40 | Usage
41 | -----
42 |
43 | Here's a simple application:
44 |
45 | ``` ruby
46 | # cat hello_world.rb
47 | require "cuba"
48 | require "cuba/safe"
49 |
50 | Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"
51 |
52 | Cuba.plugin Cuba::Safe
53 |
54 | Cuba.define do
55 | on get do
56 | on "hello" do
57 | res.write "Hello world!"
58 | end
59 |
60 | on root do
61 | res.redirect "/hello"
62 | end
63 | end
64 | end
65 | ```
66 |
67 | And the test file:
68 |
69 | ``` ruby
70 | # cat hello_world_test.rb
71 | require "cuba/test"
72 | require "./hello_world"
73 |
74 | scope do
75 | test "Homepage" do
76 | get "/"
77 |
78 | follow_redirect!
79 |
80 | assert_equal "Hello world!", last_response.body
81 | end
82 | end
83 | ```
84 |
85 | To run it, you can create a `config.ru` file:
86 |
87 | ``` ruby
88 | # cat config.ru
89 | require "./hello_world"
90 |
91 | run Cuba
92 | ```
93 |
94 | You can now run `rackup` and enjoy what you have just created.
95 |
96 | Matchers
97 | --------
98 |
99 | Here's an example showcasing how different matchers work:
100 |
101 | ``` ruby
102 | require "cuba"
103 | require "cuba/safe"
104 |
105 | Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"
106 |
107 | Cuba.plugin Cuba::Safe
108 |
109 | Cuba.define do
110 |
111 | # only GET requests
112 | on get do
113 |
114 | # /
115 | on root do
116 | res.write "Home"
117 | end
118 |
119 | # /about
120 | on "about" do
121 | res.write "About"
122 | end
123 |
124 | # /styles/basic.css
125 | on "styles", extension("css") do |file|
126 | res.write "Filename: #{file}" #=> "Filename: basic"
127 | end
128 |
129 | # /post/2011/02/16/hello
130 | on "post/:y/:m/:d/:slug" do |y, m, d, slug|
131 | res.write "#{y}-#{m}-#{d} #{slug}" #=> "2011-02-16 hello"
132 | end
133 |
134 | # /username/foobar
135 | on "username/:username" do |username|
136 | user = User.find_by_username(username) # username == "foobar"
137 |
138 | # /username/foobar/posts
139 | on "posts" do
140 |
141 | # You can access `user` here, because the `on` blocks
142 | # are closures.
143 | res.write "Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
144 | end
145 |
146 | # /username/foobar/following
147 | on "following" do
148 | res.write user.following.size #=> "1301"
149 | end
150 | end
151 |
152 | # /search?q=barbaz
153 | on "search", param("q") do |query|
154 | res.write "Searched for #{query}" #=> "Searched for barbaz"
155 | end
156 | end
157 |
158 | # only POST requests
159 | on post do
160 | on "login" do
161 |
162 | # POST /login, user: foo, pass: baz
163 | on param("user"), param("pass") do |user, pass|
164 | res.write "#{user}:#{pass}" #=> "foo:baz"
165 | end
166 |
167 | # If the params `user` and `pass` are not provided, this
168 | # block will get executed.
169 | on true do
170 | res.write "You need to provide user and pass!"
171 | end
172 | end
173 | end
174 | end
175 | ```
176 |
177 | Note that once an `on` block matches, processing halts at the conclusion of that block.
178 |
179 | Status codes
180 | ------------
181 |
182 | If you don't assign a status code and you don't write to the `res`
183 | object, the status will be set as `404`. The method `not_found` is
184 | in charge of setting the proper status code, and you can redefine
185 | it if you want to render a template or configure custom headers.
186 |
187 | For example:
188 |
189 | ``` ruby
190 | Cuba.define do
191 | on get do
192 | on "hello" do
193 | res.write "hello world"
194 | end
195 | end
196 | end
197 |
198 | # Requests:
199 | #
200 | # GET / # 404
201 | # GET /hello # 200
202 | # GET /hello/world # 200
203 | ```
204 |
205 | As you can see, as soon as something was written to the response,
206 | the status code was changed to 200.
207 |
208 | If you want to match just "hello", but not "hello/world", you can do
209 | as follows:
210 |
211 | ``` ruby
212 | Cuba.define do
213 | on get do
214 | on "hello" do
215 | on root do
216 | res.write "hello world"
217 | end
218 | end
219 | end
220 | end
221 |
222 | # Requests:
223 | #
224 | # GET / # 404
225 | # GET /hello # 200
226 | # GET /hello/world # 404
227 | ```
228 |
229 | You can also use a regular expression to match the end of line:
230 |
231 | ``` ruby
232 | Cuba.define do
233 | on get do
234 | on /hello\/?\z/ do
235 | res.write "hello world"
236 | end
237 | end
238 | end
239 |
240 | # Requests:
241 | #
242 | # GET / # 404
243 | # GET /hello # 200
244 | # GET /hello/world # 404
245 | ```
246 |
247 | This last example is not a common usage pattern. It's here only to
248 | illustrate how Cuba can be adapted for different use cases.
249 |
250 | If you need this behavior, you can create a helper:
251 |
252 | ``` ruby
253 | module TerminalMatcher
254 | def terminal(path)
255 | /#{path}\/?\z/
256 | end
257 | end
258 |
259 | Cuba.plugin TerminalMatcher
260 |
261 | Cuba.define do
262 | on get do
263 | on terminal("hello") do
264 | res.write "hello world"
265 | end
266 | end
267 | end
268 | ```
269 |
270 | Security
271 | --------
272 |
273 | The most important security consideration is to use `https` for all
274 | requests. If that's not the case, any attempt to secure the application
275 | could be in vain. The rest of this section assumes `https` is
276 | enforced.
277 |
278 | When building a web application, you need to include a security
279 | layer. Cuba ships with the `Cuba::Safe` plugin, which applies several
280 | security related headers to prevent attacks like clickjacking and
281 | cross-site scripting, among others. It is not included by default
282 | because there are legitimate uses for plain Cuba (for instance,
283 | when designing an API).
284 |
285 | Here's how to include it:
286 |
287 | ```ruby
288 | require "cuba/safe"
289 |
290 | Cuba.plugin Cuba::Safe
291 | ```
292 |
293 | You should also always set a session secret to some undisclosed
294 | value. Keep in mind that the content in the session cookie is
295 | *not* encrypted.
296 |
297 | ``` ruby
298 | Cuba.use(Rack::Session::Cookie, :secret => "__a_very_long_string__")
299 | ```
300 |
301 | In the end, your application should look like this:
302 |
303 | ```ruby
304 | require "cuba"
305 | require "cuba/safe"
306 |
307 | Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"
308 |
309 | Cuba.plugin Cuba::Safe
310 |
311 | Cuba.define do
312 | on csrf.unsafe? do
313 | csrf.reset!
314 |
315 | res.status = 403
316 | res.write("Not authorized")
317 |
318 | halt(res.finish)
319 | end
320 |
321 | # Now your app is protected against a wide range of attacks.
322 | ...
323 | end
324 | ```
325 |
326 | The `Cuba::Safe` plugin is composed of two modules:
327 |
328 | * `Cuba::Safe::SecureHeaders`
329 | * `Cuba::Safe::CSRF`
330 |
331 | You can include them individually, but while the modularity is good
332 | for development, it's very common to use them in tandem. As that's
333 | the normal use case, including `Cuba::Safe` is the preferred way.
334 |
335 | Cross-Site Request Forgery
336 | --------------------------
337 |
338 | The `Cuba::Safe::CSRF` plugin provides a `csrf` object with the
339 | following methods:
340 |
341 | * `token`: the current security token.
342 | * `reset!`: forces the token to be recreated.
343 | * `safe?`: returns `true` if the request is safe.
344 | * `unsafe?`: returns `true` if the request is unsafe.
345 | * `form_tag`: returns a string with the `csrf_token` hidden input tag.
346 | * `meta_tag`: returns a string with the `csrf_token` meta tag.
347 |
348 | Here's an example of how to use it:
349 |
350 | ```ruby
351 | require "cuba"
352 | require "cuba/safe"
353 |
354 | Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"
355 |
356 | Cuba.plugin Cuba::Safe
357 |
358 | Cuba.define do
359 | on csrf.unsafe? do
360 | csrf.reset!
361 |
362 | res.status = 403
363 | res.write("Forbidden")
364 |
365 | halt(res.finish)
366 | end
367 |
368 | # Here comes the rest of your application
369 | # ...
370 | end
371 | ```
372 |
373 | You have to include `csrf.form_tag` in your forms and `csrf.meta_tag`
374 | among your meta tags. Here's an example that assumes you are using
375 | `Cuba::Mote` from `cuba-contrib`:
376 |
377 | ```html
378 |
379 |
380 |
381 | {{ app.csrf.meta_tag }}
382 | ...
383 |
384 | ...
385 |
386 |
390 | ...
391 |
392 |
393 | ```
394 |
395 | HTTP Verbs
396 | ----------
397 |
398 | There are matchers defined for the following HTTP Verbs: `get`,
399 | `post`, `put`, `patch`, `delete`, `head`, `options`, `link`, `unlink`
400 | and `trace`. As you have the whole request available via the `req`
401 | object, you can also query it with helper methods like `req.options?`
402 | or `req.head?`, or you can even go to a lower level and inspect the
403 | environment via the `env` object, and check for example if
404 | `env["REQUEST_METHOD"]` equals the verb `PATCH`.
405 |
406 | What follows is an example of different ways of saying the same thing:
407 |
408 | ``` ruby
409 | on env["REQUEST_METHOD"] == "GET", "api" do ... end
410 |
411 | on req.get?, "api" do ... end
412 |
413 | on get, "api" do ... end
414 | ```
415 |
416 | Actually, `get` is syntax sugar for `req.get?`, which in turn is syntax sugar
417 | for `env["REQUEST_METHOD"] == "GET"`.
418 |
419 | Headers
420 | -------
421 |
422 | You can set the headers by assigning values to the hash `req.headers`.
423 | If you want to inspect the incoming headers, you have to read from
424 | the `env` hash. For example, if you want to know the referrer you
425 | can check `env["HTTP_REFERER"]`.
426 |
427 | Request and Response
428 | --------------------
429 |
430 | You may have noticed we use `req` and `res` a lot. Those variables are
431 | instances of [Rack::Request][request] and `Cuba::Response` respectively, and
432 | `Cuba::Response` is just an optimized version of
433 | [Rack::Response][response].
434 |
435 | [request]: http://www.rubydoc.info/github/rack/rack/Rack/Request
436 | [response]: http://www.rubydoc.info/github/rack/rack/Rack/Response
437 |
438 | Those objects are helpers for accessing the request and for building
439 | the response. Most of the time, you will just use `res.write`.
440 |
441 | If you want to use custom `Request` or `Response` objects, you can
442 | set the new values as follows:
443 |
444 | ``` ruby
445 | Cuba.settings[:req] = MyRequest
446 | Cuba.settings[:res] = MyResponse
447 | ```
448 |
449 | Make sure to provide classes compatible with those from Rack.
450 |
451 | Captures
452 | --------
453 |
454 | You may have noticed that some matchers yield a value to the block. The rules
455 | for determining if a matcher will yield a value are simple:
456 |
457 | 1. Regex captures: `"posts/(\\d+)-(.*)"` will yield two values, corresponding to each capture.
458 | 2. Placeholders: `"users/:id"` will yield the value in the position of :id.
459 | 3. Symbols: `:foobar` will yield if a segment is available.
460 | 4. File extensions: `extension("css")` will yield the basename of the matched file.
461 | 5. Parameters: `param("user")` will yield the value of the parameter user, if present.
462 |
463 | The first case is important because it shows the underlying effect of regex
464 | captures.
465 |
466 | In the second case, the substring `:id` gets replaced by `([^\\/]+)` and the
467 | string becomes `"users/([^\\/]+)"` before performing the match, thus it reverts
468 | to the first form we saw.
469 |
470 | In the third case, the symbol--no matter what it says--gets replaced
471 | by `"([^\\/]+)"`, and again we are in presence of case 1.
472 |
473 | The fourth case, again, reverts to the basic matcher: it generates the string
474 | `"([^\\/]+?)\.#{ext}\\z"` before performing the match.
475 |
476 | The fifth case is different: it checks if the the parameter supplied is present
477 | in the request (via POST or QUERY_STRING) and it pushes the value as a capture.
478 |
479 | Composition
480 | -----------
481 |
482 | You can mount a Cuba app, along with middlewares, inside another Cuba app:
483 |
484 | ``` ruby
485 | class API < Cuba; end
486 |
487 | API.use SomeMiddleware
488 |
489 | API.define do
490 | on param("url") do |url|
491 | ...
492 | end
493 | end
494 |
495 | Cuba.define do
496 | on "api" do
497 | run API
498 | end
499 | end
500 | ```
501 |
502 | If you need to pass information to one sub-app, you can use the
503 | `with` method and access it with `vars`:
504 |
505 | ```ruby
506 | class Platforms < Cuba
507 | define do
508 | platform = vars[:platform]
509 |
510 | on default do
511 | res.write(platform) # => "heroku" or "salesforce"
512 | end
513 | end
514 | end
515 |
516 | Cuba.define do
517 | on "(heroku|salesforce)" do |platform|
518 | with(platform: platform) do
519 | run(Platforms)
520 | end
521 | end
522 | end
523 | ```
524 |
525 | ## Embedding routes from other modules
526 |
527 | While the `run` command allows you to handle over the control to a
528 | sub app, sometimes you may want to just embed routes defined in
529 | another module. There's no built-in method to do it, but if you are
530 | willing to experiment you can try the following.
531 |
532 | Let's say you have defined routes in modules `A` and `B`, and you
533 | want to mount those routes in your application.
534 |
535 | First, you will have to extend Cuba with this code:
536 |
537 | ```ruby
538 | class Cuba
539 | def mount(app)
540 | result = app.call(req.env)
541 | halt result if result[0] != 404
542 | end
543 | end
544 | ```
545 |
546 | It doesn't matter where you define it as long as Cuba has already
547 | been required. For instance, you could extract that to a plugin and
548 | it would work just fine.
549 |
550 | Then, in your application, you can use it like this:
551 |
552 | ```ruby
553 | Cuba.define do
554 | on default do
555 | mount A
556 | mount B
557 | end
558 | end
559 | ```
560 |
561 | It should halt the request only if the resulting status from calling
562 | the mounted app is not 404. If you run into some unexpected behavior,
563 | let me know by creating an issue and we'll look at how to workaround
564 | any difficulties.
565 |
566 | Testing
567 | -------
568 |
569 | Given that Cuba is essentially Rack, it is very easy to test with
570 | `Rack::Test`, `Webrat` or `Capybara`. Cuba's own tests are written
571 | with a combination of [Cutest][cutest] and [Rack::Test][rack-test],
572 | and if you want to use the same for your tests it is as easy as
573 | requiring `cuba/test`:
574 |
575 | ``` ruby
576 | require "cuba/test"
577 | require "your/app"
578 |
579 | scope do
580 | test "Homepage" do
581 | get "/"
582 |
583 | assert_equal "Hello world!", last_response.body
584 | end
585 | end
586 | ```
587 |
588 | If you prefer to use [Capybara][capybara], instead of requiring
589 | `cuba/test` you can require `cuba/capybara`:
590 |
591 | ``` ruby
592 | require "cuba/capybara"
593 | require "your/app"
594 |
595 | scope do
596 | test "Homepage" do
597 | visit "/"
598 |
599 | assert has_content?("Hello world!")
600 | end
601 | end
602 | ```
603 |
604 | To read more about testing, check the documentation for
605 | [Cutest][cutest], [Rack::Test][rack-test] and [Capybara][capybara].
606 |
607 | Settings
608 | --------
609 |
610 | Each Cuba app can store settings in the `Cuba.settings` hash. The settings are
611 | inherited if you happen to subclass `Cuba`
612 |
613 | ``` ruby
614 | Cuba.settings[:layout] = "guest"
615 |
616 | class Users < Cuba; end
617 | class Admin < Cuba; end
618 |
619 | Admin.settings[:layout] = "admin"
620 |
621 | assert_equal "guest", Users.settings[:layout]
622 | assert_equal "admin", Admin.settings[:layout]
623 | ```
624 |
625 | Feel free to store whatever you find convenient.
626 |
627 | Rendering
628 | ---------
629 |
630 | Cuba includes a plugin called `Cuba::Render` that provides a couple of helper
631 | methods for rendering templates. This plugin uses [Tilt][tilt], which serves as
632 | an interface to a bunch of different Ruby template engines (ERB, Haml, Sass,
633 | CoffeeScript, etc.), so you can use the template engine of your choice.
634 |
635 | To set up `Cuba::Render`, do:
636 |
637 | ```ruby
638 | require "cuba"
639 | require "cuba/render"
640 | require "erb"
641 |
642 | Cuba.plugin Cuba::Render
643 | ```
644 |
645 | This example uses ERB, a template engine that comes with Ruby. If you want to
646 | use another template engine, one [supported by Tilt][templates], you need to
647 | install the required gem and change the `template_engine` setting as shown
648 | below.
649 |
650 | ```ruby
651 | Cuba.settings[:render][:template_engine] = "haml"
652 | ```
653 |
654 | The plugin provides three helper methods for rendering templates: `partial`,
655 | `view` and `render`.
656 |
657 | ```ruby
658 | Cuba.define do
659 | on "about" do
660 | # `partial` renders a template called `about.erb` without a layout.
661 | res.write partial("about")
662 | end
663 |
664 | on "home" do
665 | # Opposed to `partial`, `view` renders the same template
666 | # within a layout called `layout.erb`.
667 | res.write view("about")
668 | end
669 |
670 | on "contact" do
671 | # `render` is a shortcut to `res.write view(...)`
672 | render("contact")
673 | end
674 | end
675 | ```
676 |
677 | By default, `Cuba::Render` assumes that all templates are placed in a folder
678 | named `views` and that they use the proper extension for the chosen template
679 | engine. Also for the `view` and `render` methods, it assumes that the layout
680 | template is called `layout`.
681 |
682 | The defaults can be changed through the `Cuba.settings` method:
683 |
684 | ```ruby
685 | Cuba.settings[:render][:template_engine] = "haml"
686 | Cuba.settings[:render][:views] = "./views/admin/"
687 | Cuba.settings[:render][:layout] = "admin"
688 | ```
689 |
690 | NOTE: Cuba doesn't ship with Tilt. You need to install it (`gem install tilt`).
691 |
692 | [templates]: https://github.com/rtomayko/tilt/blob/master/docs/TEMPLATES.md
693 |
694 | Plugins
695 | -------
696 |
697 | Cuba provides a way to extend its functionality with plugins.
698 |
699 | ### How to create plugins
700 |
701 | Authoring your own plugins is pretty straightforward.
702 |
703 | ``` ruby
704 | module MyOwnHelper
705 | def markdown(str)
706 | BlueCloth.new(str).to_html
707 | end
708 | end
709 |
710 | Cuba.plugin MyOwnHelper
711 | ```
712 |
713 | That's the simplest kind of plugin you'll write. In fact, that's exactly how
714 | the `markdown` helper is written in `Cuba::TextHelpers`.
715 |
716 | A more complicated plugin can make use of `Cuba.settings` to provide default
717 | values. In the following example, note that if the module has a `setup` method, it will
718 | be called as soon as it is included:
719 |
720 | ``` ruby
721 | module Render
722 | def self.setup(app)
723 | app.settings[:template_engine] = "erb"
724 | end
725 |
726 | def partial(template, locals = {})
727 | render("#{template}.#{settings[:template_engine]}", locals)
728 | end
729 | end
730 |
731 | Cuba.plugin Render
732 | ```
733 |
734 | This sample plugin actually resembles how `Cuba::Render` works.
735 |
736 | Finally, if a module called `ClassMethods` is present, `Cuba` will be extended
737 | with it.
738 |
739 | ``` ruby
740 | module GetSetter
741 | module ClassMethods
742 | def set(key, value)
743 | settings[key] = value
744 | end
745 |
746 | def get(key)
747 | settings[key]
748 | end
749 | end
750 | end
751 |
752 | Cuba.plugin GetSetter
753 |
754 | Cuba.set(:foo, "bar")
755 |
756 | assert_equal "bar", Cuba.get(:foo)
757 | assert_equal "bar", Cuba.settings[:foo]
758 | ```
759 |
760 | Contributing
761 | ------------
762 |
763 | A good first step is to meet us on IRC and discuss ideas. If that's
764 | not possible, you can create an issue explaining the proposed change
765 | and a use case. We pay a lot of attention to use cases, because our
766 | goal is to keep the code base simple. In many cases, the result of
767 | a conversation will be the creation of another tool, instead of the
768 | modification of Cuba itself.
769 |
770 | If you want to test Cuba, you may want to use a gemset to isolate
771 | the requirements. We recommend the use of tools like [dep][dep] and
772 | [gs][gs], but you can use similar tools like [gst][gst] or [bs][bs].
773 |
774 | The required gems for testing and development are listed in the
775 | `.gems` file. If you are using [dep][dep], you can create a gemset
776 | and run `dep install`.
777 |
778 | [dep]: http://cyx.github.io/dep/
779 | [gs]: http://soveran.github.io/gs/
780 | [gst]: https://github.com/tonchis/gst
781 | [bs]: https://github.com/educabilia/bs
782 |
--------------------------------------------------------------------------------