Waves Overview

We have to start with Hello World. Everyone does it. In Waves, you just put one line of code in your mapping.rb file, and you’re done. Considering how little we’re doing, that’s all you should have to do. Sometimes, MVC is just overkill. And, even though Waves is an MVC framework, it understands.

path '/' { 'Hello, World!' }

Request Mappings Lambdas

Everything in Waves starts with request lambdas. Each mapping consists of a rule and a block. If the rule matches the request, the block is run. You can put whatever you want in the block. Rules consist of either strings or regular expressions and an optional hash of constraints. Binding matches of the regular expression are passed into the block as parameters.

Hello world, version 2:

# /dan => Hello, Dan!
path %r{^/(\w+)$} { |name| "Hello, #{name.capitalize}!" }

Resource = Model + View + Controller

Request mappings support the idea that you are operating on a “resource,” a triple of a model, view, and controller. There are three special methods that can facilitate writing rule blocks for a mapping: use, controller, and view.

The use method simply sets the resource context, i.e., which MVC classes to instantiate.

The controller method instantiates an appropriate controller object, initiates a request, and evaluates a block argument in the scope of the controller. The view method works similarly, except that the block may take an argument, typically the result of evaluating the controller block. Further, view methods may take an assigns hash that will be accessible as instance variables from within an associated template.

A generic rule for finding and displaying something:

path %r{^/#{resource}/#{name}/?$} do | model, name |
  use( resource ) | controller { find(name) } | 
    view { |object| show( resource => object ) }
end

A generic rule for updating something, REST-style:

path %r{^/#{resource}/#{name}/?$}, :method => :post do | model, name |
    use( resource ) | controller { update(name) } | 
      view { |object| show( resource => object ) }
  end
end

Filters

Any URL that starts with /admin requires a login / password:

before %r{^/admin/} do | model, name |
  redirect('/login') unless session[:user]
end

You don’t have to worry whether you might have inadvertantly created a security hole by adding a method to a controller that you forgot to hide or make private. You lock your application down in the request mapping rules and forget it about it.

Mapping Mixins

You can encapsulate sets of common rules using mixins. For example, Waves provides a set of rules for mapping “pretty URLs” to a simple CRUD controller. You can just include it in your mapping file to pick up all the necessary mapping rules:

include Waves::Mapping::PrettyUrls::GetRules
include Waves::Mapping::PrettyUrls::RestRules

Just In Time Resources

Waves allows you to implicitly define resources (MVC triples). You define the idioms you want to use for a given type of resource and Waves will define the necessary classes for you on the fly, when / if they are needed. This speeds up development and reduces the amount of documentation and testing required. It also encourages reuse.

In this example, we haven’t defined an Entry class, but the table exists in the database, which means we can access it programmatically as if we had defined it:

~/blog $ waves_console
irb(main):001:0> M = Blog::Models
irb(main):002:0> M::Entry.all
=> []

First Class Views

In Waves, a View is a class that attempts to map a request to a template. That’s it. However, you can define your own view class if you want. You can even have different Resources use different defaults. It’s up to you.

You can also provide helpers for use in your templates. Helpers are magic the same way that Views and Controllers are; an “intelligent default” is provided that mixes in a variety of helpful methods, but you can add to these, or explicitly define a helper for a specific resource.

The default helpers include support for nested views, layouts, markdown formatting, customizable form controls, and more.

Layouts

Specify the layouts right there in the view.

layout :admin, :title => @entry.title do
  # markup for blog entry editor here
end

If you want to reuse views across layouts, just re-factor, like you would with anything else you want to reuse, using the view method:

layout :admin, :title => @entry.title do
  view :blog_entry, :editor_fragment, :entry => @entry
end

There is no special naming convention for nested views, like prefixing views with underscores. You can do that if you want, but you don’t have to. You can nest any view within another view and you can have multiple, nested layouts if you want.

This means you can have layouts specific to various types of page elements (forms, dialogs, menus, whatever), not just for an entire Web page. Did you just realize you wanted all your forms to use H3 tags instead of H2? No problem, just change your form layout and your done.

Custom Form Properties

Since Waves uses Markaby as its primary templating engine, there is no real need for form helpers just to create a text box or other form control. However, most real-world Web applications have a strong need to consistently render the label, control, help text, control groups, etc., as well as support more sophisticated composite controls, like date pickers. So Waves provides a property helper that uses templates that you can modify to render complete property blocks within a form. When you modify one of these templates, all your application forms will get the new control.

# use our custom date template to render a date picker
property :type => :date, :name => reservation.start_date, 
  :value => @reservation.start_date, :class => :required

Sessions

You can store things in file-based session (database sessions coming soon), from pretty much anywhere you deal with a Resource (the request mappings, controller blocks, view templates, etc.):

session[:user] = @user.id if User.authenticate( email,password)?

Inheritable Configurations

You can define hierarchies of configurations for use in testing, development, and production scenarios because configurations are just Ruby classes and attributes are inherited. You can also incorporate your own attributes into the configuration just by declaring them using the attribute method or by defining a class method on a configuration.

module Blog
  module Configurations
    class Development < Default
      host '127.0.0.1'
      port 3000
      reloadable [ Blog ]
      log :level => :debug  
      application do
        use Rack::ShowExceptions
        use Rack::Static, :urls => [ '/css', '/javascript' ], :root => 'public'
        run Waves::Dispatchers::Default.new
      end       
    end
  end
end

Configurable Applications

You can customize the request processing chain using the application configuration parameter. Thus, you can define your development configuration to use the ShowErrors Rack “middleware” and set your production configuration to include an analytics module. You can even replace or extend the Waves dispatcher with your own!

Soon, we’ll add the ability to customize which Rack::Handler to use, like this:

server :mongrel

True Code Reloading

During development, no one wants to have to keep restarting the server for each change you make to your code. So most frameworks will reload your code on each request. However, this often leads to hard to debug errors when previously defined constants remain in memory. Waves is the first framework that completely unloads old class or module definitions before reloading. Further, reloading is done only on demand, so you incur a minimum performance penalty.

Hot Patching

In production, this means you can “hot patch” your code, since code reloading is safe and since you can reload all of an application module’s code just by calling reload on it. In combination with LiveConsole, you can upload patches to your production servers and then reload the code on the fly without actually restarting your servers.

Cluster Support

Starting up server clusters is a breeze. Just set the ports attribute of your configuration to contain an array of the ports you want to listen on, and then run:

  rake cluster:start
  

To restart, just do:

  rake cluster:restart
  

Thread Safety

I know it isn’t cool right now to do threads. But Ruby 1.9 will support native threading and the coarse-grained nature of most Web apps makes threads ideally suited for them. Threads share memory, meaning you can have more memory for actually processing requests. And even if you decide to use an event-driven model, isn’t it nice to know that using a thread-per-request model is an option? That’s why Waves is written to be thread-safe, using thread-safe libraries like Rack.

Migrations Support

Sequel provides support for migrations and Waves defines the basic Rake tasks necessary to use it. Migrations remains the most reliable way to safely version a database across multiple hosts, and that’s why Waves helps make it easy to use them.

It’s All Just Ruby

Everything is Waves is just Ruby code. And the libraries that Waves favors use the same approach. The configuration files are Ruby. The request mapping rules are just Ruby. Markaby is just Ruby. Sequel supports writing SQL queries as … you guessed it … Ruby. Rack applications are defined using just Ruby.

Furthermore, none of these libraries, including Waves, are intrusive by nature. They add a smattering of extensions to the core Ruby library (mostly via the extensions gem), but they only extend, they don’t override (well, except for RubyGems). So you can use Ruby however you want without unexpected surprises.

Ruby is such a great language, why should your framework get in its way? That is the underlying approach taken by Waves and its component libraries.