A focused and straightforward Ruby web framework.
source: 'https://www.jubigems.org/'
gem 'core'
gem 'tony'
end
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.
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.
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.
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.
Many excellent Rack middlewares and Ruby language features already exist, and there's no reason for Tony
to reinvent those wheels.
In a config.ru
file:
require 'tony'
app = Tony.new
app.get('/', ->(_, resp) {
resp.write('Hello World')
})
run app
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
You can also return a status and message directly if you prefer.
app.get('/', ->(_, _) {
return 200, 'Hello World'
})
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")
})
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
})
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
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.
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
})
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')
})
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 theCache-Control
header to tell a client to always cache what its fetched.Tony::AssetTagHelper
checks themtime
for each file (just once at launch, then it keeps the value in memory) and it appends thatmtime
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.
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.
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.
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.
A Tony::Slim
instance takes four parameters;
views:
: The path where views are stored. (default isviews
)layout:
: The path to a layout wrapping file (optional, default isnil
).partials:
: The path where partial views are stored. (default isviews/partials
)options:
: The "option hash" defined inSlim
itself. For example if you wanted to useinclude
you could passinclude_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))
})
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
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}")
})
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
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.`
Tony
has some sibling libraries that offer additional functionality:
tony/auth
provides middleware for users to log in via 3rd party services.
tony/test
provides helpers for testing an app written using Tony
.
The gem is available as open source under the terms of the MIT License.