explicit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdfcf17daab19ff8c25fa77a72b8e7d7bf085ee98726f898c7bc74819accda49
4
- data.tar.gz: 2409883f725d14e47afc8509c3083c3e014713053b91b72bc48d13c1f1ea0b16
3
+ metadata.gz: 3dd0ae09016ab5f9efdb05b3b4e7ec68f8c824bbb9fcbe347d3e277ee5f8ae9e
4
+ data.tar.gz: 14d778db7346642da91f253ab53e39471de8e3a4c23e7647cc3803af3961a138
5
5
  SHA512:
6
- metadata.gz: e50376405bd6f2305de7716df8f414f26fc4b660999ec673bc9f20f54d41ccd06f535704c382ab2f636b789db34ba1b39f7929cce974dfb3440b16aa4f53e774
7
- data.tar.gz: 5a6c7fb91762aff2a883165288b2a4d3a6caa5058a243a185c82fa9a4e9acf14fa8db68a3bba7a43aff3935a936f954ff86f46c5c49845ec930515a4f60b2741
6
+ metadata.gz: 5e6d8e4c26483a1e04376956588f220fd6a5442c008a8193c96fececc2b9f4ad2a73ffcb49c9bad22757e7d21237599fcdda977a96bdc204bc8b7f194c7c102c
7
+ data.tar.gz: cd4801c48e82ac2683b3b2a1118f2e908f0189adb975fbe38d49210615463963d8fb0e45afb22e9fde8cba6b21a0d14fb0898d35dab58755137c963994dd8c38
data/README.md CHANGED
@@ -1,21 +1,24 @@
1
1
  # Explicit
2
2
 
3
3
  Explicit is a validation and documentation library for JSON APIs that enforces
4
- documented specs during runtime.
4
+ documented specs at runtime.
5
5
 
6
6
  1. [Installation](#installation)
7
7
  2. [Defining requests](#defining-requests)
8
8
  3. [Reusing specs](#reusing-specs)
9
- 4. [Writing tests](#writing-tests)
10
- 5. [Writing documentation](#writing-documentation)
11
- 6. [Performance benchmark](#performance-benchmark)
9
+ 4. [Reusing requests](#reusing-requests)
10
+ 5. [Writing tests](#writing-tests)
11
+ 6. [Publishing documentation](#publishing-documentation)
12
+ - [Adding request examples](#adding-request-examples)
12
13
  7. Specs
13
14
  - [Agreement](#agreement)
14
15
  - [Array](#array)
16
+ - [BigDecimal](#bigdecimal)
15
17
  - [Boolean](#boolean)
16
18
  - [Date Time ISO8601](#date-time-iso8601)
17
19
  - [Date Time Posix](#date-time-posix)
18
20
  - [Default](#default)
21
+ - [Description](#description)
19
22
  - [Hash](#hash)
20
23
  - [Inclusion](#inclusion)
21
24
  - [Integer](#integer)
@@ -24,9 +27,11 @@ documented specs during runtime.
24
27
  - [One of](#one-of)
25
28
  - [Record](#record)
26
29
  - [String](#string)
27
- 8. Advanced configuration
30
+ 8. [Configuration](#configuration)
31
+ - [Changing examples file path](#changing-examples-file-path)
28
32
  - [Customizing error messages](#customizing-error-messages)
29
33
  - [Customizing error serialization](#customizing-error-serialization)
34
+ 9. [Performance benchmark](#performance-benchmark)
30
35
 
31
36
  # Installation
32
37
 
@@ -38,26 +43,33 @@ gem "explicit", "~> 0.1"
38
43
 
39
44
  # Defining requests
40
45
 
41
- You define request specs by inheriting from `Explicit::Request`. The following
42
- methods are available:
46
+ Call `Explicit::Request.new` to define a request. The following methods are
47
+ available:
43
48
 
44
- - `get(path)` - Adds a route to the request. Use the syntax `:param` for path
45
- params. For example: `get "/customers/:customer_id"`.
49
+ - `get(path)` - Adds a route to the request. Use the syntax `/:param` for path
50
+ params.
46
51
  - There is also `head`, `post`, `put`, `delete`, `options` and `patch` for
47
52
  other HTTP verbs.
48
- - `title(text)` - Adds a title to the request. Displayed in the documentation.
49
- - `description(text)` - Adds a description to the endpoint. Markdown supported.
50
- - `header(name, spec)` - Adds a header to the endpoint.
51
- - `param(name, spec, options = {})` - Adds the request param to the endpoint.
53
+ - `title(text)` - Adds a title to the request. Displayed in documentation.
54
+ - `description(text)` - Adds a description to the endpoint. Displayed in
55
+ documentation. Markdown supported.
56
+ - `header(name, spec)` - Adds a spec to the request header.
57
+ - `param(name, spec, options = {})` - Adds a spec to the request param.
52
58
  It works for params in the request body, query string and path params.
53
59
  - `response(status, spec)` - Adds a response spec. You can add multiple
54
60
  responses with different formats.
61
+ - `add_example(params:, headers:, response:)` - Adds an example to the
62
+ documentation. [See more details here](#adding-request-examples).
63
+ - `base_url(url)` - Sets the host for this API. For example: "https://api.myapp.com".
64
+ Meant to be used with [request composition](#reusing-requests).
65
+ - `base_path(prefix)` - Sets a prefix for the routes. For example: "/api/v1".
66
+ Meant to be used with [request composition](#reusing-requests).
55
67
 
56
68
  For example:
57
69
 
58
70
  ```ruby
59
71
  class RegistrationsController < ActionController::API
60
- class Request < Explicit::Request
72
+ Request = Explicit::Request.new do
61
73
  post "/api/registrations"
62
74
 
63
75
  description <<-MD
@@ -79,7 +91,7 @@ class RegistrationsController < ActionController::API
79
91
 
80
92
  user = User.create!(name:, email:, payment_type:)
81
93
 
82
- render json: user: { user.as_json(:id, :email) }
94
+ render json: { user: user.as_json(only: %[id email]) }
83
95
  rescue ActiveRecord::RecordNotUnique
84
96
  render json: { error: "email_already_taken" }, status: 422
85
97
  end
@@ -88,7 +100,7 @@ end
88
100
 
89
101
  # Reusing specs
90
102
 
91
- Specs are just data. You can reuse specs the same way you reuse constants or
103
+ Specs are just data. You can share specs the same way you reuse constants or
92
104
  configs in your app. For example:
93
105
 
94
106
  ```ruby
@@ -97,27 +109,48 @@ module MyApp::Spec
97
109
  EMAIL = [:string, format: URI::MailTo::EMAIL_REGEXP, strip: true].freeze
98
110
 
99
111
  ADDRESS = {
100
- country_name: :string,
112
+ country_name: [:string, empty: false],
101
113
  zipcode: [:string, format: /\d{6}-\d{3}/]
102
114
  }.freeze
103
115
  end
104
116
 
105
117
  # ... and then reference the shared specs when needed
106
- class Request < Explicit::Request
118
+ Request = Explicit::Request.new do
107
119
  param :customer_uuid, MyApp::Spec::UUID
108
120
  param :email, MyApp::Spec::EMAIL
109
121
  param :address, MyApp::Spec::ADDRESS
110
122
  end
111
123
  ```
112
124
 
125
+ # Reusing requests
126
+
127
+ Sometimes it is useful to share a group of params, headers or responses between
128
+ requests. You can achieve this by instantiating requests from an existing
129
+ request instead of `Explicit::Request`. For example:
130
+
131
+ ```ruby
132
+ AuthenticatedRequest = Explicit::Request.new do
133
+ header "Authorization", [:string, format: /Bearer [a-zA-Z0-9]{20}/]
134
+
135
+ response 403, { error: "unauthorized" }
136
+ end
137
+
138
+ Request = AuthenticatedRequest.new do
139
+ # Request inherits all definitions from AuthenticatedRequest.
140
+ # Any change you make to params, headers, responses or examples will add to
141
+ # existing definitions.
142
+ end
143
+ ```
144
+
113
145
  # Writing tests
114
146
 
115
147
  Include `Explicit::TestHelper` in your `test/test_helper.rb` or
116
148
  `spec/rails_helper.rb`. This module provides the method
117
- `fetch(request, params:, headers:)` that let's you verify the endpoint works as
149
+ `fetch(request, **options)` that let's you verify the endpoint works as
118
150
  expected and that it responds with a valid response according to the spec.
119
151
 
120
- Add the following line to your `test/test_helper.rb`.
152
+ <details open>
153
+ <summary>For Minitest users, add the following line to your <code>test/test_helper.rb</code></summary>
121
154
 
122
155
  ```diff
123
156
  module ActiveSupport
@@ -132,13 +165,26 @@ module ActiveSupport
132
165
  end
133
166
  ```
134
167
 
135
- To test your controller, call `fetch(request, params:, headers:)` and write
136
- assertions against the response. If the endpoint sends a response that does not
137
- match expected spec the test fails with
138
- `Explicit::Request::InvalidResponseError`.
168
+ </details>
169
+
170
+ <details open>
171
+ <summary>For RSpec users, add the following line to your <code>spec/rails_helper.rb</code></summary>
172
+
173
+ ```diff
174
+ RSpec.configure do |config|
175
+ + config.include Explicit::TestHelper
176
+ end
177
+ ```
178
+
179
+ </details>
180
+
181
+ To test your controller, call `fetch(request, **options)` and write
182
+ assertions against the response. If the response is invalid according to the
183
+ spec the test fails with `Explicit::Request::InvalidResponseError`.
139
184
 
140
185
  The response object has a `status`, an integer value for the http status, and
141
- `data`, a hash with the response data.
186
+ `data`, a hash with the response data. It also provides `dig` for a
187
+ slighly shorter syntax when accessing nested attributes.
142
188
 
143
189
  > Path params are matched by name, so if you have an endpoint configured with
144
190
  > `put "/customers/:customer_id"` you must call as
@@ -147,8 +193,11 @@ The response object has a `status`, an integer value for the http status, and
147
193
  > Note: Response specs are only verified in test environment with no
148
194
  > performance penalty when running in production.
149
195
 
196
+ <details open>
197
+ <summary>Minitest example</summary>
198
+
150
199
  ```ruby
151
- class RegistrationsControllerTest < ActionDispatch::IntegrationTest
200
+ class API::V1::RegistrationsControllerTest < ActionDispatch::IntegrationTest
152
201
  test "successful registration" do
153
202
  response = fetch(RegistrationsController::Request, params: {
154
203
  name: "Bilbo Baggins",
@@ -158,54 +207,73 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
158
207
  })
159
208
 
160
209
  assert_equal 200, response.status
210
+ assert_equal "bilbo@shire.com", response.dig(:user, :email)
211
+ end
212
+ end
213
+ ```
161
214
 
162
- assert response.data[:id]
163
- assert_equal "bilbo@shire.com", response.data[:email]
215
+ </details>
216
+
217
+ <details open>
218
+ <summary>RSpec example</summary>
219
+
220
+ ```ruby
221
+ describe RegistrationsController::Request, type: :request do
222
+ context "when request params are valid" do
223
+ it "successfully registers a new user" do
224
+ response = fetch(described_class, params: {
225
+ name: "Bilbo Baggins",
226
+ email: "bilbo@shire.com",
227
+ payment_type: "free_trial",
228
+ terms_of_use: true
229
+ })
230
+
231
+ expect(response.status).to eql(200)
232
+ expect(response.dig(:user, :email)).to eql("bilbo@shire.com")
233
+ end
164
234
  end
165
235
  end
166
236
  ```
167
237
 
168
- # Writing documentation
238
+ </details>
169
239
 
170
- Documentation is written . To make documentation available to users
171
- you must configure it via `Explicit::Documentation.build` and then publish it
172
- mounting it in `routes.rb`.
240
+ # Publishing documentation
173
241
 
174
- Inside `Explicit::Documentation.build` you have access to the following methods:
242
+ Call `Explicit::Documentation.new` to group, organize and publish the
243
+ documentation for your API. The following methods are available:
175
244
 
176
- - `page_title(text)`
177
- - `primary_color(hexcode)`
178
- - `section(&block)`
179
- - `add(request)`
180
- - `add(title:, partial:)`
245
+ - `page_title(text)` - Sets the web page title.
246
+ - `section(name, &block)` - Adds a section to the navigation menu.
247
+ - `add(request)` - Adds a request to the section
248
+ - `add(title:, partial:)` - Adds a partial to the section
181
249
 
182
250
  For example:
183
251
 
184
252
  ```ruby
185
253
  module MyApp::API::V1
186
- Documentation = Explicit::Documentation.build do
187
- page_title "Acme Co. | API Docs"
188
- primary_color "#6366f1"
254
+ Documentation = Explicit::Documentation.new do
255
+ page_title "Acme API Docs"
189
256
 
190
257
  section "Introduction" do
191
258
  add title: "About", partial: "api/v1/introduction/about"
192
259
  end
193
260
 
194
261
  section "Auth" do
195
- add SessionsController::CreateRequest
196
262
  add RegistrationsController::CreateRequest
263
+ add SessionsController::CreateRequest
264
+ add SessionsController::DestroyRequest
197
265
  end
198
266
 
199
- section "Posts" do
200
- add PostsController::CreateRequest
201
- add PostsController::UpdateRequest
202
- add PostsController::DestroyRequest
267
+ section "Articles" do
268
+ add ArticlesController::CreateRequest
269
+ add ArticlesController::UpdateRequest
270
+ add ArticlesController::DestroyRequest
203
271
  end
204
272
  end
205
273
  end
206
274
  ```
207
275
 
208
- `Explicit::Documentation.build` returns a rails engine that you can mount in
276
+ `Explicit::Documentation.new` returns a rails engine that you can mount in
209
277
  your `config/routes.rb`. For example:
210
278
 
211
279
  ```ruby
@@ -214,6 +282,93 @@ Rails.application.routes.draw do
214
282
  end
215
283
  ```
216
284
 
285
+ ## Adding request examples
286
+
287
+ You can add request examples in two different ways:
288
+
289
+ 1. Manually add an example with `add_example(params:, headers:, response:)`
290
+ 2. Automatically save examples from tests
291
+
292
+ ### 1. Manually adding examples
293
+
294
+ In a request, call `add_example(params:, headers:, response:)` after declaring
295
+ params and responses. It's important the example comes after params and
296
+ responses to make sure it actually follows the spec.
297
+
298
+ For example:
299
+
300
+ ```ruby
301
+ Request = Explicit::Request.new do
302
+ # ... other configs, params and responses
303
+
304
+ add_example(
305
+ params: {
306
+ name: "Bilbo baggins",
307
+ email: "bilbo@shire.com",
308
+ payment_type: "free_trial",
309
+ terms_of_use: true
310
+ }
311
+ response: {
312
+ status: 200,
313
+ data: {
314
+ user: {
315
+ id: 15123,
316
+ email: "bilbo@shire.com"
317
+ }
318
+ }
319
+ }
320
+ )
321
+ end
322
+ ```
323
+
324
+ Request examples are just data, so you can extract and reference them in any
325
+ way you like. For example:
326
+
327
+ ```ruby
328
+ Request = Explicit::Request.new do
329
+ # ... other configs, params and responses
330
+
331
+ add_example MyApp::Examples::REQUEST_1
332
+ add_example MyApp::Examples::REQUEST_2
333
+ end
334
+ ```
335
+
336
+ ### 2. Automatically saving examples from tests
337
+
338
+ The `fetch` method provided by `Explicit::TestHelper` accepts the option
339
+ `add_as_example`. When set to true, the request example is persisted to a local
340
+ file. For example:
341
+
342
+ ```ruby
343
+ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
344
+ test "successful registration" do
345
+ response = fetch(
346
+ RegistrationsController::Request,
347
+ params: {
348
+ name: "Bilbo Baggins",
349
+ email: "bilbo@shire.com",
350
+ payment_type: "free_trial",
351
+ terms_of_use: true
352
+ },
353
+ add_as_example: true # <-- add this line
354
+ )
355
+
356
+ assert_equal 200, response.status
357
+ assert_equal "bilbo@shire.com", response.dig(:user, :email)
358
+ end
359
+ end
360
+ ```
361
+
362
+ Whenever you wish to refresh the examples file run the test suite with the ENV
363
+ `EXPLICIT_PERSIST_EXAMPLES` set. For example
364
+ `EXPLICIT_PERSIST_EXAMPLES=true bin/rails test` or
365
+ `EXPLICIT_PERSIST_EXAMPLES=true bundle exec rspec`. The examples file is located
366
+ at `#{Rails.root}/public/explicit_request_examples.json` by default, but you can
367
+ [change it here](#request-examples-file-path).
368
+
369
+ **Important: be careful not to leak any sensitive data when persisting
370
+ examples from tests**
371
+
217
372
  # Specs
218
373
 
219
374
  ### Agreement
@@ -223,9 +378,9 @@ end
223
378
  [:agreement, parse: true]
224
379
  ```
225
380
 
226
- The `agreement` type is a boolean that must always be true. Useful for terms of
227
- use or agreement acceptances. If `parse: true` is specified then the following
228
- values are accepted alongisde `true`: "`true`", `"on"` and `"1"`.
381
+ A boolean that must always be true. Useful for terms of use or agreement
382
+ acceptances. If `parse: true` is specified then the following
383
+ values are accepted: `true`, `"true"`, `"on"`, `"1"` and `1`.
229
384
 
230
385
  ### Array
231
386
 
@@ -238,6 +393,18 @@ values are accepted alongisde `true`: "`true`", `"on"` and `"1"`.
238
393
  All items in the array must be valid according to the subspec. If at least one
239
394
  value is invalid then the array is invalid.
240
395
 
396
+ ### BigDecimal
397
+
398
+ ```ruby
399
+ :bigdecimal
400
+ [:bigdecimal, min: 0] # inclusive
401
+ [:bigdecimal, max: 100] # inclusive
402
+ ```
403
+
404
+ Value must be an integer or a string like `"0.2"` to avoid rounding errors.
405
+
406
+ [Reference](https://ruby-doc.org/stdlib-3.1.0/libdoc/bigdecimal/rdoc/BigDecimal.html)
407
+
241
408
  ### Boolean
242
409
 
243
410
  ```ruby
@@ -246,8 +413,8 @@ value is invalid then the array is invalid.
246
413
  ```
247
414
 
248
415
  If `parse: true` is specified then the following values are converted to `true`:
249
- `"true"`, `"on"` and `"1"`, and the following values are converted to `false`:
250
- `"false"`, `"off"` and `"0"`.
416
+ `"true"`, `"on"`, `"1"` and `1`, and the following values are converted to
417
+ `false`: `"false"`, `"off"`, `"0"` and `0`.
251
418
 
252
419
  ### Date Time ISO8601
253
420
 
@@ -280,7 +447,20 @@ Provides a default value for the param if the value is not present or it is
280
447
  `nil`. Other falsy values such as empty string or zero have precedence over
281
448
  the default value.
282
449
 
283
- If you provide a lambda it will execute in every `validate!` call.
450
+ If you provide a lambda it will execute every time `Request.validate!` is
451
+ called.
452
+
453
+ ### Description
454
+
455
+ ```ruby
456
+ [:description, markdown_text, subspec]
457
+ [:description, "Customer full name", :string]
458
+ [:description, "Rating score from 0 (bad) to 5 (good)", :integer]
459
+ ```
460
+
461
+ Adds a description to the spec. Descriptions are displayed in documentation
462
+ and do not affect validation in any way with. There is no overhead at runtime.
463
+ Markdown supported.
284
464
 
285
465
  ### Hash
286
466
 
@@ -293,8 +473,8 @@ If you provide a lambda it will execute in every `validate!` call.
293
473
  ```
294
474
 
295
475
  Hashes are key value pairs where all keys must match keyspec and all values must
296
- match valuespec. If you are expecting a hash with a specific set of keys it is
297
- best to use a [record](#record) instead.
476
+ match valuespec. If you are expecting a hash with a specific set of keys use a
477
+ [record](#record) instead.
298
478
 
299
479
  ### Inclusion
300
480
 
@@ -326,11 +506,11 @@ If `parse: true` is specified then integer encoded string values such as "10" or
326
506
  [:literal, value]
327
507
  [:literal, 6379]
328
508
  [:literal, "value"]
329
- "value" # literal strings can use shorter syntax
509
+ "value" # strings work like a literal specs, so you can use this shorter syntax.
330
510
  ```
331
511
 
332
512
  A literal value behaves similar to inclusion with a single value. Useful for
333
- declaring multiple types in `one_of`.
513
+ matching against multiple specs in [`one_of`](#one-of).
334
514
 
335
515
  ### Nilable
336
516
 
@@ -340,7 +520,7 @@ declaring multiple types in `one_of`.
340
520
  [:nilable, [:array, :integer]]
341
521
  ```
342
522
 
343
- Value must be `nil` or valid according to the subspec.
523
+ Value must either match the subspec or be nil.
344
524
 
345
525
  ### One of
346
526
 
@@ -387,3 +567,46 @@ records with array of records, etc.
387
567
  [:string, minlength: 8] # inclusive
388
568
  [:string, maxlength: 20] # inclusive
389
569
  ```
570
+
571
+ # Configuration
572
+
573
+ Add an initializer `config/initializers/explicit.rb` with the following
574
+ code, and then make the desired changes to the config.
575
+
576
+ ```ruby
577
+ Explicit.configure do |config|
578
+ # change config here...
579
+ end
580
+ ```
581
+
582
+ ### Changing examples file path
583
+
584
+ ```ruby
585
+ config.request_examples_file_path = Rails.root.join("public/request_examples.json")
586
+ ```
587
+
588
+ ### Customizing error messages
589
+
590
+ Copy the [default error messages translations](https://github.com/luizpvas/explicit/blob/main/config/locales/en.yml)
591
+ to your project and make the desired changes.
592
+
593
+ ### Customizing error serialization
594
+
595
+ First disable the default response:
596
+
597
+ ```ruby
598
+ config.rescue_from_invalid_params = false
599
+ ```
600
+
601
+ and then add a custom `rescue_from Explicit::Request::InvalidParamsError` to
602
+ your base controller. Use the following code as a starting point:
603
+
604
+ ```ruby
605
+ class ApplicationController < ActionController::API
606
+ rescue_from Explicit::Request::InvalidParamsError do |err|
607
+ params = Explicit::Spec::Error.translate(err.errors)
608
+
609
+ render json: { error: "invalid_params", params: }, status: 422
610
+ end
611
+ end
612
+ ```
@@ -2,9 +2,135 @@
2
2
  <html>
3
3
  <head>
4
4
  <title><%= local_assigns[:page_title] || "API Documentation" %></title>
5
+
6
+ <style>
7
+ html, body {
8
+ font-family: sans-serif;
9
+ font-size: 14px;
10
+ margin: 0;
11
+ padding: 0;
12
+
13
+ --color-neutral-100: #f5f5f5;
14
+ --color-neutral-200: #e5e5e5;
15
+ --color-neutral-300: #d4d4d8;
16
+ --color-neutral-400: #a3a3a3;
17
+ --color-neutral-500: #737373;
18
+ --color-neutral-600: #525252;
19
+ }
20
+
21
+ .container {
22
+ display: flex;
23
+ }
24
+
25
+ .navigation {
26
+ background-color: var(--color-neutral-100);
27
+ border-right: 1px solid var(--color-neutral-300);
28
+ width: 380px;
29
+ height: 100vh;
30
+ }
31
+ .navigation__section {
32
+ }
33
+ .navigation__section__page {
34
+ display: block;
35
+ }
36
+
37
+ .main {
38
+ flex-grow: 1;
39
+ width: 100%;
40
+ height: 100vh;
41
+ overflow: auto;
42
+ }
43
+
44
+ .page {
45
+ padding: 0 2rem;
46
+ margin-bottom: 100px;
47
+ }
48
+ .page:not(:first-of-type) {
49
+ border-top: 1px solid var(--color-neutral-200);
50
+ }
51
+ .page__container {
52
+ display: flex;
53
+ gap: 1rem;
54
+ }
55
+ .page__request {
56
+ width: 50%;
57
+ }
58
+ .page__request__description {
59
+ line-height: 1.6rem;
60
+ }
61
+ .page__response {
62
+ width: 50%;
63
+ }
64
+
65
+ .markdown code {
66
+ background: var(--color-neutral-100);
67
+ color: var(--color-neutral-900);
68
+ }
69
+ .markdown p:first-of-type {
70
+ margin-block-start: 0em;
71
+ }
72
+ .markdown p:last-of-type {
73
+ margin-block-end: 0em;
74
+ }
75
+
76
+ .params {
77
+ margin-top: 1rem;
78
+ border: 1px solid var(--color-neutral-200);
79
+ border-radius: 8px;
80
+ }
81
+ .params__header {
82
+ padding: 5px;
83
+ text-align: center;
84
+
85
+ font-size: 12px;
86
+ text-transform: uppercase;
87
+ color: var(--color-neutral-500);
88
+ border-bottom: 1px solid var(--color-neutral-200);
89
+
90
+ background-color: var(--color-neutral-100);
91
+ border-top-left-radius: 8px;
92
+ border-top-right-radius: 8px;
93
+ }
94
+ .params__param {
95
+ padding: 0.8rem;
96
+ }
97
+ .params__param:not(:last-of-type) {
98
+ border-bottom: 1px solid var(--color-neutral-200);
99
+ }
100
+ .params__param__name {
101
+ font-family: monospace;
102
+ font-weight: bold;
103
+ }
104
+ .params__param__description {
105
+ margin-top: 0.5rem;
106
+ color: var(--color-neutral-500);
107
+ }
108
+ </style>
5
109
  </head>
6
110
 
7
111
  <body>
8
- <h1>Hello world</h1>
112
+ <div class="container">
113
+ <section class="navigation">
114
+ <% sections.each do |section| %>
115
+ <details class="navigation__section" open>
116
+ <summary><%= section.name %></summary>
117
+
118
+ <% section.pages.each do |page| %>
119
+ <%= link_to page.title, "##{page.anchor}", class: "navigation__section__page" %>
120
+ <% end %>
121
+ </details>
122
+ <% end %>
123
+ </section>
124
+
125
+ <main class="main">
126
+ <% sections.each do |section| %>
127
+ <% section.pages.each do |page| %>
128
+ <div class="page">
129
+ <%= render partial: page.partial, locals: { page: } %>
130
+ </div>
131
+ <% end %>
132
+ <% end %>
133
+ </main>
134
+ </div>
9
135
  </body>
10
136
  </html>