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