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 Hanami’s app
folder.
A Hanami slice:
- has its own container
- can have its own providers (e.g.
slices/api/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
# frozen_string_literal: true
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 = 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
# frozen_string_literal: true
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"}
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.
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
Under construction - the remainder of slices, including:
- conditional slice loading
- per slice settings