Skip to content
Zete Lui edited this page Aug 26, 2013 · 44 revisions

Getting started with a single file

Here is an industry-level, production-ready hello world:

#! /usr/bin/env ruby
require 'nyara'

configure do
  set :port, 3000
  set :workers, 4
  set :env, 'production'
end

get '/' do
  send_string 'hello world!'
end

Then make it executable and run.

Getting started with a project structure

When you need to start with a bigger application, you can use the nyara command to initiate the project with a bit conventions.

Create a new project

For example, you want to create a project named fancyblog:

$ nyara new fancyblog

There are several options for nyara new

$ nyara new fancyblog -t slim         # use slim as template engine
$ nyara new fancyblog -o activerecord # use AR instead of mongo
$ nyara new fancyblog -o none         # don't add any orm yet

To see all options:

$ nyara help new

The generated project uses bundler to manage gem dependencies, you can edit Gemfile to make sure desired gems are included, then

$ cd fancyblog
$ bundle # install all gems, and generates Gemfile.lock

It is recommended that you also add Gemfile.lock into your version control system.

The project structure looks like rails very much:

app/
  assets/
    files/   # files to copy into `public` dir
    scripts/ # js sources
    styles/  # css sources
  controllers/
  models/
  views/
config/
  application.rb  # generic environment and config
  boot.rb         # starts server
  production.rb   # environment for production
  development.rb  # environment for development
  test.rb         # environment for test
  database.yml    # database connection config
  session.key     # session signing key, you should not show it to public
spec/
  spec_helper.rb
  unit/
  integration/
Rakefile   # generates some default tasks
Gemfile    # bundler
Linnerfile # config assets compiling, generates `public` dir
.gitignore

The generated project follows an MVC convention, in which:

  • M -- the model, usually are classes that maps into db tables or document groups
  • C -- the controller, which defines routing and wires models and views up
  • V -- the view (templates)

Start the server

Before starting server, you should:

  • make sure the database server is online.
  • check database configs in config/database.yml that it uses correct driver and connects to the right db.
  • if you are using activerecord, rake db:create and rake db:migrate (there's also a convenient nyara g migration YOUR_MIGTATION_NAME).
$ nyara server
$ nyara server -e production # start with production environment
$ nyara server -p 8083       # listen to 8083 instead
$ nyara server -d            # start daemon server in the background
$ nyara help server          # see available options

To start an interactive shell (if you added pry in Gemfile, it will try use pry instead of irb):

$ nyara console
$ nyara help console # see availble options

To force an irb shell

$ nyara console -s irb

All commands can be abbreviated if there's no conflicts:

$ nyara s # start server
$ nyara c # start console

For instant refresh after editing a sass / coffee file, you can install this Chrome plugin:

https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei?hl=en

Generate keys

You may need to generate some keys to secure your cookies. See also Header, param, session and cookie.

$ nyara g session.key
$ nyara g session_cipher.key

Rake tasks

$ rake -T # show all tasks
$ rake routes # display all routes
$ rake assets:build # build assets into public
$ rake assets:clean # clean public

todo db tasks?

Configure options

The global method configure evals the block inside Nyara::Config. Options can be retrieved with get/self[], and can be changed with set/self[]=.

There are options that, are used specially for the app. Here's the list:

configure do
  set :env, 'test'        # set environment
                          # in "development" env reloader is enabled
                          # in "test" env autorun is disabled
                          # in "production" env workers are enabled

  set :port n             # set listening port number
  set :workers n          # set number of worker process
                          # if not set, will estimate best n by CPU count

  set :host, 'some.com'   # host name used in `url_to` helper
  set :root, __dir__      # set app root dir, can affect other dir settings
                          # default is `Dir.pwd`
  set :views, 'templates' # set view dir, relative to root
                          # default is `views`
  set :public, 'static'   # set public dir, relative to root
                          # if not set, nyara will not serve static files

  set :x_send_file, "X-Accel-Redirect"
                          # for accelerating send_file with nginx/apache
                          # see also the "Response helpers" section

  set :logger, true       # equivalent to no setting
  set :logger, false      # do not log
  set :logger, lambda{ MyLogger.new }
  set :logger, MyLogger   # logger will be created with:
                          # `MyLogger.new("production.log")`
                          # or `MyLogger.new(STDOUT)`

  set :before_fork, lambda{ ... set some fd to close_on_exec etc ... }
  set :after_fork, lambda{ ... re-connect db etc ... }

  set :watch, true        # watch app and view change, and auto reload
  set :watch_assets, true # watch assets change, requires linner
  set :timeout, 120       # request lives about 120 seconds if inactive
end

Session options

# config (cookie) session name
set :session, :name, 'JSESSIONID'

# session private RSA key, used in DSA signing
set :session, :key, File.binread('some.pem')

# if set, ciphers the session content with AES
set :session, :cipher_key, 'a cipher key'

# always use secure, that requires your whole site run on https
set :session, :secure, true

# expires after 30 * 60 minutes
# usually no need to set this, default session expires at browser close
set :session, :expires, 30 * 60

To retrieve options, you can visit Nyara.config. For example:

Nyara.config[:port]
Nyara.config[:root]

There are also shortcuts to environment options:

Nyara.config.env
Nyara.config.development?
Nyara.config.test?
Nyara.config.production?

To reset options

Nyara.config.reset

For convenience getting absolute file and directory paths

# strict mode, only files under project_path is returned, otherwise nil
Nyara.config.project_path relative_path

# second arg=false means non-strict mode
Nyara.config.project_path relative_path, false

Nyara.config.views_path relative_path

Nyara.config.public_path relative_path

Nyara.config.path_under 'public', relative_path, true

To use logger

Nyara.config.logger

For less typing, the following methods retrieving config information are delegated in Nyara

Nyara.env
Nyara.production?
Nyara.test?
Nyara.development?
Nyara.project_path
Nyara.views_path
Nyara.public_path
Nyara.path_under
Nyara.logger

Modulize actions with controller

You can group actions into many controllers, for example

# requirement: the class name should end with "Controller"
class MyController < Nyara::Controller
  get '/' do
    send_string "hello my controller"
  end
end

A controller will not response without the mapping configure

configure do
  # MyController may not be defined yet, so use a string identifier
  map "/my", "MyController"
end

You can also map a controller under a module or class

configure do
  map "/my", "FooBar::MyController"
end

module FooBar
  class MyController < Nyara::Controller
    ...
  end
end

Setting controller name can effect Routing and path helpers.

class MyController < Nyara::Controller
  set_controller_name 'your'
end

Setting default layout can effect Render and layout.

class MyController < Nyara::Controller
  set_default_layout 'layout'
end

Example of nesting layouts by inheritance:

class OuterController < Nyara::Controller
  set_default_layout 'outer_layout'
end

class InnerController < OuterController
  set_default_layout ['inner_layout', OuterController.default_layout]
end

Routing and path helper

In Nyara the following http methods are supported:

get
post
put
delete
patch
options

If you want other http methods, you can use the http helper

# map a "POKE" method (I think it will be defined in HTTP 10.0)
http :poke, '/a/path' do
  ...
end

During request handling, HTTP methods other than get and post are identified by:

  • First word of the request, as stated in http protocol.
  • For non-standard browsers, post is used, and a _method param in query specifies the overridden method.

* NOTE that the _method param in request body won't take effect in routing. Because in Nyara routing is finished when header completes.

The path format is very similar to scanf/printf, for example:

get '/users/%u/edit' do |id|
  @user = User.where(id: id).first
  render "users/edit"
end

The %u in the above code specifies the id argument in the path. It can save you some work because paths like /users/foo/edit are rejected because "foo" is not an unsigned integer, and you get the argument id in type of Integer.

Available argument formats are:

  • %d signed integer
  • %u unsigned integer, useful for ids
  • %x hex digits
  • %f floating point number
  • %s string containing not '/'
  • %z string to the end

Action metadata and content format

Actions with an id (which starts with "#") can be associated with path helpers. For example, if you defined an action with

class FooController < Nyara::Controller
  meta "#index"
  get "/" do
    ...
  end
end

You can get the path with

path_to "#index" # only available under FooController

or

path_to "foo#index" # available in every controllers

The action identifier is in the form of CONTROLLER_NAME#ACTION_ID. By default, controller name is computed by snake cased class name minus the Controller suffix. It can be custom with set_controller_name

class FooController < Nyara::Controller
  set_controller_name 'bar'
  ...
end

Then visiting path helper for #index becomes:

path_to "bar#index"

Path helper can receive and validate arguments. For example, you have an action demanding floating point number arguments:

meta "#coordinate"
get '/coordinate/%f,%f' do |x, y|
  ...
end

To generate path for the action

x = 1.1
y = 2.1
path_to "#coordinate", x, y #=> "/coordinate/1.1,2.1"

If the number or type of arguments doesn't match, an error will be raised

path_to "#coordinate", 'foo', 2.1 # type mismatch, error
path_to "#coordinate", 1.1        # arity mismatch, error

The url_to helper is similar to path_to, but adds scheme, domain and host. For example

url_to "#coordinate", 0, 0  #=> "//coordinate/0.0,0.0"

* NOTE In the above example, starting with double slash makes the url flexible. The scheme of the page containing the url will be used.

More examples

url_to "#index", scheme: 'http', format: 'js'
#=> http://localhost:3000/.js

url_to '#index', host: '1.2.3.4:3000', 'q' => 'nyara'
#=> "//1.2.3.4:3000/?q=nyara

Nyara can scan request and choose the most desirable content format. To limit content formats the action responds to, use meta before defining an action:

meta formats: %w[js jpg jpeg]
get '/data' do
  case format
  when 'js'
    ...
  when 'jpg', 'jpeg'
    ...
  end
end

In the above code, the format helper returns the matched content format, which is determined by either:

  • Request path extension. For example, a path of /data.js sets format to 'js'.
  • Accept field in request header. For example, a header of Accept: image/jpeg sets format to 'jpg'.

* NOTE format helper returns String, not Symbol.

Header, param, session and cookie

header, param, session and cookie are hashes with indifferent access, robust to String or Symbol keys.

* NOTE that only mutating session can affect response.

To change response header and cookie, use the following helpers

set_header field, value # set response header
add_header_line # add a response header line,
                # useful when you need to repeat same header fields
set_cookie name, value=nil, opts={}
delete_cookie name
clear_cookie

Flash

You can use flash to pass some information to next action. For example

post '/' do
  flash[:info] = 'Welcome'
  redirect '/'
end

get '/' do
  render erb: "<%= flash['info'] %>"
end

You may come into cases that, a template used in different actions visiting flash[some_key], but the information may come from current action or previous action, so there's flash.now is to specify information only retrievable in current action:

flash.now[:info] = 'Welcome'

Flash is swept as soon as a request is handled, and +flash.next+ will be frozen after header is sent.

The request object

Methods

request.http_method # "GET" or "POST" or "PUT", ... etc
request.scheme      # "http" or "https", ... etc
request.ssl?        # https request
request.path        # path without query
request.query       # query params after path (Hash)
request.path_with_query # path+query (String)
request.scope       # the mapped scope in configure
request.domain
request.port
request.host_with_port  # domain with port
request.xhr?        # whether is ajax
accept              # computed `Accept` MIME types array
                    # with respect to order and q=...
accept_language     # computed `Accept-Language` array
accept_encoding     # computed `Accept-Encoding` array
form?               # request content type is url-encoded or multipart
header
body                # stream
param
cookie
session
flash

Response helpers

status 404          # set response status
content_type 'html' # set response content type
send_header # send response header, and freeze it
send_data   # send raw data, not wrapped in chunked encoding
send_chunk(string)  # use this instead of `send_data` unless
                    # you know what you are doing
send_string(string) # same as `send_chunk`
send_file   # send file, and terminate the action
halt                # terminate action
redirect '/'        # redirect to '/', terminating the action
redirect_to '#index'

Render, layout and partial

There are also render helpers (NOTE that render also terminates action, code after render is not executed)

# render a template
# template path relative to Nyara.config[:views]
# engine determined by file extension
render 'user/index', locals: {foo: 'bar'}

# render with inline template source
# content type is set to +text/html+ if not given
render erb: "<%= 1 + 1 %>"

In the above example, if there are more than one templates matching "user/index", say, "user/index.slim" and "user/index.erb" for example, then an error is raised. You can add extension name to make it definitive:

render 'user/index.slim', locals: {foo: 'bar'}

Render can accept a second argument: the layout(s). If controller has set the default layout, render will use it(them), or you can use other layout(s):

# render with layout
render 'user/index', 'layout'

# render with layouts
render 'user/index', layout: ['layouts/inner', 'layouts/outer']

Current implementation is not very beautiful, they have some Caveats that may lead to unexpected behavior:

  • All <% yield %>, <%= yield %> are transformed into <%== yield >.
  • The first time a template is rendered, all the local variables are fixed.

You can also use partial helper, it doesn't terminate the action, neither sends data. It only renders the template into a string. Examples:

foo = partial 'user/_some_partial'

render erb: "<%= partial 'another_partial', locals: {foo: 'bar'} %>"

View streaming

You can use stream instead of render, which returns a resumable Nyara::View object.

Every call to resume executes the template to next Fiber.yield, and sends out the corresponding data.

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

Other helpers

sleep 3.4         # put current fiber into sleep for 3.4 seconds
                  # while not blocking other clients
handle_error      # the default error handler

You may need to add your own error handler. Here's an 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

Custom helpers

Just define helpers in a module, and include in your controller. For example

module MyHelpers
  def foo
    'bar'
  end
end

class MyController < Nyara::Controller
  include MyHelpers
end

You can use prepend if you want to overwrite some built-in helpers:

module MyRedirectOverWrite
  def redirect path
    # ... do my super awesome redirect!
  end
end

class MyController < Nyara::Controller
  prepend MyRedirectOverWrite
end

Be careful with prepend: most helpers can be safely overwritten, but you should be careful not to re-define request or send_header.

Action lifecycle callbacks

You can use before to add filter / interceptor / decorater to actions. The call seq is

before <selector> do
  ...
end

It is very similar to CSS selectors, here are a few examples:

  • ":get" (verb match with pseudo class) match all actions with get method
  • "#index" (id match) match the action defined with meta "#index"
  • ".auth" (class match) match all actions defined with meta ".auth"
  • ".auth:get" (class + verb match) match all actions defined with meta ".auth" and get method

You can put multiple selectors at once, and it is equivalent to calling before with each individual selectors (union):

before ".auth:post", ".auth:patch" do
  ...
end

Test helper

Assume you have an application like this:

require 'nyara/nyara'

configure do
  map '/', 'App'
end

class App < Nyara::Controller
  get '/' do
    send_string 'hello world'
  end

  meta '#r'
  get '/r' do
    redirect '/'
  end
end

if $0 == __FILE__
  Nyara.setup
  Nyara.start_server
end

You can write test with request / response helpers

require './app.rb'
require 'test/unit'
require 'nyara/test'

class MyTest < Test::Unit::TestCase
  include Nyara::Test

  def test_index
    get '/'
    assert response.success?
  end

  def test_r
    get 'my#r'
    follow_redirect
    assert_equal 'hello world', response.body
  end
end

Test helper methods in Nyara::Test

  • get/post/put/delete/patch/options path, header={}, body_string_or_hash='' initiate a request
  • http method, path, header, body initiate a request, you can use any other methods
  • path_to id, *args path helper just like in a controller, but the controller name must be specified
  • cookie object representing client cookie, you can change it and effect the next request
  • session you can modify session and it will effect the next request
  • request last request object
  • response last response object
  • response.header
  • response.body
  • response.status
  • response.success?
  • redirect_location last redirect location
  • follow_redirect follow redirect and initiate another request
  • env the environment object
  • env.controller last controller object which had processed the request, you can use it for inspecting path_helpers or instance variables... etc

It's OK to initiate any number of requests in one test.

Clone this wiki locally