V2.0: Slices
In addition to the app
directory, Hanami also supports organising your application code into slices.
You can think of slices as distinct modules of your application. A typical case is to use slices to separate your business domains (for example billing, accounting or admin) or to have separate modules for a particular feature (API) or technical concern (search).
Slices exist in the slices
directory.
Creating a slice
Hanami provides a slice generator. To create an API slice, run bundle exec hanami generate slice api
.
This creates a directory in slices
, adding some slice-specific classes like actions:
$ bundle exec hanami generate slice api
slices
└── api
├── action.rb
└── actions
Simply creating a new directory in slices
will also create a slice:
$ mkdir -p slices/admin
slices
└── admin
Features of a slice
Slices offer much of the same behaviour and features as the Hanami app.
A Hanami slice:
- has its own container
- imports a number of standard components from the app
- can have its own providers (e.g.
slices/api/config/providers/my_provider.rb
) - can include actions, routable from the application’s router
- can import and export components from other slices
- can be prepared and booted independently of other slices
- can have its own slice-specific settings (e.g.
slices/api/config/settings.rb
)
Slice containers
Like Hanami’s app
folder, components added to a Hanami slice are automatically organised into the slice’s container.
For example, suppose our Bookshelf application, which catalogues international books, needs an API to return the name, flag, and currency of a given country. We can create a countries show action in our API slice (by running bundle exec hanami generate action countries.show --slice api
or by adding the file manually) that looks like:
# slices/api/actions/countries/show.rb
require "countries"
module API
module Actions
module Countries
class Show < API::Action
include Deps[
query: "queries.countries.show"
]
params do
required(:country_code).value(included_in?: ISO3166::Country.codes)
end
def handle(request, response)
response.format = :json
halt 422, {error: "Unprocessable country code"}.to_json unless request.params.valid?
result = query.call(
request.params[:country_code]
)
response.body = result.to_json
end
end
end
end
end
This action uses the countries gem to check that the provided country code (request.params[:country_code]
) is a valid ISO3166 code and returns a 422 response if it isn’t.
If the code is valid, the action calls the countries show query (aliased here as query
for readability). That class might look like:
# slices/api/queries/countries/show.rb
require "countries"
module API
module Queries
module Countries
class Show
def call(country_code)
country = ISO3166::Country[country_code]
{
name: country.iso_short_name,
flag: country.emoji_flag,
currency: country.currency_code
}
end
end
end
end
end
As an exercise, as with Hanami.app
and its app container, we can boot the API::Slice
to see what its container contains:
bundle exec hanami console
bookshelf[development]> API::Slice.boot
=> API::Slice
bookshelf[development]> API::Slice.keys
=> ["settings",
"actions.countries.show",
"queries.countries.show",
"inflector",
"logger",
"notifications",
"rack.monitor",
"routes"]
We can call the query with a country code:
bookshelf[development]> API::Slice["queries.countries.show"].call("UA")
=> {:name=>"Ukraine", :flag=>"🇺🇦", :currency=>"UAH"}
Standard app components
Since every slice is part of the larger app, a number of standard app components are automatically imported into each slice. These include:
"settings"
— the app’s settings object"inflector"
— the app’s inflector"logger"
— the app’s logger"routes"
— the app’s routes helper
If you have additional components in your app that you wish to make available to each slice, you can configure these via config.shared_app_component_keys
:
# config/app.rb
require "hanami"
module Bookshelf
class App < Hanami::App
config.shared_app_component_keys += ["my_app_component"]
end
end
Think carefully before making components available to every slice, since this can create an undesirable level of coupling between the slices and the app. Instead, you may wish to consider slice imports and exports.
Slice imports and exports
Suppose that our bookshelf application uses a content delivery network (CDN) to serve book covers. While this makes these images fast to download, it does mean that book covers need to be purged from the CDN when they change, in order for freshly updated images to take their place.
Images can be updated in one of two ways: the publisher of the book can sign in and upload a new image, or a Bookshelf staff member can use an admin interface to update an image on the publisher’s behalf.
In our bookshelf app, an Admin
slice supports the latter functionality, and a Publisher
slice the former. Both these slices want to trigger a CDN purge when a book cover is updated, but neither slice needs to know exactly how that’s achieved. Instead, a CDN
slice can manage this operation.
# slices/cdn/book_covers/purge.rb
module CDN
module BookCovers
class Purge
def call(book_cover_path)
# "Purging logic here!"
end
end
end
end
Slices can be configured by creating a file at config/slices/slice_name.rb
.
To configure the Admin
slice to import components from the CDN container (including the purge component above), we can create a config/slices/admin.rb
file with the following configuration:
# config/slices/admin.rb
module Admin
class Slice < Hanami::Slice
import from: :cdn
end
end
Let’s see this import in action in the console, where we can see that the Admin
slices' container now has a "cdn.book_covers.purge"
component:
bundle exec hanami console
bookshelf[development]> Admin::Slice.boot.keys
=> ["settings",
"cdn.book_covers.purge",
"inflector",
"logger",
"notifications",
"rack.monitor",
"routes"]
Using the purge operation from the CDN
slice within the Admin
slice component below is now as simple as using the Deps
mixin:
# slices/admin/books/operations/update.rb
module Admin
module Books
module Operations
class Update
include Deps[
"repositories.book_repo",
"cdn.book_covers.purge"
]
def call(id, params)
# ... update the book using the book repository ...
# If the update is successful, purge the book cover from the CDN
purge.call(book.cover_path)
end
end
end
end
end
It’s also possible to import only specific components from another slice. Here for example, the Publisher
slice imports strictly the purge operation, while also - for reasons of its own choosing - using the suffix content_network
instead of cdn
:
# config/slices/publisher.rb
module Publisher
class Slice < Hanami::Slice
import keys: ["book_covers.purge"], from: :cdn, as: :content_network
end
end
In action in the console:
bundle exec hanami console
bookshelf[development]> Publisher::Slice.boot.keys
=> ["settings",
"content_network.book_covers.purge",
"inflector",
"logger",
"notifications",
"rack.monitor",
"routes"]
Slices can also limit what they make available for export to other slices.
Here, we configure the CDN slice to export only its purge component:
# config/slices/cdn.rb
module CDN
class Slice < Hanami::Slice
export ["book_covers.purge"]
end
end
Slice settings
Every slice having automatic access to the app’s "settings"
component is convenient, but for large apps this may lead to those settings becoming unwieldy: the list of settings can become long, and many settings will not be relevant to large portions of your app.
You can instead elect to define settings within specific slices. To do this, create a config/settings.rb
within your slice directory.
# slices/cdn/config/settings.rb
module CDN
class Settings < Hanami::Settings
setting :cdn_api_key, constructor: Types::String
end
end
With this in place, the "settings"
component within your slice will be an instance of this slice-specific settings object.
CDN_API_KEY=xyz bundle exec hanami console
bookshelf[development]> CDN::Slice["settings"].cdn_api_key # => "xyz"
You can then include the slice settings via the Deps mixin within your slice.
# slices/cdn/book_covers/purge.rb
module CDN
module BookCovers
class Purge
include Deps["settings"]
def call(book_cover_path)
# use settings.cdn_api_key here
end
end
end
end
Slice settings are loaded from environment variables just like the app settings, so take care to ensure you have no naming clashes between your slice and app settings.
See the settings guide for more information on settings.
Slice loading
Hanami will load all slices when your app boots. However, for certain workloads of your app, you may elect to load only a specified list of slices.
Loading specific slices brings the benefit of stronger code isolation, faster boot time and reduced memory usage. If your app had a background worker that processed jobs from one slice only, then it would make sense to load only that slice for the worker’s process.
To do this, set the HANAMI_SLICES
environment variable with a comma-separated list of slice names.
$ HANAMI_SLICES=cdn,other_slice_here bundle exec your_hanami_command
Setting this environment variable is a shortcut for setting config.slices
in your app class.
# config/app.rb
require "hanami"
module Bookshelf
class App < Hanami::App
config.slices = ["cdn"]
end
end
You may find the HANAMI_SLICES
environment variable more convenient since it will not disturb slice loading for all other processes running your app.
Slice routing
By generating an action for a slice, the code generator will add the new corresponding route to config/routes.rb
.
If you need per slice Rack Middleware, you can add within the slice block:
# frozen_string_literal: true
require "omniauth/builder"
require "omniauth-google-oauth2"
module MyApp
class Routes < Hanami::Routes
root { "Hello from Hanami" }
slice :admin, at: "/admin" do
use OmniAuth::Builder do
provider :google_oauth2 # ...
end
get "/users", to: "users.index"
end
end
end
If you want to separate the routes of your slice from those of the application, you can cut the routes from config/routes.rb
and create a new file under the slice directory: slices/admin/config/routes.rb
# frozen_string_literal: true
module MyApp
class Routes < Hanami::Routes
root { "Hello from Hanami" }
slice :admin, at: "/admin"
end
end
# frozen_string_literal: true
require "omniauth/builder"
require "omniauth-google-oauth2"
module Admin
class Routes < Hanami::Routes
use OmniAuth::Builder do
provider :google_oauth2 # ...
end
get "/users", to: "users.index"
end
end