Skip to content

Commit 746a8c6

Browse files
committed
Add more content to slices.
1 parent f70a909 commit 746a8c6

File tree

1 file changed

+249
-6
lines changed

1 file changed

+249
-6
lines changed

docs/application-architecture/slices.md

Lines changed: 249 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ sidebar_position: 3
44

55
# Slices
66

7-
In addition to the `/app` directory, Hanami also supports organising your application code into **slices**.
7+
In addition to the `app` directory, Hanami also supports organising your application code into **slices**.
88

9-
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 by feature concern (api or search).
9+
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 separate modules by feature or concern (api or search).
1010

11-
Slices live in the `/slices` directory.
11+
Slices exist in the `slices` directory.
12+
## Creating a slice
1213

13-
To create a slice, you can either create a new directory in `/slices`:
14+
To create a slice, you can either create a new directory in `slices`:
1415

15-
```ruby
16+
```shell
1617
mkdir -p slices/admin
1718

19+
slices
20+
└── admin
21+
1822
bundle exec hanami console
1923
Admin::Slice
2024
=> Admin::Slice
@@ -26,10 +30,249 @@ Or run `bundle exec hanami generate slice api`, which has the added benefit of a
2630
bundle exec hanami generate slice api
2731

2832
slices
33+
├── admin
2934
└── api
3035
├── action.rb
3136
└── actions
3237
```
3338

39+
## Features of a slice
40+
41+
Slices offer much of the same behaviour and features as Hanami's `app` folder.
42+
43+
A Hanami slice:
44+
45+
- has its own container (e.g. `API::Slice.container`)
46+
- can have its own providers (e.g. `slices/api/providers/my_provider.rb`)
47+
- can include actions, routable from the application's router
48+
- can import and export components from other slices
49+
- can be prepared and booted independently of other slices
50+
- can have its own slice-specific settings (e.g. `slices/api/config/settings.rb`)
51+
52+
## Slice containers
53+
54+
Like Hanami's `app` folder, components added to a Hanami slice are automatically organised into the slice's container.
55+
56+
For example, suppose our Bookshelf application, which catalogues international books, is in need of an API to return the name, flag, and currency of a given country. We might create a show action in our API slice (by adding the file manually or by running `bundle exec hanami generate action countries.show --slice api`), that looks something like:
57+
58+
```ruby title="slices/api/actions/countries/show.rb"
59+
# frozen_string_literal: true
60+
61+
require "countries"
62+
63+
module API
64+
module Actions
65+
module Countries
66+
class Show < API::Action
67+
include Deps[
68+
query: "queries.countries.show"
69+
]
70+
71+
params do
72+
required(:country_code).value(included_in?: ISO3166::Country.codes)
73+
end
74+
75+
def handle(request, response)
76+
response.format = format(:json)
77+
78+
halt 422, {error: "Unprocessable country code"}.to_json unless request.params.valid?
79+
80+
result = query.call(
81+
request.params[:country_code]
82+
)
83+
84+
response.body = result.to_json
85+
end
86+
end
87+
end
88+
end
89+
end
90+
```
91+
92+
This action checks that the provided country code (`request.params[:country_code]`) is a valid ISO3166 code (using the countries gem) and returns a 422 response if it isn't.
93+
94+
If the code is valid, the action calls the countries show query (by including the `"queries.countries.show"` item from the slice's container - aliased here as `query` for readability). That class might look like:
95+
96+
```ruby title="slices/api/queries/countries/show.rb"
97+
# frozen_string_literal: true
98+
99+
require "countries"
100+
101+
module API
102+
module Queries
103+
module Countries
104+
class Show
105+
def call(country_code)
106+
country = ISO3166::Country[country_code]
107+
108+
{
109+
name: country.iso_short_name,
110+
flag: country.emoji_flag,
111+
currency: country.currency_code
112+
}
113+
end
114+
end
115+
end
116+
end
117+
end
118+
```
119+
120+
As an exercise, as with `Hanami.app` and its app container, we can boot the `API::Slice` to see what its container contains:
121+
122+
```ruby
123+
bundle exec hanami console
124+
125+
bookshelf[development]> API::Slice.boot
126+
=> API::Slice
127+
bookshelf[development]> API::Slice.keys
128+
=> ["settings",
129+
"actions.countries.show",
130+
"queries.countries.show",
131+
"inflector",
132+
"logger",
133+
"notifications",
134+
"rack.monitor",
135+
"routes"]
136+
```
137+
138+
We can call the query with a country code:
139+
140+
```
141+
bookshelf[development]> API::Slice["queries.countries.show"].call("UA")
142+
=> {:name=>"Ukraine", :flag=>"🇺🇦", :currency=>"UAH"}
143+
```
144+
145+
To add a route for our action, we can add the below to our application's `config/routes.rb` file. This is done for you if you used the action generator.
146+
147+
```ruby title="config/routes.rb"
148+
# frozen_string_literal: true
149+
150+
module Bookshelf
151+
class Routes < Hanami::Routes
152+
root { "Hello from Hanami" }
153+
154+
slice :api, at: "/api" do
155+
get "/countries/:country_code", to: "countries.show"
156+
end
157+
end
158+
end
159+
```
160+
161+
`slice :api` tells the router it can find actions for the routes within the block in the API slice. `at: "/api"` specifies an optional mount point, such the routes in the block each be mounted at `/api`.
162+
163+
After running `bundle exec hanami server`, the endpoint can be hit via a `GET` request to `/api/countries/:country_code`:
164+
165+
```shell
166+
curl http://localhost:2300/api/countries/AU
167+
{"name":"Australia","flag":"🇦🇺","currency":"AUD"}
168+
```
169+
170+
171+
# Slice imports and exports
172+
173+
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.
174+
175+
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.
176+
177+
In our bookshelf app, an `Admin` 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 necessarily needs to know how that's acheived. Instead, a `CDN` slice can manage this operation.
178+
179+
```ruby title="slices/cdn/book_covers/purge.rb"
180+
module CDN
181+
module BookCovers
182+
class Purge
183+
def call(book_cover_path)
184+
puts "Purging #{book_cover_path}"
185+
# "Purging logic here!"
186+
end
187+
end
188+
end
189+
end
190+
```
191+
192+
To allow slices other than the CDN slice to use this component, we first export it from the CDN.
193+
194+
Any slice can be optionally configured by creating a file at `config/slices/slice_name.rb`.
195+
196+
Here, we configure the CDN slice to export is purge component:
197+
198+
```ruby title="config/slices/cdn.rb"
199+
module CDN
200+
class Slice < Hanami::Slice
201+
export ["book_covers.purge"]
202+
end
203+
end
204+
```
205+
206+
Now, the `Admin` slice can be configured to import _everything_ that the CDN slice exports:
207+
208+
```ruby title="config/slices/admin.rb"
209+
module Admin
210+
class Slice < Hanami::Slice
211+
import from: :cdn
212+
end
213+
end
214+
```
215+
216+
In action in the console:
217+
218+
```ruby
219+
bundle exec hanami console
220+
221+
bookshelf[development]> Admin::Slice.boot.keys
222+
=> ["settings",
223+
"cdn.book_covers.purge",
224+
"inflector",
225+
"logger",
226+
"notifications",
227+
"rack.monitor",
228+
"routes"]
229+
```
230+
231+
In use within an admin slice component:
232+
233+
```ruby title="slices/admin/books/operations/update.rb"
234+
module Admin
235+
module Books
236+
module Operations
237+
class Update
238+
include Deps[
239+
"repositories.book_repo",
240+
"cdn.book_covers.purge"
241+
]
242+
243+
def call(id, params)
244+
# ... update the book using the book repository ...
245+
246+
# If the update is successful, purge the book cover from the CDN
247+
purge.call(book.cover_path)
248+
end
249+
end
250+
end
251+
end
252+
end
253+
```
254+
255+
It's also possible to import only specific exports 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`:
256+
257+
```ruby title="config/slices/publisher.rb"
258+
module Publisher
259+
class Slice < Hanami::Slice
260+
import keys: ["book_covers.purge"], from: :cdn, as: :content_network
261+
end
262+
end
263+
```
264+
265+
In action in the console:
34266

35-
TODO - the rest of slices :)
267+
```ruby
268+
bundle exec hanami console
269+
270+
bookshelf[development]> Publisher::Slice.boot.keys
271+
=> ["settings",
272+
"content_network.book_covers.purge",
273+
"inflector",
274+
"logger",
275+
"notifications",
276+
"rack.monitor",
277+
"routes"]
278+
```

0 commit comments

Comments
 (0)