lib/cuba.rb in cuba-2.0.1 vs lib/cuba.rb in cuba-2.1.0.rc1
- old
+ new
@@ -1,9 +1,30 @@
+require "rack"
+require "tilt"
require "cuba/version"
-require "cuba/ron"
-module Cuba
+class Rack::Response
+ # 301 Moved Permanently
+ # 302 Found
+ # 303 See Other
+ # 307 Temporary Redirect
+ def redirect(target, status = 302)
+ self.status = status
+ self["Location"] = target
+ end
+end
+
+class Cuba
+ class RedefinitionError < StandardError
+ end
+
+ @@methods = []
+
+ def self.method_added(meth)
+ @@methods << meth
+ end
+
def self.reset!
@app = nil
@prototype = nil
end
@@ -14,16 +35,271 @@
def self.use(middleware, *args, &block)
app.use(middleware, *args, &block)
end
def self.define(&block)
- app.run Cuba::Ron.new(&block)
+ app.run Cuba.new(&block)
end
+ def self.build
+ Class.new(self)
+ end
+
def self.prototype
@prototype ||= app.to_app
end
def self.call(env)
prototype.call(env)
end
-end
\ No newline at end of file
+
+ attr :env
+ attr :req
+ attr :res
+ attr :captures
+
+ def initialize(&blk)
+ @blk = blk
+ @captures = []
+ end
+
+ def call(env)
+ dup._call(env)
+ end
+
+ def _call(env)
+ @env = env
+ @req = Rack::Request.new(env)
+ @res = Rack::Response.new
+ @matched = false
+
+ catch(:ron_run_next_app) do
+ instance_eval(&@blk)
+
+ @res.status = 404 unless @matched || !@res.empty?
+
+ return @res.finish
+ end.call(env)
+ end
+
+ # @private Used internally by #render to cache the
+ # Tilt templates.
+ def _cache
+ Thread.current[:_cache] ||= Tilt::Cache.new
+ end
+ private :_cache
+
+ # Render any type of template file supported by Tilt.
+ #
+ # @example
+ #
+ # # Renders home, and is assumed to be HAML.
+ # render("home.haml")
+ #
+ # # Renders with some local variables
+ # render("home.haml", site_name: "My Site")
+ #
+ # # Renders with HAML options
+ # render("home.haml", {}, ugly: true, format: :html5)
+ #
+ # # Renders in layout
+ # render("layout.haml") { render("home.haml") }
+ #
+ def render(template, locals = {}, options = {}, &block)
+ _cache.fetch(template, locals) {
+ Tilt.new(template, 1, options)
+ }.render(self, locals, &block)
+ end
+
+ # The heart of the path / verb / any condition matching.
+ #
+ # @example
+ #
+ # on get do
+ # res.write "GET"
+ # end
+ #
+ # on get, "signup" do
+ # res.write "Signup
+ # end
+ #
+ # on "user/:id" do |uid|
+ # res.write "User: #{uid}"
+ # end
+ #
+ # on "styles", extension("css") do |file|
+ # res.write render("styles/#{file}.sass")
+ # end
+ #
+ def on(*args, &block)
+ # No use running any other matchers if we've already found a
+ # proper matcher.
+ return if @matched
+
+ try do
+ # For every block, we make sure to reset captures so that
+ # nesting matchers won't mess with each other's captures.
+ @captures = []
+
+ # We stop evaluation of this entire matcher unless
+ # each and every `arg` defined for this matcher evaluates
+ # to a non-false value.
+ #
+ # Short circuit examples:
+ # on true, false do
+ #
+ # # PATH_INFO=/user
+ # on true, "signup"
+ return unless args.all? { |arg| match(arg) }
+
+ begin
+ # The captures we yield here were generated and assembled
+ # by evaluating each of the `arg`s above. Most of these
+ # are carried out by #consume.
+ yield *captures
+
+ ensure
+ # Regardless of what happens in the `yield`, we should ensure that
+ # we successfully set `@matched` to true.
+
+ # At this point, we've successfully matched with some corresponding
+ # matcher, so we can skip all other matchers defined.
+ @matched = true
+ end
+ end
+ end
+
+ # @private Used internally by #on to ensure that SCRIPT_NAME and
+ # PATH_INFO are reset to their proper values.
+ def try
+ script, path = env["SCRIPT_NAME"], env["PATH_INFO"]
+
+ yield
+
+ ensure
+ env["SCRIPT_NAME"], env["PATH_INFO"] = script, path unless @matched
+ end
+ private :try
+
+ def consume(pattern)
+ return unless match = env["PATH_INFO"].match(/\A\/(#{pattern})((?:\/|\z))/)
+
+ path, *vars = match.captures
+
+ env["SCRIPT_NAME"] += "/#{path}"
+ env["PATH_INFO"] = "#{vars.pop}#{match.post_match}"
+
+ captures.push(*vars)
+ end
+ private :consume
+
+ def match(matcher, segment = "([^\\/]+)")
+ case matcher
+ when String then consume(matcher.gsub(/:\w+/, segment))
+ when Regexp then consume(matcher)
+ when Symbol then consume(segment)
+ when Proc then matcher.call
+ else
+ matcher
+ end
+ end
+
+ # A matcher for files with a certain extension.
+ #
+ # @example
+ # # PATH_INFO=/style/app.css
+ # on "style", extension("css") do |file|
+ # res.write file # writes app
+ # end
+ def extension(ext = "\\w+")
+ lambda { consume("([^\\/]+?)\.#{ext}\\z") }
+ end
+
+ # Used to ensure that certain request parameters are present. Acts like a
+ # precondition / assertion for your route.
+ #
+ # @example
+ # # POST with data like user[fname]=John&user[lname]=Doe
+ # on "signup", param("user") do |atts|
+ # User.create(atts)
+ # end
+ def param(key)
+ lambda { captures << req[key] unless req[key].to_s.empty? }
+ end
+
+ def header(key)
+ lambda { env[key.upcase.tr("-","_")] }
+ end
+
+ # Useful for matching against the request host (i.e. HTTP_HOST).
+ #
+ # @example
+ # on host("account1.example.com"), "api" do
+ # res.write "You have reached the API of account1."
+ # end
+ def host(hostname)
+ hostname === req.host
+ end
+
+ # If you want to match against the HTTP_ACCEPT value.
+ #
+ # @example
+ # # HTTP_ACCEPT=application/xml
+ # on accept("application/xml") do
+ # # automatically set to application/xml.
+ # res.write res["Content-Type"]
+ # end
+ def accept(mimetype)
+ lambda do
+ String(env["HTTP_ACCEPT"]).split(",").any? { |s| s.strip == mimetype } and
+ res["Content-Type"] = mimetype
+ end
+ end
+
+ # Syntactic sugar for providing catch-all matches.
+ #
+ # @example
+ # on default do
+ # res.write "404"
+ # end
+ def default
+ true
+ end
+
+ # Syntatic sugar for providing HTTP Verb matching.
+ #
+ # @example
+ # on get, "signup" do
+ # end
+ #
+ # on post, "signup" do
+ # end
+ def get ; req.get? end
+ def post ; req.post? end
+ def put ; req.put? end
+ def delete ; req.delete? end
+
+ # If you want to halt the processing of an existing handler
+ # and continue it via a different handler.
+ #
+ # @example
+ # def redirect(*args)
+ # run Cuba.new { on(default) { res.redirect(*args) }}
+ # end
+ #
+ # on "account" do
+ # redirect "/login" unless session["uid"]
+ #
+ # res.write "Super secure account info."
+ # end
+ def run(app)
+ throw :ron_run_next_app, app
+ end
+
+ # In order to prevent people from overriding the standard Cuba
+ # methods like `get`, `put`, etc, we add this as a safety measure.
+ def self.method_added(meth)
+ if @@methods.include?(meth)
+ raise RedefinitionError, meth
+ end
+ end
+end