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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +142 -0
- data/LICENSE.txt +21 -0
- data/README.md +552 -0
- data/app/controllers/rails_api_docs/docs_controller.rb +20 -0
- data/config/routes.rb +5 -0
- data/lib/generators/rails-api-docs/init/init_generator.rb +167 -0
- data/lib/generators/rails-api-docs/update/update_generator.rb +28 -0
- data/lib/rails-api-docs/config/appender.rb +98 -0
- data/lib/rails-api-docs/config/builder.rb +175 -0
- data/lib/rails-api-docs/config/loader.rb +30 -0
- data/lib/rails-api-docs/configuration.rb +64 -0
- data/lib/rails-api-docs/engine.rb +27 -0
- data/lib/rails-api-docs/inspectors/body_inferrer.rb +88 -0
- data/lib/rails-api-docs/inspectors/controller_inspector.rb +78 -0
- data/lib/rails-api-docs/inspectors/json_route_detector.rb +189 -0
- data/lib/rails-api-docs/inspectors/route_inspector.rb +126 -0
- data/lib/rails-api-docs/inspectors/schema_inspector.rb +36 -0
- data/lib/rails-api-docs/sample_value.rb +28 -0
- data/lib/rails-api-docs/tasks/rails-api-docs.rake +25 -0
- data/lib/rails-api-docs/templates/api_docs.html.erb +695 -0
- data/lib/rails-api-docs/version.rb +5 -0
- data/lib/rails-api-docs.rb +21 -0
- metadata +155 -0
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
|
+

|
|
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
|