lib/nyara/controller.rb in nyara-0.0.1.pre.8 vs lib/nyara/controller.rb in nyara-0.0.1.pre.9
- old
+ new
@@ -4,22 +4,29 @@
end
Controller = Struct.new :request
class Controller
module ClassMethods
- # Connect HTTP +method+, +path+ with +blk+ action
+ # #### Call-seq
+ #
+ # http :get, '/' do
+ # send_string 'hello world'
+ # end
+ #
def http method, path, &blk
- @route_entries ||= []
+ @routes ||= []
@used_ids = {}
- action = RouteEntry.new
+ action = Route.new
action.http_method = HTTP_METHODS[method]
action.path = path
action.set_accept_exts @formats
action.id = @curr_id if @curr_id
+ action.classes = @curr_classes if @curr_classes
+ # todo validate arity of blk (before filters also needs arity validation)
action.blk = blk
- @route_entries << action
+ @routes << action
if @curr_id
raise ArgumentError, "action id #{@curr_id} already in use" if @used_ids[@curr_id]
@used_ids[@curr_id] = true
@curr_id = nil
@@ -29,11 +36,11 @@
end
# Set meta data for next action
def meta tag=nil, opts=nil
if @meta_exist
- raise 'contiguous meta data descriptors, should follow by an action'
+ raise 'contiguous meta data descriptors, should be followed by an action'
end
if tag.nil? and opts.nil?
raise ArgumentError, 'expect tag or options'
end
@@ -41,59 +48,49 @@
opts = tag
tag = nil
end
if tag
- # todo scan class
- id = tag[/\#\w++(\-\w++)*/]
- @curr_id = id.to_sym
+ selectors = tag.scan(/[\#\.]\w++(?:\-\w++)*/).to_a
+ @curr_id = selectors.find{|s| s.start_with?('#') }
+ @curr_id = @curr_id.to_sym if @curr_id
+ @curr_classes = selectors.select{|s| s.start_with?('.') }
end
if opts
# todo add opts: strong param, etag, cache-control
@formats = opts[:formats]
end
@meta_exist = true
end
- # HTTP GET
- def get path, &blk
- http 'GET', path, &blk
- end
+ eval %w[GET POST PUT DELETE PATCH OPTIONS].map{|meth|
+ <<-RUBY
+ def #{meth.downcase} path, &blk
+ http '#{meth}', path, &blk
+ end
+ RUBY
+ }.join "\n"
- # HTTP POST
- def post path, &blk
- http 'POST', path, &blk
+ # Add *before* processor, invoke order is the same as definition order
+ #
+ # #### Call-seq
+ #
+ # before '.foo', '.bar:post', ':get' do
+ # require_login
+ # end
+ #
+ def before *selectors, &p
+ raise ArgumentError, "need a block" unless p
+ @before_filters ||= {}
+ selectors.each do |selector|
+ selector = Route.canonicalize_callback_selector selector
+ (@before_filters[selector] ||= []) << p
+ end
end
- # HTTP PUT
- def put path, &blk
- http 'PUT', path, &blk
- end
-
- # HTTP DELETE
- def delete path, &blk
- http 'DELETE', path, &blk
- end
-
- # HTTP PATCH
- def patch path, &blk
- http 'PATCH', path, &blk
- end
-
- # HTTP OPTIONS<br>
- # todo generate options response for a url<br>
- # see http://tools.ietf.org/html/rfc5789
- def options path, &blk
- http 'OPTIONS', path, &blk
- end
-
- # ---
- # todo http method: trace ?
- # +++
-
# Set default layout
def set_default_layout l
@default_layout = l
end
attr_reader :default_layout
@@ -102,12 +99,13 @@
def set_controller_name n
@controller_name = n
end
attr_reader :controller_name
- def compile_route_entries scope # :nodoc:
- raise "#{self}: no action defined" unless @route_entries
+ # @private
+ def nyara_compile_routes scope # :nodoc:
+ raise "#{self}: no action defined" unless @routes
curr_id = :'#0'
next_id = proc{
while @used_ids[curr_id]
curr_id = curr_id.succ
@@ -116,37 +114,60 @@
curr_id
}
next_id[]
@path_templates = {}
- @route_entries.each do |e|
+ @routes.each do |e|
e.id = next_id[] if e.id.empty?
- define_method e.id, &e.blk
+
+ before_actions = e.matched_lifecycle_callbacks @before_filters
+ senders = []
+ before_actions.each_with_index do |blk, idx|
+ method_name = "#{e.id}\##{idx}"
+ senders << "send #{method_name.inspect}\n"
+ define_method method_name, &blk
+ end
+ method_name = "#{e.id}\##{before_actions.size}"
+ senders << "send #{method_name.inspect}, *xs\n"
+ define_method method_name, e.blk
+ class_eval <<-RUBY
+ def __nyara_tmp_action *xs
+ #{senders.join}
+ rescue Exception => e
+ handle_error e
+ end
+ alias :#{e.id.inspect} __nyara_tmp_action
+ undef __nyara_tmp_action
+ RUBY
+
e.compile self, scope
e.validate
@path_templates[e.id] = e.path_template
end
- @route_entries
+ @routes
end
attr_accessor :path_templates
end
include Renderable
def self.inherited klass
- # klass will also have this inherited method
- # todo check class name
- klass.extend ClassMethods
- [:@used_ids, :@default_layout].each do |iv|
- klass.instance_variable_set iv, klass.superclass.instance_variable_get(iv)
+ # note: klass will also have this inherited method
+
+ unless klass.name.end_with?('Controller')
+ raise "class #{klass.name} < Nyara::Controller -- class name must end with `Controller`"
end
- route_entries = klass.superclass.instance_variable_get :@route_entries
- if route_entries
- route_entries.map! {|e| e.dup }
- klass.instance_variable_set :@route_entries, route_entries
+ klass.extend ClassMethods
+ [:@used_ids, :@default_layout, :@before_filters, :@routes].each do |iv|
+ if value = klass.superclass.instance_variable_get(iv)
+ if value.is_a? Array
+ value = value.map &:dup
+ end
+ klass.instance_variable_set iv, value.dup
+ end
end
end
# Path helper
def path_to id, *args
@@ -173,17 +194,17 @@
path = path_to id, *args, opts
scheme << host << path
end
# Redirect to a url or path, terminates action<br>
- # +status+ can be one of:
+ # `status` can be one of:
#
- # - 300 multiple choices (e.g. offer different languages)
- # - 301 moved permanently
- # - 302 found (default)
- # - 303 see other (e.g. for results of cgi-scripts)
- # - 307 temporary redirect
+ # - 300 - multiple choices (e.g. offer different languages)
+ # - 301 - moved permanently
+ # - 302 - found (default)
+ # - 303 - see other (e.g. for results of cgi-scripts)
+ # - 307 - temporary redirect
#
# Caveats: there's no content in a redirect response yet, if you want one, you can configure nginx to add it
def redirect url_or_path, status=302
status = status.to_i
raise "unsupported redirect status: #{status}" unless HTTP_REDIRECT_STATUS.include?(status)
@@ -209,22 +230,35 @@
Ext.request_send_data r, data.join
Fiber.yield :term_close
end
- # Shortcut for +redirect url_to *xs+
+ # Shortcut for `redirect url_to *xs`
def redirect_to *xs
redirect url_to(*xs)
end
+ # Stop processing and close connection<br>
+ # Calling `halt` closes the connection at once, you may usually need to set status code and send header before halt.
+ #
+ # #### Example
+ #
+ # status 500
+ # send_header
+ # halt
+ #
+ def halt
+ Fiber.yield :term_close
+ end
+
# Request extension or generated by `Accept`
def format
request.format
end
# Request header<br>
- # NOTE to change response header, use +set_header+
+ # NOTE to change response header, use `set_header`
def header
request.header
end
alias headers header
@@ -233,13 +267,13 @@
request.response_header[field] = value
end
# Append an extra line in reponse header
#
- # :call-seq:
+ # #### Call-seq
#
- # add_header_line "X-Myheader: here we are"
+ # add_header_line "X-Myheader: here we are"
#
def add_header_line h
raise 'can not modify sent header' if request.response_header.frozen?
h = h.sub /(?<![\r\n])\z/, "\r\n"
request.response_header_extra_lines << h
@@ -257,16 +291,16 @@
end
alias cookies cookie
# Set cookie, if expires is +Time.now+, will remove the cookie entry
#
- # :call-seq:
+ # #### Call-seq
#
- # set_cookie 'JSESSIONID', 'not-exist'
- # set_cookie 'key-without-value'
+ # set_cookie 'JSESSIONID', 'not-exist'
+ # set_cookie 'key-without-value'
#
- # +opt: default_value+ are:
+ # #### Default values in `opts`
#
# expires: nil
# max_age: nil
# domain: nil
# path: nil
@@ -306,18 +340,18 @@
def status n
raise ArgumentError, "unsupported status: #{n}" unless HTTP_STATUS_FIRST_LINES[n]
Ext.request_set_status request, n
end
- # Set response Content-Type, if there's no +charset+ in +ty+, and +ty+ is not text, adds default charset
+ # Set response Content-Type, if there's no `charset` in `ty`, and `ty` is not text, adds default charset
def content_type ty
mime_ty = MIME_TYPES[ty.to_s]
raise ArgumentError, "bad content type: #{ty.inspect}" unless mime_ty
request.response_content_type = mime_ty
end
- # Send respones first line and header data, and freeze +header+ to forbid further changes
+ # Send respones first line and header data, and freeze `header`, `session`, `flash.next` to forbid further changes
def send_header template_deduced_content_type=nil
r = request
header = r.response_header
Ext.request_send_data r, HTTP_STATUS_FIRST_LINES[r.status]
@@ -349,54 +383,56 @@
Ext.request_send_data request, data.to_s
end
# Send a data chunk, it can send_header first if header is not sent.
#
- # :call-seq:
+ # #### Call-seq
#
- # send_chunk 'hello world!'
+ # send_chunk 'hello world!'
+ #
def send_chunk data
send_header unless request.response_header.frozen?
Ext.request_send_chunk request, data.to_s
end
alias send_string send_chunk
# Set aproppriate headers and send the file<br>
- # :call-seq:
#
- # send_file '/home/www/no-virus-inside.exe', disposition: 'attachment'
+ # #### Call-seq
#
- # options are:
+ # send_file '/home/www/no-virus-inside.exe', disposition: 'attachment'
#
- # [disposition] 'inline' by default, if set to 'attachment', the file is presented as a download item in browser.
- # [x_send_file] if not false/nil, it is considered to be behind a web server.
- # Then the app sends file with only header configures,
- # which proxies the actual action to the web server,
- # which can take the advantage of system calls and reduce transfered data,
- # thus faster.
- # [filename] name for the downloaded file, will use basename of +file+ if not set.
- # [content_type] defaults to the MIME type matching +file+ or +filename+.
+ # #### Options
#
+ # * `disposition` - `'inline'` by default, if set to `'attachment'`, the file is presented as a download item in browser.
+ # * `x_send_file` - if not false/nil, it is considered to be behind a web server.<br>
+ # Then the app sends file with only header configures,<br>
+ # which proxies the actual action to the web server,<br>
+ # which can take the advantage of system calls and reduce transfered data,<br>
+ # thus faster.
+ # * `filename` - name for the downloaded file, will use basename of `file` if not set.
+ # * `content_type` - defaults to the MIME type matching `file` or `filename`.
+ #
# To configure for lighttpd and apache2 mod_xsendfile (https://tn123.org/mod_xsendfile/):
#
- # configure do
- # set :x_send_file, 'X-Sendfile'
- # end
+ # configure do
+ # set :x_send_file, 'X-Sendfile'
+ # end
#
# To configure for nginx (http://wiki.nginx.org/XSendfile):
#
- # configure do
- # set :x_send_file, 'X-Accel-Redirect'
- # end
+ # configure do
+ # set :x_send_file, 'X-Accel-Redirect'
+ # end
#
- # To disable x_send_file while configured:
+ # To disable `x_send_file` while it is enabled globally:
#
- # send_file '/some/file', x_send_file: false
+ # send_file '/some/file', x_send_file: false
#
- # To enable x_send_file while not configured:
+ # To enable `x_send_file` while it is disabled globally:
#
- # send_file '/some/file', x_send_file: 'X-Sendfile'
+ # send_file '/some/file', x_send_file: 'X-Sendfile'
#
def send_file file, disposition: 'inline', x_send_file: Config['x_send_file'], filename: nil, content_type: nil
header = request.response_header
unless header['Content-Type']
@@ -426,16 +462,16 @@
else
# todo nonblock read file?
data = File.binread file
header['Content-Length'] = data.bytesize
send_header unless request.response_header.frozen?
- send_data data
+ Ext.request_send_data request, data
end
Fiber.yield :term_close
end
- # Resume action after +seconds+
+ # Resume action after `seconds`
def sleep seconds
seconds = seconds.to_f
raise ArgumentError, 'bad sleep seconds' if seconds < 0
# NOTE request_wake requires request as param, so this method can not be generalized to Fiber.sleep
@@ -448,20 +484,20 @@
Fiber.yield :sleep # see event.c for the handler
end
# One shot render, and terminate the action.
#
- # :call-seq:
+ # #### Call-seq
#
- # # render a template, engine determined by extension
- # render 'user/index', locals: {}
+ # # render a template, engine determined by extension
+ # render 'user/index', locals: {}
#
- # # with template source, set content type to +text/html+ if not given
- # render erb: "<%= 1 + 1 %>"
+ # # with template source, set content type to +text/html+ if not given
+ # render erb: "<%= 1 + 1 %>"
#
- # # layout can be string or array
- # render 'index', ['inner_layout', 'outer_layout']
+ # # layout can be string or array
+ # render 'index', ['inner_layout', 'outer_layout']
#
# For steam rendering, see #stream
def render view_path=nil, layout: self.class.default_layout, locals: nil, **opts
view = View.new self, view_path, layout, locals, opts
unless request.response_header.frozen?
@@ -470,21 +506,50 @@
view.render
end
# Stream rendering
#
- # :call-seq:
+ # #### Call-seq
#
- # view = stream erb: "<% 5.times do |i| %>i<% Fiber.yield %><% end %>"
- # view.resume # sends "0"
- # view.resume # sends "1"
- # view.resume # sends "2"
- # view.end # sends "34" and closes connection
+ # view = stream erb: "<% 5.times do |i| %>i<% Fiber.yield %><% end %>"
+ # view.resume # sends "0"
+ # view.resume # sends "1"
+ # view.resume # sends "2"
+ # view.end # sends "34" and closes connection
+ #
def stream view_path=nil, layout: self.class.default_layout, locals: nil, **opts
view = View.new self, view_path, layout, locals, opts
unless request.response_header.frozen?
send_header view.deduced_content_type
end
view.stream
+ end
+
+ # Handle error, the default is just log it.
+ # You may custom your error handler by re-defining `handle_error`.
+ # But remember if this fails, the whole program exits.
+ #
+ # #### Customization Example
+ #
+ # def handle_error e
+ # case e
+ # when ActiveRecord::RecordNotFound
+ # # if we are lucky that header has not been sent yet
+ # # we can manage to change response status
+ # status 404
+ # send_header rescue nil
+ # else
+ # super
+ # end
+ # end
+ #
+ def handle_error e
+ if l = Nyara.logger
+ l.error "#{e.class}: #{e.message}"
+ l.error e.backtrace
+ end
+ status 500
+ send_header rescue nil
+ # todo send body without Fiber.yield :term_close
end
end
end