tony

RSpec Status Rubocop Status

A focused and straightforward Ruby web framework.

Tony

Installation

In a Gemfile

source: 'https://www.jubigems.org/'
  gem 'core'
  gem 'tony'
end

Guiding Principles

Understandable

Tony is tiny. There is no excessive metaprogramming or syntactical shenanigans. You can read the code and understand it. Magical constructs can make Hello World examples look beautiful, but become increasingly problematic as your program scales in complexity.

Composition over inheritance

Tony encourages a design pattern of composing small and highly targeted utilities rather than inheriting from one mammoth kitchen-sink base class. No single file is more than 100 lines, and each class has a specific, singular purpose.

Fast and inherently thread safe

Tony follows the elegant design principles of Rack. A Tony app is one instance that is frozen after initialization. Everything regarding a single request happens inside the call() method. This makes Tony inherently fast and thread safe.

One way to do things

We all love the flexibility and expressiveness of Ruby. But when there's just one way to do something, the library code remains simpler and developers moving from one project to another can easily understand what's happening.

Use what exists

Many excellent Rack middlewares and Ruby language features already exist, and there's no reason for Tony to reinvent those wheels.

Hello World

In a config.ru file:

require 'tony'

app = Tony.new
app.get('/', ->(_, resp) {
  resp.write('Hello World')
})

run app

Routing

Tony routes paths to lambdas and passes them two parameters: a Tony::Request and a Tony::Response. These classes extend Rack::Request and Rack::Response respectively. A simple route can be created for exact matches with a String, but you can also pass a Regexp, in which case any named_captures will be appended to the .params Hash inside the Tony::Response:

require 'tony'

app = Tony.new
# This would capture, say: /Tony_Bennett/Life_Is_Beautiful
app.get(%r{^/(?<artist>.+?)/(?<album>.+)$}, ->(req, resp) {
  resp.write("Artist/Album: #{req.params[:artist]}/#{req.params[:album]}")
})

app.post('/save', ->(req, resp) {
  # Save something here, using values in the `req.params` Hash.
  resp.status = 201
  resp.write('Save successful')
})

run app

Not Found

If no path matches, Tony will call the not_found block if it exists.

app.not_found(->(req, resp) {
  # Status will default to 404 unless you set it yourself.
  resp.write("Sorry, #{req.url} is not a valid url")
})

Catching Errors

If any call raises an Error, Tony will catch it and call the error block if it exists, adding the caught error message as .error to the Tony::Response instance. You might want to choose to display a friendly error message in production but raise the stack trace in development. You could do something like:

app.error(->(_, resp) {
  if env['APP_ENV'] == 'production'
    resp.status = 500
    resp.write('Sorry, an error has occurred')
  else
    raise resp.error
  end
})

throw(:response)

Every call is wrapped in a catch(:response), which means wherever you are in the stack, once you've filled in your Tony::Response, you can call throw(:response) to immediately unwind the stack and respond:

def level_three(resp)
  resp.write('Hello from down here!')
  throw(:response)
end

def level_two(resp)
  level_three(resp)
end

def level_one(resp)
  level_two(resp)
end

app.get('/deep_stack', ->(_, resp) {
  level_one(resp)
  resp.404 # this won't get called because of the throw(:response).
  resp.write('No response was found I guess')
})

Encrypted Cookies

Tony provides strong aes-256-cbc encryption, you can see exactly how it works in crypt.rb. Once you've passed a :secret param to your Tony instance, it will provide methods in the Tony::Response to set and encrypt cookies, and in Tony::Request to get and decrypt them. If you don't pass a :secret, Tony will refuse to read or write cookies for you. (Pro-tip: Use SecureRandom to easily make yourself a strong secret.)

app = Tony.new(secret: 'PLEASE_REPLACE_THIS')
app.post('/set_cookie', ->(_, resp) {
  resp.set_cookie('tony', 'bennett')
  resp.write('Ok I set a cookie for key: tony')
})

app.post('/get_cookie', ->(req, resp) {
  value = req.get_cookie('tony')
  resp.write("Ok the cookie value for tony is: #{value}") # bennett
})

Unencrypted Cookies

If you are setting plain text cookies from Javascript, you can read those by using the built in cookies Hash provided by Rack::Request:

app.get('/', ->(req, resp) {
  simple_cookie = req.cookies.fetch('key', 'default_value')
})

Serving Static Files

Tony provides a static file server and an intelligent strategy for ensuring clients always cache files that haven't changed, but also always fetch them again once they have.

  • Tony::Static passes 'public, max-age=31536000, immutable' for the Cache-Control header to tell a client to always cache what its fetched.
  • Tony::AssetTagHelper checks the mtime for each file (just once at launch, then it keeps the value in memory) and it appends that mtime to each asset url as part of a ?v= parameter.

As soon as a file has been modified, the mtime will change and clients will fetch the new version. But as long as it hasn't changed, clients will use the cached version for a year (31536000 seconds).

When env['APP_ENV'] is anything other than production, Tony::AssetTagHelper will instead simply append the current unix timestamp to aid in development, so you always get the latest version on refresh.

Tony::Static

To utilize this functionality, first, add Tony::Static to your Rack config.ru file as a middleware, optionally passing it the file location of all public assets (it defaults to the public folder)

# In config.ru

require `tony`
use Tony::Static, public_folder: `my_public_folder`

# Now you'd create your `Tony::App` instance and `run` as in other examples.

AssetTagHelper

Next, use the methods provided in AssetTagHelper to create your asset tags for CSS, Javascript etc. These will be covered in greater detail in the Rendering (Slim) AssetTagHelper section below.

Rendering (Slim)

Tony provides support for Slim, but, like all parts of Tony, it is a standalone utility and you could easily incorporate your own rendering class instead. You can include Tony::AssetTagHelper and include Tony::ContentFor to incorporate much of the same functionality.

Tony::Slim

A Tony::Slim instance takes two parameters;

  • views: : The path where views are stored. (default is views)
  • layout: : The path to a layout wrapping file (optional, default is nil).

Tony::Slim will automatically append the .slim file extension for you.

app = Tony::App.new
slim = Tony::Slim.new(views: 'my_views', layout: 'my_views/layout')

app.get('/', ->(_, resp) {
  # Renders `my_views/index.slim`, wrapped in `my_views/layout.slim`
  resp.write(slim.render(:index))
})

Tony::AssetTagHelper

Inside your slim template files, these methods will be provided for you, loosely modeled off those provided by ActionView::Helpers::AssetTagHelper in Rails. Tony::AssetTagHelper will automatically append the proper file extension for you.

  • favicon_link_tag(source = :favicon, rel: :icon)
  • preconnect_link_tag(source)
  • image_tag(source, alt:)
  • stylesheet_link_tag(source, media: :screen)
  • javascript_include_tag(source, crossorigin: :anonymous)

There are also a few extras that have no parallel in Rails:

  • google_fonts(*fonts)
  • font_awesome(kit_id)

In slim you use == to call these tags and output their contents directly without any HTML escaping:

/ In a .slim file
==stylesheet_link_tag(:main)
==javascript_include_tag(:main)
==google_fonts('Fira Code', 'Fira Sans')
==font_awesome('123abc')

ContentFor

Tony::Slim provides its own implementation of yield_content and content_for in Tony::ContentFor, which is most commonly used to allow internal views to inject asset tags into the <head> of the layout file. For example:

/ In layout.slim
doctype html
html lang="en"
  head
    ==yield_content :head
  body
    ==yield
/ In view.slim
= content_for :head
  title This Is The Index Page

/ This yields into the body
p Hello World

Enforcing HTTPS

Tony provides its own middleware for enforcing immediate redirects to https. You may ask, why not just use rack-ssl-enforcer? Unfortunately, it is not thread-safe and seems to be dead. So Tony provides a modern, thread-safe alternative.

Simply add Tony::SSLEnforcer to your Rack middlewares. You probably want it at the very top, and you may want to only apply it when in production:

# In config.ru
require 'tony'

use Tony::SSLEnforcer if ENV.fetch('RACK_ENV') == 'production'
# Now add `use Tony::Static` and `run Tony::App as in other examples.`

Sibling Libraries

Tony has some sibling libraries that offer additional functionality:

3rd Party Auth

tony/auth provides middleware for users to log in via 3rd party services.

Testing

tony/test provides helpers for testing an app written using Tony.

Production Examples

License

The gem is available as open source under the terms of the MIT License.