Skip to content
/ tony Public

A focused and straightforward Ruby web framework.

License

Notifications You must be signed in to change notification settings

jubishop/tony

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

Simply Returning Status/Message

You can also return a status and message directly if you prefer.

app.get('/', ->(_, _) {
  return 200, 'Hello World'
})

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, [status, message])

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')
})

You can also add a status and message directly to the throw(:response):

throw(:response, [404, 'Hello world'])

Tony::Request

Fetching Parameters

Tony::Request offers two helper methods for extracting parameters from any request: param(key, default = nil) and list_param(key, default = nil). They will return the given key (or default) or throw a 400 automatically if the param does not exist and no default is given. list_param() will demand the key be of type Enumerable. It will also automatically remove any duplicate or empty entries.

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: ENV.fetch('MY_COOKIE_SECRET'))
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, include Tony::ContentFor, and include Tony::ScriptHelper to incorporate much of the same functionality.

Tony::Slim

A Tony::Slim instance takes four parameters;

  • views: : The path where views are stored. (default is views)
  • layout: : The path to a layout wrapping file (optional, default is nil).
  • partials: : The path where partial views are stored. (default is views/partials)
  • options: : The "option hash" defined in Slim itself. For example if you wanted to use include you could pass include_dirs here. (optional, default is {}).

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))
})

Rendering partials

Tony::Slim provides a way to render partials which allows you to pass local variables into the partial. Specify your partials directory with a partials: 'my_partials' parameter. Then, if you called ==partial(:my_template, some_var: 'some_value') inside a slim view, the file my_partials/my_template.slim would be rendered and the variable some_var would be available for reference inside the partial.

You can also yield inside partials, so if you put a yield inside my_partial.slim and said:

==partial(:my_partial)
  p Hello from on top

The Hello from on top would display wherever you put the yield inside.

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')

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

Timezones

Tony provides an easy method for getting a user's local timezone with every request. Simply add ==timezone_script to the head of your html content. For example:

doctype html
html lang="en"
  head
    ==timezone_script
  body
    | Hello World.

Then, you can access the timezone as a TZInfo::Timezone from any Tony::Request by merely calling the method timezone. For example:

app.post('/save', ->(req, resp) {
  resp.write("Your current timezone is: #{req.timezone}")
})

Module Includes

If you want to include additional functions for rendering inside your .slim files, include them for Tony::Slim and Tony::Slim::Env:

module SlimHelpers
  def say_hello
    'Hello World'
  end
end

Tony::Slim.include(SlimHelpers)
Tony::Slim::Env.include(SlimHelpers)

Then you can use the method SayHello directly from your .slim files:

doctype html
html lang="en"
  body
    ==say_hello

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

More Documentation

License

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

About

A focused and straightforward Ruby web framework.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages