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.
