module Nyara # Contain render methods module Renderable end Controller = Struct.new :request class Controller module ClassMethods # Connect HTTP +method+, +path+ with +blk+ action def http method, path, &blk @route_entries ||= [] @used_ids = {} action = RouteEntry.new action.http_method = HTTP_METHODS[method] action.path = path action.set_accept_exts @formats action.id = @curr_id if @curr_id action.blk = blk @route_entries << 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 @meta_exist = nil end @formats = nil 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' end if tag.nil? and opts.nil? raise ArgumentError, 'expect tag or options' end if opts.nil? and tag.is_a?(Hash) opts = tag tag = nil end if tag # todo scan class id = tag[/\#\w++(\-\w++)*/] @curr_id = id.to_sym 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 # HTTP POST def post path, &blk http 'POST', path, &blk 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
# todo generate options response for a url
# 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 # Set controller name, so you can use a shorter name to reference the controller in path helper 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 curr_id = :'#0' next_id = proc{ while @used_ids[curr_id] curr_id = curr_id.succ end @used_ids[curr_id] = true curr_id } next_id[] @path_templates = {} @route_entries.each do |e| e.id = next_id[] if e.id.empty? define_method e.id, &e.blk e.compile self, scope e.validate @path_templates[e.id] = e.path_template end @route_entries 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) 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 end end # Path helper def path_to id, *args if args.last.is_a?(Hash) opts = args.pop end r = self.class.path_templates[id.to_s] % args if opts format = opts.delete :format r << ".#{format}" if format r << '?' << opts.to_query unless opts.empty? end r end # Url helper
# NOTE: host string can include port number
# TODO: user and password? def url_to id, *args, scheme: nil, host: nil, **opts scheme = scheme ? scheme.sub(/\:?$/, '://') : '//' host ||= request.host_with_port path = path_to id, *args, opts scheme << host << path end # Redirect to a url or path, terminates action
# +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 # # 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) r = request header = r.header self.status status uri = URI.parse url_or_path if uri.host.nil? uri.host = request.domain uri.port = request.port end uri.scheme = r.ssl? ? 'https' : 'http' r.header['Location'] = uri.to_s # similar to send_header, but without content-type Ext.request_send_data r, HTTP_STATUS_FIRST_LINES[r.status] data = header.serialize data.concat r.response_header_extra_lines data << Session.encode_set_cookie(r.session, r.ssl?) data << "\r\n" Ext.request_send_data r, data.join Fiber.yield :term_close end # Shortcut for +redirect url_to *xs+ def redirect_to *xs redirect url_to(*xs) end # Request extension or generated by `Accept` def format request.format end # Request header
# NOTE to change response header, use +set_header+ def header request.header end alias headers header # Set response header def set_header field, value request.response_header[field] = value end # Append an extra line in reponse header # # :call-seq: # # 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 /(? # NOTE: often you should call send_header before doing this. def send_data data 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: # # 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
# :call-seq: # # send_file '/home/www/no-virus-inside.exe', disposition: 'attachment' # # options are: # # [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+. # # To configure for lighttpd and apache2 mod_xsendfile (https://tn123.org/mod_xsendfile/): # # 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 # # To disable x_send_file while configured: # # send_file '/some/file', x_send_file: false # # To enable x_send_file while not configured: # # 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'] unless content_type extname = File.extname(file) extname = File.extname(filename) if extname.blank? and filename content_type = MIME_TYPES[extname] || 'application/octet-stream' end header['Content-Type'] = content_type end disposition = disposition.to_s if disposition != 'inline' if disposition != 'attachment' raise ArgumentError, "disposition should be inline or attachment, but got #{disposition.inspect}" end end filename ||= File.basename file header['Content-Disposition'] = "#{disposition}; filename=#{Ext.escape filename, true}" header['Transfer-Encoding'] = '' # delete it if x_send_file header[x_send_file] = file # todo escape name? send_header unless request.response_header.frozen? else # todo nonblock read file? data = File.binread file header['Content-Length'] = data.bytesize send_header unless request.response_header.frozen? send_data data end Fiber.yield :term_close end # 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 Ext.request_sleep request # place sleep actions before wake Thread.new do Kernel.sleep seconds Ext.request_wakeup request end Fiber.yield :sleep # see event.c for the handler end # One shot render, and terminate the action. # # :call-seq: # # # 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 %>" # # # 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? send_header view.deduced_content_type end view.render end # Stream rendering # # :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 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 end end