Class: Nyara::Controller

Inherits:
Struct
  • Object
show all
Defined in:
lib/nyara/controller.rb,
lib/nyara/controller.rb

Direct Known Subclasses

ApplicationController, SimpleController

Defined Under Namespace

Modules: ClassMethods

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#requestObject

Returns the value of attribute request

Returns:

  • (Object)

    the current value of request


2
3
4
# File 'lib/nyara/controller.rb', line 2

def request
  @request
end

Class Method Details

.dispatch(request, instance, args) ⇒ Object


211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/nyara/controller.rb', line 211

def self.dispatch request, instance, args
  if cookie_str = request.header._aref('Cookie')
    ParamHash.parse_cookie request.cookie, cookie_str
  end
  request.flash = Flash.new(
    request.session = Session.decode(request.cookie)
  )

  l = Nyara.logger

  if instance
    if l
      l.info "#{request.http_method} #{request.path} => #{instance.class}"
      if %W"POST PUT PATCH".include?(request.http_method)
        l.info "  params: #{instance.params.inspect}"
      end
    end
    instance.send *args
    return
  elsif request.http_method == 'GET' and Config['public']
    path = Config.public_path request.path
    if File.file?(path)
      if l
        l.info "GET #{request.path} => public 200"
      end
      instance = Controller.new request
      instance.send_file path
      return
    end
  elsif Config.development?
    if process_reload(request, l)
      Ext.request_send_data request, "HTTP/1.1 200 OK\r\n\r\n"
      return
    end
  end

  if l
    l.info "#{request.http_method} #{request.path} => 404"
  end
  Ext.request_send_data request, "HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
  Fiber.yield :term_close

rescue Exception
  instance.handle_error($!) if instance
end

.process_reload(request, l) ⇒ Object


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/nyara/controller.rb', line 177

def self.process_reload request, l
  if request.http_method == 'POST' and request.path =~ /\A\/reload:([\w-]+)\z/
    ty = $1
    files = request.param['files']
    case ty
    when 'views-modified'
      files.each do |f|
        if l
          l.info "modified: #{f}"
        end
        View.on_removed f
        View.on_modified f
      end
    when 'views-removed'
      files.each do |f|
        if l
          l.info "removed: #{f}"
        end
        View.on_removed f
      end
    when 'app-modified'
      files.each do |f|
        if l
          l.info "modified: #{f}"
        end
        Reload.load_file f
      end
    else
      return false
    end
    true
  end
end

Instance Method Details

#add_header_line(h) ⇒ Object

Append an extra line in reponse header

Call-seq

add_header_line "X-Myheader: here we are"

376
377
378
379
380
# File 'lib/nyara/controller.rb', line 376

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
end

424
425
426
427
428
# File 'lib/nyara/controller.rb', line 424

def clear_cookie
  cookie.each do |k, _|
    delete_cookie k
  end
end

#content_type(ty) ⇒ Object

Set response Content-Type, if there's no charset in ty, and ty is not text, adds default charset

Raises:

  • (ArgumentError)

446
447
448
449
450
# File 'lib/nyara/controller.rb', line 446

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

389
390
391
# File 'lib/nyara/controller.rb', line 389

def cookie
  request.cookie
end

419
420
421
422
# File 'lib/nyara/controller.rb', line 419

def delete_cookie name
  # todo domain ? path ?
  set_cookie name, nil, expires: Time.now, max_age: 0
end

#flashObject


435
436
437
# File 'lib/nyara/controller.rb', line 435

def flash
  request.flash
end

#formatObject

Request extension or generated by Accept


354
355
356
# File 'lib/nyara/controller.rb', line 354

def format
  request.format
end

#haltObject

Stop processing and close connection
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

349
350
351
# File 'lib/nyara/controller.rb', line 349

def halt
  Fiber.yield :term_close
end

#handle_error(e) ⇒ Object

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

653
654
655
656
657
658
659
660
661
# File 'lib/nyara/controller.rb', line 653

def handle_error e
  if l = Nyara.logger
    l.error "#{e.class}: #{e.message}"
    l.error e.backtrace.join "\n"
  end
  status 500
  send_header rescue nil
  # todo send body without Fiber.yield :term_close
end

#headerObject Also known as: headers

Request header
NOTE to change response header, use set_header


360
361
362
# File 'lib/nyara/controller.rb', line 360

def header
  request.header
end

#paramObject Also known as: params

todo args helper


384
385
386
# File 'lib/nyara/controller.rb', line 384

def param
  request.param
end

#partial(view_path, locals: nil) ⇒ Object

Render a template as string


590
591
592
593
# File 'lib/nyara/controller.rb', line 590

def partial view_path, locals: nil
  view = View.new self, view_path, nil, locals, {}
  view.partial
end

#path_to(id, *args) ⇒ Object

Path helper


258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/nyara/controller.rb', line 258

def path_to id, *args
  if args.last.is_a?(Hash)
    opts = args.pop
  end

  template, meth = self.class.path_templates[id.to_s]
  if template.blank? && meth.blank?
    raise ArgumentError, "#{id} route not found."
  end
  r = template % args

  if opts
    format = opts.delete :format
    r << ".#{format}" if format
    if meth and !opts.key?(:_method) and !opts.key?('_method')
      opts['_method'] = meth
    end
  elsif meth
    opts = {'_method' => meth}
  end

  if opts
    r << '?' << opts.to_query unless opts.empty?
  end
  r
end

#redirect(url_or_path, status = 302) ⇒ Object

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


305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/nyara/controller.rb', line 305

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.response_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'
  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

#redirect_to(identifier, *xs) ⇒ Object

Shortcut for redirect url_to *xs


333
334
335
336
337
338
# File 'lib/nyara/controller.rb', line 333

def redirect_to identifier, *xs
  if identifier !~ /\A\w*#\w++(?:\-\w++)*\z/
    raise ArgumentError, "not action identifier: #{identifier.inspect}, did you mean `redirect`?"
  end
  redirect url_to(identifier, *xs)
end

#render(view_path = nil, layout: self.class.default_layout, locals: nil, **opts) ⇒ Object

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


609
610
611
612
613
614
615
# File 'lib/nyara/controller.rb', line 609

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

#send_chunk(data) ⇒ Object Also known as: send_string

Send a data chunk, it can send_header first if header is not sent.

Call-seq

send_chunk 'hello world!'

492
493
494
495
# File 'lib/nyara/controller.rb', line 492

def send_chunk data
  send_header unless request.response_header.frozen?
  Ext.request_send_chunk request, data.to_s
end

#send_data(data) ⇒ Object

Send raw data, that is, not wrapped in chunked encoding
NOTE: often you should call send_header before doing this.


482
483
484
# File 'lib/nyara/controller.rb', line 482

def send_data data
  Ext.request_send_data request, data.to_s
end

#send_file(file, disposition: 'inline', x_send_file: Config['x_send_file'], filename: nil, content_type: nil) ⇒ Object

Set aproppriate headers and send the file

Call-seq

send_file '/home/www/no-virus-inside.exe', disposition: 'attachment'

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.
    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 it is enabled globally:

send_file '/some/file', x_send_file: false

To enable x_send_file while it is disabled globally:

send_file '/some/file', x_send_file: 'X-Sendfile'

535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
# File 'lib/nyara/controller.rb', line 535

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
      extname.gsub!(".","")
    
      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?
    Ext.request_send_data request, data
  end
  Fiber.yield :term_close
end

#send_header(template_deduced_content_type = nil) ⇒ Object

Send respones first line and header data, and freeze header, session, flash.next to forbid further changes


453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/nyara/controller.rb', line 453

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]

  header.aset_content_type \
    r.response_content_type ||
    header.aref_content_type ||
    (r.accept and MIME_TYPES[r.accept]) ||
    template_deduced_content_type ||
    'text/html'

  header.reverse_merge! OK_RESP_HEADER

  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

  # forbid further modification
  header.freeze
  r.session.freeze
  r.flash.next.freeze
end

#sessionObject


431
432
433
# File 'lib/nyara/controller.rb', line 431

def session
  request.session
end

Set cookie, if expires is +Time.now+, will remove the cookie entry

Call-seq

set_cookie 'JSESSIONID', 'not-exist'
set_cookie 'key-without-value'

Default values in opts

expires: nil max_age: nil domain: nil path: nil secure: nil httponly: true


410
411
412
413
414
415
416
417
# File 'lib/nyara/controller.rb', line 410

def set_cookie name, value=nil, opts={}
  if value.is_a?(Hash)
    raise ArgumentError, 'hash not allowed in cookie value, did you mean to use it as options?'
  end
  # todo default domain ?
  opts = Hash[opts.map{|k,v| [k.to_sym,v]}]
  Cookie.add_set_cookie request.response_header_extra_lines, name, value, opts
end

#set_header(field, value) ⇒ Object

Set response header


366
367
368
# File 'lib/nyara/controller.rb', line 366

def set_header field, value
  request.response_header[field] = value
end

#sleep(seconds) ⇒ Object

Resume action after seconds

Raises:

  • (ArgumentError)

575
576
577
578
579
580
581
582
583
584
585
586
587
# File 'lib/nyara/controller.rb', line 575

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

#status(n) ⇒ Object

Set response status

Raises:

  • (ArgumentError)

440
441
442
443
# File 'lib/nyara/controller.rb', line 440

def status n
  raise ArgumentError, "unsupported status: #{n}" unless HTTP_STATUS_FIRST_LINES[n]
  Ext.request_set_status request, n
end

#stream(view_path = nil, layout: self.class.default_layout, locals: nil, **opts) ⇒ Object

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

627
628
629
630
631
632
633
# File 'lib/nyara/controller.rb', line 627

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

#url_to(id, *args, scheme: nil, host: nil, **opts) ⇒ Object

Url helper
NOTE: host string can include port number
TODO: user and password?


288
289
290
291
292
293
# File 'lib/nyara/controller.rb', line 288

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