grape_openapi3 0.1.5 → 0.1.6
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 +4 -4
- data/README.md +55 -9
- data/lib/grape_openapi3/builders/operation_builder.rb +23 -9
- data/lib/grape_openapi3/document.rb +20 -11
- data/lib/grape_openapi3/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 567bc33ad5a25000b165090cc8defe1d5e819fb6c8a5df0cccbab2dddb27cca4
|
|
4
|
+
data.tar.gz: 74bebc4d7030935208f01fb429362b26041522f7616d3949e02a9d5b8a104765
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c640016dab4cb6e03d32bc9af02c3d3a22ea060ed94a3cbe6bdabbc83e423a6a0df01211460567ea155091b23ae031093ef979f1f878d770df5f560470e6ae0
|
|
7
|
+
data.tar.gz: 67ebcdac076ec212d794ca4aae5f7ecf4ddcdc1640557e9c63652dfe44af345407ae5c7558c4edf61e60d6cb5f49151cbdfe6ef0fdb31473e949f8332b268fd3
|
data/README.md
CHANGED
|
@@ -107,6 +107,24 @@ success: { code: 200, message: "Done." }
|
|
|
107
107
|
success: { code: 201, model: Entities::ProductEntity, message: "Product created." }
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Failure options (typed error responses)
|
|
111
|
+
|
|
112
|
+
Error responses can carry a schema too — point them at a `Grape::Entity` with
|
|
113
|
+
`model:` (or an inline OpenAPI schema with `schema:`). Without either, a failure
|
|
114
|
+
stays a plain description.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
failure: [
|
|
118
|
+
{ code: 401, message: "Unauthorized" }, # description only
|
|
119
|
+
{ code: 404, message: "Not found", model: Entities::ErrorEntity }, # $ref to a schema
|
|
120
|
+
{ code: 422, message: "Validation", model: Entities::ValidationErrorEntity },
|
|
121
|
+
{ code: 409, message: "Conflict", schema: { type: "object", properties: { code: { type: "string" } } } },
|
|
122
|
+
]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This lets an OpenAPI → TypeScript/Zod codegen type your error payloads, not just
|
|
126
|
+
the success body.
|
|
127
|
+
|
|
110
128
|
### Params via params do block
|
|
111
129
|
|
|
112
130
|
```ruby
|
|
@@ -244,24 +262,52 @@ Rails serves `public/` as static files automatically. Navigate to `http://localh
|
|
|
244
262
|
|
|
245
263
|
## Example project
|
|
246
264
|
|
|
247
|
-
The `example/rails_app/` folder
|
|
265
|
+
The `example/rails_app/` folder is a complete, runnable **Rails 8 + Grape** API you
|
|
266
|
+
can use to see everything in action. It models a small real-world domain:
|
|
267
|
+
|
|
268
|
+
- **`Product`** — CRUD resource (`/api/v1/products`)
|
|
269
|
+
- **`Category`** — `Product belongs_to :category`, exposed as a **nested entity**
|
|
270
|
+
- **Typed errors** — `404`/`422` responses carry `ErrorEntity` / `ValidationErrorEntity`
|
|
271
|
+
- **Live docs** — the OpenAPI JSON is served dynamically by a mounted endpoint
|
|
272
|
+
(`V1::OpenapiDoc`), no build step required
|
|
273
|
+
|
|
274
|
+
### Run it
|
|
248
275
|
|
|
249
276
|
```bash
|
|
250
277
|
cd example/rails_app
|
|
251
278
|
bundle install
|
|
252
|
-
rails db:create
|
|
253
|
-
rails server
|
|
279
|
+
bin/rails db:prepare # create + migrate + seed (5 categories, 20 products)
|
|
280
|
+
bin/rails server
|
|
281
|
+
```
|
|
254
282
|
|
|
255
|
-
|
|
256
|
-
# http://localhost:3000/api/v1/products
|
|
283
|
+
### What to open
|
|
257
284
|
|
|
258
|
-
|
|
259
|
-
|
|
285
|
+
| URL | What you get |
|
|
286
|
+
|---|---|
|
|
287
|
+
| `http://localhost:3000/swagger.html` | **Swagger UI** — browse and try every endpoint |
|
|
288
|
+
| `http://localhost:3000/api/v1/openapi` | the **OpenAPI 3.0 JSON**, generated live from the routes |
|
|
289
|
+
| `http://localhost:3000/api/v1/products` | the actual API — note `category` comes back as `{ id, name }` |
|
|
290
|
+
|
|
291
|
+
### Things to notice in the docs
|
|
292
|
+
|
|
293
|
+
- `ProductEntity.category` is a `$ref` to `CategoryEntity` (the `belongs_to`).
|
|
294
|
+
- `POST /api/v1/products` shows the `404`/`422` responses with **typed bodies**.
|
|
295
|
+
- Edit an entity or route, refresh `/swagger.html` — the doc updates immediately
|
|
296
|
+
(it's generated per request; no rake step needed).
|
|
260
297
|
|
|
261
|
-
|
|
262
|
-
|
|
298
|
+
### Static doc (optional)
|
|
299
|
+
|
|
300
|
+
If you'd rather serve a static file (e.g. for hosting), there's also a rake task:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
bundle exec rake openapi:generate # writes public/openapi.json
|
|
304
|
+
OPENAPI_SERVER_URL=https://api.example.com/api/v1 bundle exec rake openapi:generate
|
|
263
305
|
```
|
|
264
306
|
|
|
307
|
+
There's also a dependency-free plain-Grape example at `example/` (run
|
|
308
|
+
`bundle exec ruby example/generate.rb` to produce `example/openapi.json`), which
|
|
309
|
+
additionally showcases nested params and the collision-free schema naming.
|
|
310
|
+
|
|
265
311
|
---
|
|
266
312
|
|
|
267
313
|
## Type reference
|
|
@@ -40,9 +40,8 @@ module GrapeOpenapi3
|
|
|
40
40
|
responses[success_code] = build_success_response
|
|
41
41
|
|
|
42
42
|
@route[:failure_codes].each do |info|
|
|
43
|
-
code
|
|
44
|
-
|
|
45
|
-
responses[code] = { "description" => message }
|
|
43
|
+
code = (info[:code] || info["code"]).to_s
|
|
44
|
+
responses[code] = build_failure_response(info)
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
# Add 401 automatically when the API uses security and the route hasn't
|
|
@@ -83,22 +82,37 @@ module GrapeOpenapi3
|
|
|
83
82
|
|
|
84
83
|
# Unpack hash form: { code: 201, message: "Created", model: ProductEntity, schema: {...} }
|
|
85
84
|
description, entity_class, inline_schema = unpack_success(entity)
|
|
85
|
+
build_response(description, entity_class, inline_schema, is_array: @route[:is_array])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A failure entry may carry a typed body, e.g.
|
|
89
|
+
# failure: [{ code: 422, message: "Validation error", model: ErrorEntity }]
|
|
90
|
+
# failure: [{ code: 404, message: "Not found", schema: { ... } }]
|
|
91
|
+
# Without :model/:schema it stays a bare { "description" => message }.
|
|
92
|
+
def build_failure_response(info)
|
|
93
|
+
message = info[:message] || info["message"] || ""
|
|
94
|
+
model = info[:model] || info["model"]
|
|
95
|
+
inline_schema = info[:schema] || info["schema"]
|
|
96
|
+
is_array = info[:is_array] || info["is_array"] || false
|
|
97
|
+
|
|
98
|
+
build_response(message, (model if model.is_a?(Class)), inline_schema, is_array: is_array)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Shared builder for success and failure responses.
|
|
102
|
+
def build_response(description, entity_class, inline_schema, is_array:)
|
|
103
|
+
media = @route[:produces].first || "application/json"
|
|
86
104
|
|
|
87
105
|
if inline_schema
|
|
88
|
-
media = @route[:produces].first || "application/json"
|
|
89
106
|
return { "description" => description, "content" => { media => { "schema" => inline_schema } } }
|
|
90
107
|
end
|
|
91
108
|
|
|
92
|
-
unless entity_class
|
|
93
|
-
return { "description" => description }
|
|
94
|
-
end
|
|
109
|
+
return { "description" => description } unless entity_class
|
|
95
110
|
|
|
96
111
|
schema_name = @entity_reader.register(entity_class)
|
|
97
112
|
return { "description" => description } unless schema_name
|
|
98
113
|
|
|
99
114
|
ref = { "$ref" => "#/components/schemas/#{schema_name}" }
|
|
100
|
-
schema =
|
|
101
|
-
media = @route[:produces].first || "application/json"
|
|
115
|
+
schema = is_array ? { "type" => "array", "items" => ref } : ref
|
|
102
116
|
|
|
103
117
|
{ "description" => description, "content" => { media => { "schema" => schema } } }
|
|
104
118
|
end
|
|
@@ -22,7 +22,7 @@ module GrapeOpenapi3
|
|
|
22
22
|
.compact
|
|
23
23
|
|
|
24
24
|
# Pre-scan all reachable entities so schema names are stable and collision-free.
|
|
25
|
-
entity_reader.prepare(route_list.flat_map { |rd|
|
|
25
|
+
entity_reader.prepare(route_list.flat_map { |rd| referenced_entity_classes(rd) })
|
|
26
26
|
|
|
27
27
|
paths = build_paths(route_list, entity_reader)
|
|
28
28
|
components = build_components(entity_reader)
|
|
@@ -42,16 +42,25 @@ module GrapeOpenapi3
|
|
|
42
42
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
# Every Grape::Entity class referenced by a route — success AND failure
|
|
46
|
+
# responses — so the pre-scan can assign collision-free names to all of them.
|
|
47
|
+
def referenced_entity_classes(route_data)
|
|
48
|
+
classes = [entity_class_of(route_data[:success_entity])]
|
|
49
|
+
Array(route_data[:failure_codes]).each do |info|
|
|
50
|
+
classes << entity_class_of(info) if info.is_a?(Hash)
|
|
51
|
+
end
|
|
52
|
+
classes.compact
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Extracts a Grape::Entity class from the bare-class form or the hash form
|
|
56
|
+
# ({ model: SomeEntity }). Returns nil when there is none.
|
|
57
|
+
def entity_class_of(entity)
|
|
58
|
+
klass = if entity.is_a?(Class)
|
|
59
|
+
entity
|
|
60
|
+
elsif entity.is_a?(Hash)
|
|
61
|
+
entity[:model] || entity["model"]
|
|
62
|
+
end
|
|
63
|
+
klass if klass.is_a?(Class)
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
def build_paths(route_list, entity_reader)
|