1 Installation

The Ruby language must be installed, as well as a package manager, such as RubyGems. Pakyow plays nice with MRI (1.8.7 and 1.9.2) and JRuby.

Installing Pakyow is super easy:

gem install pakyow

Both pakyow-core and pakyow-presenter will be installed. Now you're ready to rumble.

2 Getting Started

Pakyow includes an application generator to make it easy to get started.

pakyow new application

Then start the server:

cd application
$ pakyow server

2.1 Architecture

Pakyow follows the MVC design pattern, but the flow is a bit different than what you might be used to. When a request is received the views are assembled first (based on the URL). If a route matches the appropriate business logic is invoked. The business logic can then manipulate the view and bind data to it.

2.2 Application Structure

app/lib

This directory contains Ruby source files that define an application's business logic. No structure is forced, all files with a .rb extension are loaded at runtime by default. This behavior is configurable via the 'app.auto_reload' configuration setting.

app/views

All of the views for an application are contained in this directory. For more information about views and view structure, see Views.

config

Upon app generation this directory contains a single file, application.rb, which contains the application class and defines the routes, configuration, and error handlers for the application.

logs

Contains the log file for the application.

public

Any file placed in this directory can be accessed directly through the URL (images, stylesheets, javascripts, etc).

3 Views

Views in Pakyow contain no logic and simply define a structure around the data to be presented. No template language or special markup is needed. Instead, the power of HTML is embraced and utilized in some creative ways to provide an easier and more familiar way to build a view.

3.1 View Construction

View construction begins with a root view, which usually defines the general view structure. Containers are created in the root view, which define the parts of the structure that are generated dynamically. Creating a container is as easy as adding an "id" attribute to any tag. For example, this is a container named "main":

<div id="main"></div>

Pakyow will look for content for this container in a content view named "main.html". Any number of containers can be defined in any view (root views or content views).

Views are defined in a folder hierarchy and are pieced together based on the request path. Say we have the following files in the views directory:

pakyow.html (the default root view; defines one content area named "main")
index/
  main.html
another_page/
  main.html

When a request is made for '/' (or '/index') the content view in the 'index' directory will be used. Same for 'another_page'. This hierarchy can be as deep as needed.

In most cases the view path matches the URL exactly. However there are some exceptions when dealing with routes that define parameters (e.g. 'foo/:bar') and restful routes.

Parameterized routes are collapsed down when determining the view path. For example, the view path for 'foo/:bar' is simply 'foo'. This rule also applies to restful routes, and there is one more rule for the 'show' route. If restful routes are defined for 'posts', the show route would be 'posts/:id'. Instead of the view path collapsing down to 'posts', it becomes 'posts/show'.

Views can be overridden at any location in the hierarchy. Let's add to our view hierarchy defined above:

pakyow.html
index/
  main.html
another_page/
  main.html
  yet_another_page/
    main.html

Requests for 'index' and 'another_page' work as before. When a request is made for 'another_page/yet_another_page' only the main.html content view in the 'yet_another_page' directory will be used. Root views can also be overridden this way.

Since any number of root views can exist, it is necessary to define which root view to use at any given level in the view hierarchy. This can be done by appending '.root_view_name' to the end of a directory. For example:

another_root.html
pakyow.html
index/
  main.html
another_page.another_root/
  main.html

In this case requests for '/another_page' will use the 'another_root.html' root view. Changing the root view on a directory also changes it for directories further down the hierarchy.

3.2 Data Informed

Views are informed of the data they present. This is accomplished with the itemprop attribute. In the following example the view represents the full_name and email of the contact data type:

<div class="contact">
  <span itemprop="contact[full_name]">John Doe</span>
  <a itemprop="contact[email]">johndoe@gmail.com</a>
</div>

Form fields can be informed using the "name" attribute. The following are identical:

<input type="text" name="contact[full_name]">
<input type="text" itemprop="contact[full_name]">

This is how connections are made between front-end and back-end. See View Logic > Binding for more information on how objects are bound to views.

3.3 AJAX & Partial Content

It's often necessary to request content only for part of a view (say for replacing content via AJAX). This is easy in Pakyow. Just set the '_container' parameter in your AJAX request to the container you want content for. The response body will consist of only the content for the container requested.

4 Routes

Routes define how requests are routed to the application's business logic. They are defined in the application's route block.

A route definition consists of an HTTP method and a route spec, which is matched against the request. Each route can be associated with a block:

get '/' {}
post '/' {}
put '/' {}
delete '/' {}

Or routed to a controller and action:

get '/', :MyController, :my_action

A route spec can also be a regular expression:

get /(.)*/ {}

Routes are matched in the order they are defined. Once a match occurs, the business logic for the route is invoked and the response is returned.

Parameters can be defined in the route spec and are accessible in the app.params hash:

get 'foo/:id' do
  Log.enter "foo's id is #{app.params[:id]}"
end

4.1 Default Route

A default route can be defined:

default {}

The default route can also be routed to a controller and action.

This is equivalent to:

get '/', {}

Restful Routes

A shortcut is provided for defining restful routes. The definition consists of a base route spec and a controller:

restful 'posts', :PostsController

This is identical to:

get 'posts', :PostsController, :index
get 'posts/:id', :PostsController, :show
get 'posts/new', :PostsController, :new
post 'posts', :PostsController, :create
get 'posts/edit/:id', :PostsController, :edit
put 'posts/:id', :PostsController, :update
delete 'posts/:id', :PostsController, :delete

A model can also be provided in the restful route definition:

restful 'posts', :PostsController, :Post

Providing the model brings a few advantages when binding. This is discussed in more depth here: Binders - Form Action.

5 Hooks

Hooks are code blocks that can run before, after, and around the code of a route. They are defined in the 'routes' block of application.rb along with routes.

A hook is defined with a name and either a code block or a controller and action.

hook :auth {}
hook :begin, :WorkflowController, :enter

Hooks are attached to routes by adding a hash argument to the route definition. The hash keys can be any of the symbols :before, :after, and :around. The hash values are the name(s) of the hooks to call at a time relative to the route that is specified by the key.

get 'some/route', :before => [:auth, :begin], :after => :other_hook {}

The hash values can either be a single hook name or an array of hook names. If an array, all hooks will be called in the array order at the specified time.

A hook can also be provided in the restful route definition:

restful 'posts', :PostsController, :before => :auth, :around => :posts_hook

In above case, the :auth hook will run before, and the :posts_hook hook with run both before and after, the code of all of the restful 'posts' routes.

6 Handlers

Handlers are named code blocks that can be invoked explicitly and, in two cases, implicitly. They are defined within a 'handlers' block in application.rb.

A handler is defined with a name and an optional response status.

handlers do
    handler :not_found, 404 {}
    handler :error, 500, :ErrorController, :handle_500
    handler :other_handler, :OtherController, :do_other_thing
end

A handler is invoked explicitly by calling 'invoke_handler!(name)' from a route block or controller, hook, or another handler.

app.invoke_handler! :not_found

When invoked, current execution is stopped and control is transferred to the handler. If the handler is defined with a response status, the response is set accordingly.

A handler may also be invoked with a status code argument.

app.invoke_handler! 500

In this case, a handler is executed if there is one with a corresponding response status. In any case, the response status is set to the status argument.

See the section on Error Handling to see how handlers for 404 and 500 response status values are implicitly invoked.

7 Controllers

In Pakyow, controllers are optional but are often helpful for large applications. When a request is matched, it is routed to a controller and an action (unless a block is defined for the route; see Routes for more information).

A controller is a class, an action is a method defined in the class. For example:

class MyController
  def my_action
  end
end

There are several convenience methods (such as 'app', 'request', 'response', and 'presenter') that you will probably want easy access to from your controller. Just include 'Pakyow::Helpers' into your class (see Helpers for more information).

8 View Logic

Pakyow provides several ways for the business logic of an application to easily interact with the views. In most frameworks the view logic is contained in the view itself, but Pakyow separates views and view logic. This keeps the roles for design and developer clearly defined and reduces annoyances.

View construction happens before the business logic is invoked. The business logic can then manipulate the constructed view. Anything that is possible in a template language is possible with Pakyow.

8.1 View Manipulation

The fully constructed view can be accessed like this:

presenter.view

Parts of the view can be accessed with the 'find' method. The 'find' method accepts a CSS selector and returns a collection of views that match the selector.

presenter.view.find('#container')

Several methods are available to modify a view (or collection of views). When a method is called on a view collection, it is simply passed through to the views in the collection. If the method returns a value when called on a single view, calling it on a collection of views will return an array of values.

Content

Content can be set and fetched:

presenter.view.find('#container').content = 'foo'
presenter.view.find('#container').content

It can also be appended:

presenter.view.find('#container').append('bar')

Or:

presenter.view.find('#container').content << 'bar'

Views can be turned into a string of HTML:

presenter.view.find('#container').to_html

Attributes

Attributes can be set or fetched:

presenter.view.find('#container').attributes.class = 'classname'
presenter.view.find('#container').attributes.class

Any HTML attribute can be set or fetched in this way. Current attribute values can be modified using procs. The current value is provided to the block and the return value is used as the new value.

presenter.view.find('#container').attributes.class = lambda { |cur_val| 
  # do something to cur_val 
}

Removing & Clearing Views

A view can be removed:

presenter.view.find('#container').remove

Or have it's content cleared:

presenter.view.find('#container').clear

8.2 View Contexts

Pakyow provides an easy way to group actions performed on a single view (or view collection) into contexts. There are two ways to do this. The first is specific to containers:

with_container :main { ... }

The second approach can be used on any view:

presenter.view.find('#container').in_context { ... }

Inside the block, the view can be accessed through the 'context' method.

with_container :main do
  context.content = 'foo'
end

The makes it really easy to group manipulations together to more easily comprehend what's happening.

8.3 Binding

Data can easily be bound to a view (for more information on how the view is informed of the data it represents, see Views > Data Informed). Given the following view:

<div class="contact">
  <span itemprop="contact[full_name]">John Doe</span>
  <a itemprop="contact[email]">johndoe@gmail.com</a>
</div>

We can bind a Contact object to it:

view.bind(Contact.new(:full_name => "Matz", :email => "matz@ruby-lang.org"))

In this case, the data type is inferred from the object type (Contact becomes contact and FooBar becomes foo_bar). Objects and hashes can also be bound to any label:

data = {:full_name => "Matz", :email => "matz@ruby-lang.org"}
view.bind(data, :to => :contact)

8.4 Repeating Views

A common task in application development is repeating a view for a data collection. Pakyow makes this easy:

presenter.view.find('.contact').repeat_for(contacts)

In this case, the view will be repeated once for each object in the array of contacts. In addition, each contact will be bound to its view automatically.

8.5 Custom Views

A custom view class can be created and used in cases where view logic is repeated across controllers. The view class can be tied to a specific view file.

class MyView < Pakyow::Presenter::View
  view_path 'path/to/default_view.html'
end

The default view file can be overridden by passing a path to the initializer.

MyView.new('path/to/another_view.html')

8.6 Changing the View Path or Root View

The view path can be changed at any point, causing the presentation layer to be rebuilt:

presenter.use_view_path('path/to/views')

A root view can also be changed, again causing the presentation layer to be rebuilt:

presenter.set_view('path/to/root_view.html')

8.7 Page Titles

The title for a page can easily be set, changed, or fetched:

presenter.view.title = 'My Page Title'
presenter.view.title

9 Binders

When binding data to a view it's often necessary to format the data before binding. Sometimes you also need to change other attributes in addition to setting content (e.g. setting the 'href' for an anchor tag). In Pakyow this logic lives in a Binder class. A Binder is a collection of functions that act on the data as it's being presented.

class ContactBinder < Pakyow::Presenter::Binder
  binder_for :contact

  def full_name
    "#{bindable.first_name} #{bindable.last_name}"
  end

  def email
    { 
      :content => bindable.email,
      :href => "mailto:#{bindable.email}"
    }
  end
end

The 'binder_for' method informs Pakyow what data type this Binder should bind data to. In the above example, the binder will be used when binding any data to a view labeled as 'contact'. When the object is bound to a view Pakyow looks for a method that matches the attribute in the binder before looking in the object.

The object being bound is accessible through the 'bindable' method. The method's return value determines the behavior of the binding process. If the return value is a hash it is mapped to content and/or attribtues for the view. Otherwise the value is converted into a string at used as the content for the view.

Attributes and content can be modified using procs. The current value is provided to the block and the return value is used as the new value.

{
  :content => lambda {|current_content| current_content.gsub('foo', 'bar')}
}

Methods defined in the Pakyow::GeneralHelpers module are automatically available in the Binder class (request/response).

9.1 Form Actions

Pakyow makes it easy to change the action and method for a form based on the state of the object. Note this will only work if a Binder exists for the object being bound and restful routes exist for the object type (see Routes).

Here's an example for the Contact object:

<form itemprop="contact[action]">…</form>

To determine state, Pakyow examines the 'id' attribute of the object. In most ORMs (such as Datamapper and ActiveRecord) the 'id' is mapped to the object's primary key in the database.

If 'id' has no value the object is assumed to be a new object, so the form will submit to the object's restful route for 'create'.

If 'id' has a value Pakyow assumes the object has been created and will be updated, thus resulting in configuring the form to submit to the object's restful route for 'update'.

9.2 Dropdowns

When binding to a dropdown, Pakyow will automatically select the option that matches the value for the attribute. Options for the dropdown can be defined in a binder:

class MyBinder < Pakyow::Presenter::Binder
  binder_for :MyModel
  options_for :attribute, [['1', 'Option 1'], ['2', 'Option 2']]

  def attribute
    1
  end
end

The options will automatically be created when binding to the dropdown. Here's an example:

<select name="my_model[attribute]">
  <option value="1" selected>Option 1</option>
  <option value="2">Option 2</option>
</select>

Options can also be specified in a method, which is useful when the options will be created dynamically.

options_for :attribute, :attribute_options

def attribute_options
  [['1', 'Option 1'], ['2', 'Option 2']]
end

9.3 Checkboxes & Radio Buttons

The checkbox or radio button who's value matches the value of the attribute is selected automatically.

10 Helpers

Helper methods are defined in one of two modules: Pakyow::Helpers or Pakyow::GeneralHelpers. GeneralHelpers only provide information and are used in the binders.

10.1 Params

Query string parameters and values from parameterized routes are available in the params hash:

request.params

10.2 Sessions & Cookies

Sessions keep state across requests. They can be enabled by using any Rack session middleware:

middleware do
  use Rack::Session::Cookie
end

Sessions are then accessed through the request object:

request.session[:value] = 'foo'

Cookies can be set or fetched the same way:

request.cookies[:value] = 'foo'

By default a cookie is created for the path '/' and is set to expire in seven days. These defaults can be overridden when setting a cookie:

request.cookies[:value] = { 
  :value => 'foo',
  :expires => Time.now + 3600
}

10.3 Redirecting

Issuing a browser redirect is easy:

app.redirect_to! 'url'

The response status is set to 302 and the response is sent immediately.

You can also pass a status code to 'redirect_to':

app.redirect_to! 'url', 301  # permanent redirect

10.4 Sending Files

A file can be sent:

app.send_file!(file)

A file or path can be passed. You can also pass the file name and the mime type (which is guessed if not passed).

app.send_file!(file, "xml_data.xml", "text/xml")

Data can also be sent:

app.send_data!(xml_data, "text/xml")

The file name can also be passed:

app.send_data!(xml_data, "text/xml", "xml_data.xml")

10.5 Request & Response Objects

The underlying Rack Request & Response objects can be accessed through the 'request' and 'response' methods. This is useful for directly modifying things like response status.

In addition, the following things are available in the request:

request.controller

The instance of the controller used in this request.

request.action

The action called in this request.

request.format

The format used in this request. This value defaults to HTML and is changed by adding an extension to the URL, for example:

/foo.json

request.error

The error that occurred during the request.

10.6 Logging

Write to the log using the static Log class:

Log.puts "Just saying hello to the log"

10.7 Interrupting Execution

halt!

The execution of a route block or controller, a hook, or a handler can be stopped immediately and the current state of the response returned.

app.halt!

invoke_handler!

The execution of a route block or controller, a hook, or a handler can be stopped immediately and control transferred to a handler. See Handlers for more information.

invoke_route!

The execution of a route block or controller, a hook, or a handler can be stopped immediately and control transferred to another block or controller.

app.invoke_route! '/some/route'
app.invoke_route! '/another/route', :get

The content returned will be the same as if this route was originally called from the client. The optional second argument specifies the request method used to find the specified route. If omitted, the request method that is currently be executed is used.

11 Error Handling

See the section on Handlers for how to define a handler that has an associated response status.

If the request doesn't match a route, a view path, or a static file then a 404 response status is set. If a handler is defined with a 404 status then the handler is invoked.

If an exception is raised in the backend code then a 500 response status is set and a 500 status handler is invoked if one is defined.

12 Configuring Pakyow

All configuration is defined in the Application class. If auto_reload is set to "true", the configuration will be reloaded on each request (perfect for development). Otherwise the configuration will be loaded once at runtime.

Configuration can be defined for the entire app:

config.app.log = true
config.app.default_environment = :development

Or for a single environment:

configure :development do
  app.dev_mode = true
end

12.1 Configuration Settings

Configuration settings are broken into three groups: app, presenter, and server.

app.application_path

The path to the file where the application class is defined. This is set automatically and shouldn't be changed.

app.auto_reload

Reloads the application class and all files in "src_dir" on every request.

Defaults to 'true'.

app.default_action

The default action to call when routing to a controller.

Defaults to 'index'.

app.default_environment

The default environment to run the application in.

Defaults to 'development'.

app.dev_mode

Issues warnings instead of breaking certain things. Currently only used by pakyow-presenter to keep the application from breaking when binding an object to a view when the attribute isn't defined on the object being bound.

Defaults to 'true'.

app.errors_in_browser

Displays any errors in the browser (useful for development).

Defaults to 'true'.

app.ignore_routes

Keeps the backend from being used (useful when working with views).

Defaults to 'false'.

app.log

Write logs to a log file, and to STDOUT.

Defaults to 'true'.

app.log_dir

Where the logs live.

Defaults to '{root}/logs'.

app.log_name

What the log file should be named.

Defaults to 'requests.log'.

app.presenter

Sets the presenter to be used. This is set automatically when a presenter is included and shouldn't be changed.

app.public_dir

The folder where static files live (CSS, Javascript, images, etc).

Defaults to '{root}/public'.

app.static

If true, static files will be served up by Pakyow (from {public_dir}). For best performance, set to false and let a web server handle this.

app.root

The root directory of the application.

Defaults to where the application is started from.

app.src_dir

Where the backend code lives.

Defaults to '{root}/app/lib'.

presenter.default_root_view_name

The default root view to use.

Defaults to 'pakyow.html'.

presenter.javascripts

Where the Javascript files live (used when adding/removing resources to a view).

Defaults to '{public_dir}/javascripts'.

presenter.stylesheets

Where the CSS stylesheets live (used when adding/removing resources to a view).

Defaults to '{public_dir}/stylesheets'.

presenter.view_caching

If views are cached, they are loaded only once at runtime. Otherwise they are loaded on every request, so changes are picked up (useful for development).

Defaults to 'false'.

presenter.view_dir

Where the views live.

Defaults to '{root}/app/views'.

server.host

What host to run the application on (only used when running the application with the built-in server).

Defaults to '0.0.0.0'.

server.port

What port to run the application on (only used when running the application with the built-in server).

Defaults to '3000'.

mailer.default_sender

The default value for the message sender. Can be overridden on a message by message basis through the message object.

Defaults to 'Pakyow'.

mailer.default_content_type

The default content type for the message.

Defaults to 'text/html; charset=UTF-8'.

mailer.delivery_method

How the message will be delivered.

Defaults to ':sendmail'.

mailer.delivery_options

Other options that go along with the delivery method.

Defaults to '{:enable_starttls_auto => false}'.

13 Mailer

Pakyow has a separate library, pakyow-mailer, for sending mail. It's built on the mail gem and adds the ability for views to be delivered through email. Here's an example:

Pakyow::Mailer.new(view_path).deliver_to("test@pakyow.com")  # also accepts an array

The view will be constructed just like it would be if it was being presented in a browser. Access to the view is available for manipulation and binding:

mailer = Pakyow::Mailer.new(view_path)
mailer.view.bind(some_data)
mailer.deliver_to("test@pakyow.com")

Access to the message object is also available:

mailer = Pakyow::Mailer.new(view_path)
mailer.message.add_file("/path/to/file.jpg")
mailer.deliver_to("test@pakyow.com")

There are several configuration settings for Mailer. See Configuration Settings for more information.

14 Core-Presenter Interface

A Pakyow Core interacts with Pakyow Presenter through 5 presenter methods.

initialize

When core initializes, the presenter instance is created which calls its initialize method. This happens at startup and is called once.

load

When the application loads, or reloads, the load method of the presenter is called

prepare_for_request

This is called for each request before any code is executed for the route. This allows the presenter to do whatever is necessary to be ready to present content for the request.

content

This is called after any code for the route is executed. The application uses the return value of this method for the response body.

presented?

This method is called to determine if the presenter actually presented anything. The application uses the return value as part of determining whether to respond with a 404.

15 Contributing

Pakyow is hosted on GitHub here. Any issues you encounter can be reported using the GitHub Issue Tracker.

If you'd like to contribute code, please run the test suite after making your changes:

rake test

Then submit a pull request on GitHub.

16 Version History

  • 0.7 (November 19, 2011)

    • The core-presenter interface has been modified slightly.
    • Added bang (!) to application methods that interrupt flow.
    • Binding now supports hashes with the :to syntax.
    • Binders are now binders for the data label instead of object type (e.g. binder_for :blog_post instead of binder_for :BlogPost).
    • Current attribute value can be modified with procs.
    • Routes and Handlers can now be invoked (with invoke_route! and invoke_handler!).
    • Hooks (before/after/around) can be defined for routes.
    • Fully constructed views are cached for each path (significant performance boost).
    • The logger, reloader, and static file handler has been abstracted into Rack Middleware.
    • Pakyow Mailer is now available to send views in an email message.
    • pakyow console script now available.
    • Root view override in index directories no longer specify a root view for siblings.
    • Fix regex route error (was removing route vars).
    • App file is no longer loaded twice upon initialization.
    • Fix cookie creation when cookie is a non-nil value but not a String.
    • Fix problem binding to a checkbox who’s value attribute is not set.
  • 0.6.3 (September 13, 2011):

    • Routing performance has been improved.
    • Several load path issues have been fixed.
    • Gems can now be built and used from anywhere.
    • An inconsistency has been fixed with request.params have both string and symbol keys.
    • Staging an application now loads middleware defined by the application. This simplifies the Rackup file.
    • Binding now works when binding to a binding defined by the HTML 'name' attribute.
  • 0.6.2 (August 30, 2011):

    • JRuby is now supported!
    • Can now run 'pakyow server' on Windows. Sorry for the delay guys.
    • Error handlers now have access to Helpers. Also fixes some issues presenting views from error handlers.
    • Binding an object to a root node now works as one would expect.
    • Fixes an issue when using alphanumeric ids in restful routes.
  • 0.6.1 (August 22, 2011):

    • Fixes gemspec issue (stupid, sorry).
  • 0.6.0 (August 22, 2011):

    • Initial public release.

17 License

(The MIT License)

Copyright © 2011 Bryan Powell, Metabahn

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.