rails-api-docs 0.1.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.

Potentially problematic release.


This version of rails-api-docs might be problematic. Click here for more details.

data/README.md ADDED
@@ -0,0 +1,552 @@
1
+ # rails-api-docs
2
+
3
+ Generate beautiful API documentation for your Rails app — two outputs from
4
+ one editable YAML:
5
+
6
+ - **Live preview in development.** Visit `http://localhost:3000/rails/api-docs`
7
+ and the page re-renders from `config/rails-api-docs.yml` on every refresh.
8
+ Edit the YAML in one window, hit F5 in the other — no build step while
9
+ iterating.
10
+
11
+ - **Self-contained HTML for production.** `rake rails-api-docs:build` writes
12
+ `public/api-docs.html`: a single file with all CSS and JS inlined. Drop it
13
+ on any static host — no asset pipeline, no JavaScript runtime.
14
+
15
+ The gem inspects your routes, controllers (via Prism AST), and
16
+ `ActiveRecord` schema to scaffold the YAML on first run; you edit it from
17
+ there to add descriptions, examples, and constraints.
18
+
19
+ ![Rails API Docs](docs/rails-api-docs-1.png)
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ Requires Ruby ≥ 3.1 and Rails ≥ 7.1.
26
+
27
+ ```ruby
28
+ # Gemfile
29
+ group :development do
30
+ gem "rails-api-docs"
31
+ end
32
+ ```
33
+
34
+ ```bash
35
+ bundle install
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ ```bash
41
+ # 1. Scaffold the YAML config from your current routes
42
+ # (use --api-only to include only routes that return JSON)
43
+ # (use --verbose-yaml to emit every possible key with defaults)
44
+ rails g rails-api-docs:init
45
+
46
+ # 2. Run the server
47
+ rails server
48
+ ```
49
+
50
+ In development access live view at `/rails/api-docs `
51
+
52
+ For production build the the static HTML
53
+
54
+ ```bash
55
+ rake rails-api-docs:build
56
+ ```
57
+
58
+ ## The workflow
59
+
60
+ The flow is designed around **two artifacts** that you commit:
61
+
62
+ | File | Role | Edited by |
63
+ | --------------------------- | ---------------------------------------- | --------------------------- |
64
+ | `config/rails-api-docs.yml` | source of truth — descriptions, examples | **you** |
65
+ | `public/api-docs.html` | rendered output | `rake rails-api-docs:build` |
66
+
67
+ The YAML is the only file you maintain by hand. The HTML is regenerated
68
+ from it (and from your routes, when you re-run the generator).
69
+
70
+ ### Updating the YAML
71
+
72
+ After adding new routes to your Rails app, run:
73
+
74
+ ```bash
75
+ rails g rails-api-docs:update
76
+ ```
77
+
78
+ It scans your routes and **appends only what's new** to
79
+ `config/rails-api-docs.yml`. Existing entries are never modified, so the
80
+ descriptions, examples, body fields and section names you've edited
81
+ survive every re-run.
82
+
83
+ To regenerate a single route with fresh inferred defaults, delete it
84
+ from the YAML and run `:update` again.
85
+
86
+ All flags (`--api-only`, `--only-controllers`, etc.) are accepted.
87
+
88
+ > ⚠️ **Caveat about comments inside the YAML:** the gem re-emits the
89
+ > entire YAML file in append-only mode using `YAML.dump`, which preserves
90
+ > values but not free-form comments _inside_ the file. The leading
91
+ > `#`-comment block at the top of the file (your notes + the gem header)
92
+ > **is** preserved.
93
+
94
+ ## YAML structure
95
+
96
+ Only `method` and `path` are required on each endpoint. Everything else
97
+ is optional and has sensible defaults. Endpoint identity for the
98
+ append-only merge is `"#{method} #{path}"`.
99
+
100
+ ### Complete reference
101
+
102
+ ```yaml
103
+ general_configurations:
104
+ title: "My App API"
105
+ base_url: "https://api.example.com"
106
+ primary_color: "#CC0000"
107
+ secondary_color: "#2E2E2E"
108
+ accent_color: "#D30001"
109
+ font_family: "system-ui, -apple-system, sans-serif"
110
+ show_curl: true
111
+ show_examples: true
112
+
113
+ sections:
114
+ users: # controller path (stable key)
115
+ name: "Users"
116
+ description: "User account endpoints"
117
+ show: true
118
+ endpoints:
119
+ - method: POST # required
120
+ path: /users # required
121
+ name: "Create User"
122
+ description: "Register a new user account."
123
+ show: true
124
+
125
+ # ── endpoint metadata ──
126
+ deprecated: false
127
+ auth: bearer # bearer | basic | none | <custom>
128
+ tags: [public, auth]
129
+
130
+ # ── request side ──
131
+ headers:
132
+ - name: Authorization
133
+ type: string
134
+ required: true
135
+ description: "Bearer token"
136
+ example: "Bearer eyJhbGciOi…"
137
+ - name: X-Idempotency-Key
138
+ type: string
139
+ required: false
140
+
141
+ params:
142
+ - name: id
143
+ type: integer
144
+ in: path # path | query
145
+ required: true
146
+ example: 42
147
+
148
+ body:
149
+ - name: email
150
+ type: string
151
+ required: true
152
+ format: email # email | uuid | uri | date-time | …
153
+ example: "marc@example.com"
154
+ description: "Unique email address."
155
+ - name: password
156
+ type: string
157
+ required: true
158
+ write_only: true
159
+ min_length: 8
160
+ max_length: 72
161
+ - name: role
162
+ type: string
163
+ enum: [user, admin, guest]
164
+ default: user
165
+ - name: age
166
+ type: integer
167
+ nullable: true
168
+ min: 0
169
+ max: 150
170
+ - name: zip
171
+ type: string
172
+ pattern: "^\\d{5}$"
173
+
174
+ request_example: |
175
+ { "user": { "email": "marc@example.com", "password": "i-love-rails" } }
176
+
177
+ # ── response side ──
178
+ responses:
179
+ "201":
180
+ description: "User created"
181
+ headers:
182
+ - name: Location
183
+ type: string
184
+ example: "https://api.example.com/users/42"
185
+ schema:
186
+ - { name: id, type: integer, read_only: true }
187
+ - { name: email, type: string, format: email }
188
+ - {
189
+ name: token,
190
+ type: string,
191
+ read_only: true,
192
+ description: "JWT, 7-day TTL",
193
+ }
194
+ example: |
195
+ { "id": 42, "email": "marc@example.com", "token": "eyJ..." }
196
+ "422":
197
+ description: "Validation failed"
198
+ example: '{ "errors": { "password": ["too short"] } }'
199
+ ```
200
+
201
+ ### Top-level keys
202
+
203
+ | Key | Type | Default | Purpose |
204
+ | ------------------------ | ---- | ------- | ------------------------------------------------------------- |
205
+ | `general_configurations` | hash | `{}` | Global config — title, base URL, theme colors, render toggles |
206
+ | `sections` | hash | `{}` | All documented routes, keyed by controller path |
207
+
208
+ ### `general_configurations` keys
209
+
210
+ | Key | Type | Default | Purpose |
211
+ | ----------------- | ---------- | --------------------------- | ------------------------------------------------------------ |
212
+ | `title` | string | `"API Documentation"` | Top-bar text + browser tab title |
213
+ | `base_url` | string | `"https://api.example.com"` | URL prefix used in the generated cURL command |
214
+ | `primary_color` | CSS color | `"#CC0000"` | Maps to CSS `--primary` — sidebar active state, brand circle |
215
+ | `secondary_color` | CSS color | `"#2E2E2E"` | Maps to CSS `--secondary` — dark UI accents |
216
+ | `accent_color` | CSS color | `"#D30001"` | Maps to CSS `--accent` — reserved for future use |
217
+ | `font_family` | CSS string | system stack | Body font of the rendered page |
218
+ | `show_curl` | bool | `true` | Whether to render the cURL block in column 3 |
219
+ | `show_examples` | bool | `true` | Whether to render the response example block in column 3 |
220
+
221
+ ### Section keys
222
+
223
+ A section's key (`users`, `api/v1/posts`) is the **controller path** — the same string Rails uses internally. It's the stable identifier for the append-only merge, so don't rename it.
224
+
225
+ | Key | Type | Default | Purpose |
226
+ | ------------- | ------ | -------------------------- | ---------------------------------------------------- |
227
+ | `name` | string | controller name, humanized | Display label in the sidebar |
228
+ | `description` | string | `""` | Currently unused by the renderer (reserved) |
229
+ | `show` | bool | `true` | Set `false` to hide the entire section from the docs |
230
+ | `endpoints` | array | `[]` | List of endpoint hashes (see below) |
231
+
232
+ ### Endpoint keys
233
+
234
+ | Key | Type | Default | Purpose |
235
+ | ----------------- | --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
236
+ | `method` | string | **required** | HTTP verb (`GET`, `POST`, `PATCH`, …) |
237
+ | `path` | string | **required** | URL path (`/users/:id`). Combined with `method` for identity |
238
+ | `name` | string | inferred from action | Display label (sidebar + page heading) |
239
+ | `description` | string | `""` | Lead paragraph under the heading |
240
+ | `show` | bool | `true` | Set `false` to hide this endpoint individually |
241
+ | `deprecated` | bool | `false` | Red "Deprecated" badge + strikethrough on the title |
242
+ | `auth` | string | none | `bearer` / `basic` / `none` / custom. Adds a dark badge in the header. Auto-injects an `Authorization` placeholder into cURL when not declared in `headers:` |
243
+ | `tags` | array | `[]` | Clickable chips next to the method pill — click to filter the sidebar to endpoints with that tag. See [Tag filtering](#tag-filtering) |
244
+ | `headers` | array of fields | `[]` | Request headers — rendered as a "Headers" section AND injected into cURL |
245
+ | `params` | array of fields | `[]` | Path & query params (`in: path` / `in: query`) |
246
+ | `body` | array of fields | `[]` | Request body fields |
247
+ | `request_example` | string | inferred | Raw body shown in `curl --data`. Wins over the auto-generated sample |
248
+ | `responses` | hash | `{}` | Per-status responses, keyed by HTTP status code (string keys: `"201"`, `"422"`) |
249
+
250
+ ### Tag filtering
251
+
252
+ Tags double as a **category filter** in the rendered docs. Add them under
253
+ any endpoint:
254
+
255
+ ```yaml
256
+ endpoints:
257
+ - method: POST
258
+ path: /users
259
+ tags: [public, auth]
260
+ - method: POST
261
+ path: /admin/users
262
+ tags: [admin, auth]
263
+ - method: GET
264
+ path: /health
265
+ tags: [public]
266
+ ```
267
+
268
+ How they behave in the live preview AND the built `public/api-docs.html`
269
+ (same JS runs in both):
270
+
271
+ - **Display.** Each tag is a small pill next to the method pill in the
272
+ endpoint header.
273
+ - **Click to filter.** Click any tag → the sidebar collapses to only
274
+ endpoints carrying that tag. The clicked pill highlights in the
275
+ primary color across every endpoint page.
276
+ - **Toggle.** Click the same tag again → filter clears.
277
+ - **Switch.** Click a different tag → switches to that one (single-tag
278
+ exclusive; no multi-select).
279
+ - **Combine with search.** Tag filter and the search box compose with
280
+ AND — only items matching both stay visible.
281
+ - **Clear indicator.** When a tag is active, a pill appears between the
282
+ search box and the section list: `Filtered by: auth ×`. Click the ×
283
+ (or the active tag again) to clear.
284
+
285
+ Anything works in `tags` — strings with spaces, special characters,
286
+ versions like `v1`/`beta`. They're JSON-encoded internally on the
287
+ sidebar `<li>` so quoting never breaks.
288
+
289
+ Accessibility: tags render as real `<button>` elements (Tab-navigable,
290
+ Enter/Space activates) with `aria-pressed` reflecting the toggle state.
291
+ The active-filter pill uses `aria-live="polite"` so screen readers
292
+ announce changes.
293
+
294
+ ### Field keys
295
+
296
+ Used uniformly by `body`, `params`, `headers`, and `responses["XXX"].schema`. Only `name` is required.
297
+
298
+ | Key | Type | Default | Purpose |
299
+ | ------------- | ------- | ------------ | --------------------------------------------------------------------------- |
300
+ | `name` | string | **required** | Field name |
301
+ | `type` | string | `string` | Logical type (`string`, `integer`, `boolean`, `date`, `array`, `object`, …) |
302
+ | `required` | bool | `false` | Adds yellow "Required" badge |
303
+ | `description` | string | none | Subtle line under the field row |
304
+ | `example` | any | none | "Example: `value`" line under the description |
305
+ | `format` | string | none | Green badge (`email`, `uuid`, `uri`, `date-time`, `ipv4`, …) |
306
+ | `enum` | array | none | Renders "one of: `a` · `b` · `c`" |
307
+ | `default` | any | none | Meta badge — even `false`/`0` are shown |
308
+ | `min` | numeric | none | Meta badge `min: N` (numeric values) |
309
+ | `max` | numeric | none | Meta badge `max: N` |
310
+ | `min_length` | integer | none | Meta badge `min: N` (string length) |
311
+ | `max_length` | integer | none | Meta badge `max: N` (string length) |
312
+ | `pattern` | string | none | Monospace `pattern: …` meta |
313
+ | `read_only` | bool | `false` | Blue badge |
314
+ | `write_only` | bool | `false` | Red badge |
315
+ | `nullable` | bool | `false` | Italic gray badge |
316
+ | `in` | string | none | Indigo badge — typically `path` or `query`, used on `params` entries |
317
+
318
+ ### Response keys
319
+
320
+ Each entry in `responses` is keyed by HTTP status code (as a **string**, quote the YAML key) and accepts:
321
+
322
+ | Key | Type | Default | Purpose |
323
+ | ------------- | --------------- | -------------------- | -------------------------------------------------------------------------- |
324
+ | `description` | string | `""` | Shown next to the status code in the central column |
325
+ | `example` | string | inferred from `body` | Raw body shown in the response tab (right column). Used by the copy button |
326
+ | `headers` | array of fields | `[]` | Response headers — rendered as field rows under a "Headers" subhead |
327
+ | `schema` | array of fields | `[]` | Typed response fields — rendered as field rows under a "Schema" subhead |
328
+
329
+ ### What gets inferred automatically
330
+
331
+ | Source | Filled into the YAML |
332
+ | ---------------------------- | -------------------------------------------------------------------------------- |
333
+ | `Rails.application.routes` | section keys, method, path, action-based endpoint name |
334
+ | Path itself (`:id`, `:slug`) | `params: [{ in: path, type, required: true }]` — type heuristic: `_id` → integer |
335
+ | Controller (Prism AST) | `body:` field names from `params.permit(:a, :b, …)` patterns |
336
+ | ActiveRecord `columns_hash` | `type` and `required` (NOT NULL + no default) for each inferred body field |
337
+
338
+ Every body field, path param, and response stub is also seeded with
339
+ `description: ""` and `example: <type-sample>` so you can customize values
340
+ without having to remember key names. The `example` value flows into the
341
+ generated cURL command and the default response block — change it once,
342
+ both reflect.
343
+
344
+ Advanced field keys (`format`, `enum`, `default`, `min`/`max`,
345
+ `min_length`/`max_length`, `pattern`, `read_only`/`write_only`,
346
+ `nullable`) and endpoint-level keys (`deprecated`, `auth`, `tags`,
347
+ `headers`, `request_example`) stay opt-in to keep the YAML lean. Pass
348
+ `--verbose-yaml` to emit them all with defaults for full discoverability:
349
+
350
+ ```bash
351
+ rails g rails-api-docs:init --verbose-yaml
352
+ rails g rails-api-docs:update --verbose-yaml
353
+ ```
354
+
355
+ Persist as the default:
356
+
357
+ ```ruby
358
+ RailsApiDocs.configure { |c| c.verbose_yaml = true }
359
+ ```
360
+
361
+ Combines freely with other flags — the two operate on different pipeline
362
+ stages (`--api-only` filters which routes are included, `--verbose-yaml`
363
+ controls how each included route is rendered):
364
+
365
+ ```bash
366
+ rails g rails-api-docs:init --api-only --verbose-yaml
367
+ ```
368
+
369
+ ## Customization
370
+
371
+ ### Colors and labels
372
+
373
+ Edit `general_configurations`. The colors are wired straight into CSS
374
+ variables (`--primary`, `--secondary`, `--accent`) at render time, so a
375
+ single value change re-themes the whole page.
376
+
377
+ ### Filtering routes
378
+
379
+ Add an initializer in your Rails app:
380
+
381
+ ```ruby
382
+ # config/initializers/rails_api_docs.rb
383
+ RailsApiDocs.configure do |c|
384
+ c.ignored_path_prefixes = ["/admin", "/internal"]
385
+ c.ignored_controllers = [/^devise\//, "health"]
386
+ c.ignored_actions = %w[new edit] # typical for JSON APIs
387
+ end
388
+ ```
389
+
390
+ These are applied at `rails g rails-api-docs:init` time (and at the
391
+ dev `/rails/api-docs` mount). Built-in Rails internals
392
+ (`/rails/info`, `/rails/active_storage`, etc.) and routes without a
393
+ controller (engine mounts, lambdas) are always skipped.
394
+
395
+ `ignored_controllers` and `only_controllers` (below) share the same
396
+ matching rule:
397
+
398
+ - **Regexp** → standard regex match.
399
+ - **String with `/`** → exact path match. `"api/v1/users"` only matches
400
+ that exact controller.
401
+ - **String without `/`** → boundary-aware suffix match. `"users"` matches
402
+ both `users` and `api/v1/users`, but NOT `super_users` or `users_admin`.
403
+
404
+ ### Scaffolding only specific controllers (`--only-controllers`)
405
+
406
+ Whitelist by controller name. Four CLI forms are accepted — pick whichever
407
+ feels natural:
408
+
409
+ ```bash
410
+ # Space-separated (Thor native — usually the most ergonomic)
411
+ rails g rails-api-docs:init --only-controllers users posts comments
412
+
413
+ # Comma-separated
414
+ rails g rails-api-docs:init --only-controllers=users,posts,comments
415
+
416
+ # Bracketed
417
+ rails g rails-api-docs:init --only-controllers=[users,posts,comments]
418
+
419
+ # Namespaced exact-path
420
+ rails g rails-api-docs:init --only-controllers api/v1/users api/v1/posts
421
+ ```
422
+
423
+ Bare names match across namespaces (e.g. `users` keeps both `users` and
424
+ `api/v1/users`); slash-qualified names are exact.
425
+
426
+ When both `only_controllers` and `ignored_controllers` apply to the same
427
+ controller, the **blacklist wins** — natural read: "include only these,
428
+ except the ones I explicitly excluded".
429
+
430
+ Combine with `--api-only` for the intersection:
431
+
432
+ ```bash
433
+ rails g rails-api-docs:init --only-controllers users orders --api-only
434
+ ```
435
+
436
+ Persist as a default:
437
+
438
+ ```ruby
439
+ RailsApiDocs.configure do |c|
440
+ c.only_controllers = %w[users posts api/v1/widgets]
441
+ # or mix strings and regexps
442
+ c.only_controllers = [/^api\/v1\//, "users"]
443
+ end
444
+ ```
445
+
446
+ The CLI flag wins when both are set. Repeating the flag (e.g.
447
+ `--only-controllers users --only-controllers posts`) **does not** merge —
448
+ Thor takes the last one. Always pass all names in a single flag.
449
+
450
+ ### Scaffolding only JSON-returning routes (`--api-only`)
451
+
452
+ ```bash
453
+ rails g rails-api-docs:init --api-only
454
+ ```
455
+
456
+ Includes a route in the YAML only if the gem can prove it returns JSON:
457
+
458
+ 1. **Controller-level:** the controller's inheritance chain (followed via
459
+ Prism AST through `ApplicationController` / any custom base) reaches
460
+ `ActionController::API`. Whole controller is considered JSON.
461
+ 2. **Action-level:** the specific action body contains a literal
462
+ `render json: …` (kwarg or hash-rocket form), including inside
463
+ `respond_to { |f| f.json { render json: … } }` blocks.
464
+
465
+ Anything we can't statically classify as JSON is **excluded** (strict).
466
+ That keeps the YAML clean even when your app has Devise, RailsAdmin, or
467
+ HTML controllers mixed in.
468
+
469
+ Persist it as the default in an initializer:
470
+
471
+ ```ruby
472
+ RailsApiDocs.configure { |c| c.api_only = true }
473
+ ```
474
+
475
+ The CLI flag wins when both are set — pass `--no-api-only` to override
476
+ the config for a single run.
477
+
478
+ > Append-only doctrine still applies: filtering happens at scaffold time,
479
+ > not at render time. Re-running with `--api-only` after you've already
480
+ > got HTML routes in the YAML won't remove them — delete them by hand if
481
+ > needed.
482
+ >
483
+ > **Known gaps:** `render json:` called from a helper method (we don't
484
+ > follow method calls), `render template: "x", formats: :json` (no
485
+ > `json:` key), and includes (`include SomeRenderingModule`) are not
486
+ > traversed.
487
+
488
+ ### Disabling the dev mount
489
+
490
+ ```ruby
491
+ RailsApiDocs.configure { |c| c.mount_in_development = false }
492
+ ```
493
+
494
+ ### Custom paths
495
+
496
+ ```ruby
497
+ RailsApiDocs.configure do |c|
498
+ c.config_path = "doc/api.yml"
499
+ c.output_path = "public/docs/index.html"
500
+ c.mount_path = "/internal/api-docs"
501
+ end
502
+ ```
503
+
504
+ Or per-build via env vars:
505
+
506
+ ```bash
507
+ rake rails-api-docs:build CONFIG=doc/api.yml OUTPUT=public/docs/index.html
508
+ ```
509
+
510
+ ## The dev mount: `/rails/api-docs`
511
+
512
+ When `Rails.env.development?` the engine appends a `mount` for itself at
513
+ `/rails/api-docs` (configurable). Visiting that URL renders the HTML
514
+ **directly from the current YAML on disk** — no build step. Edit the
515
+ YAML, refresh the browser, see the change.
516
+
517
+ If the YAML doesn't exist yet, the page tells you to run
518
+ `rails g rails-api-docs:init`.
519
+
520
+ In `production` the mount is not registered. Production should serve the
521
+ prebuilt `public/api-docs.html` as a static asset.
522
+
523
+ ## Development
524
+
525
+ ```bash
526
+ bundle install
527
+ bundle exec rake test # 91 tests, ~0.1s
528
+ ```
529
+
530
+ The test suite uses minitest + a real `ActionDispatch::Routing::RouteSet`
531
+ to exercise the inspectors and renderers in isolation — no dummy Rails
532
+ app needed.
533
+
534
+ For end-to-end verification against a real Rails host app, see the
535
+ smoke scripts in `/tmp/rad_smoke_test.sh` (sets up a Rails app, mounts
536
+ the gem, hits `/rails/api-docs`).
537
+
538
+ ## Limitations
539
+
540
+ - **Inline YAML comments aren't preserved** across `update` re-runs
541
+ (header-block comments at the top of the file are).
542
+ - **Nested permits** (`permit(items: [:name])`) — only the top-level keys
543
+ are inferred. Nested arrays/hashes need to be edited in by hand.
544
+ - **Splat permits** (`permit(*USER_FIELDS)`) — not resolved; you'll get
545
+ an empty `body:` and can fill it in by hand.
546
+ - **Custom inflections** — section names use `String#singularize` /
547
+ `#pluralize` from ActiveSupport, so unusual model names may need a
548
+ manual rename in the YAML.
549
+
550
+ ## License
551
+
552
+ MIT — see `LICENSE.txt`.
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiDocs
4
+ class DocsController < ActionController::Base
5
+ # We render the full HTML doc ourselves (self-contained <html>),
6
+ # so disable the host app's layout.
7
+ layout false
8
+
9
+ def show
10
+ status, html = Doc::Responder.new(config_path: full_config_path).render
11
+ render html: html.html_safe, status: status
12
+ end
13
+
14
+ private
15
+
16
+ def full_config_path
17
+ Rails.root.join(RailsApiDocs.configuration.config_path).to_s
18
+ end
19
+ end
20
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ RailsApiDocs::Engine.routes.draw do
4
+ root to: "docs#show", via: :get
5
+ end