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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c028381878b52f6a689b843e5e0b4aa37799bc26fc612316b51a6617783f4afa
4
- data.tar.gz: b72d1e69efff1e6575cb72c2d1fae4c2a09d5a908f5e809a7c3b8303f681c4b8
3
+ metadata.gz: 567bc33ad5a25000b165090cc8defe1d5e819fb6c8a5df0cccbab2dddb27cca4
4
+ data.tar.gz: 74bebc4d7030935208f01fb429362b26041522f7616d3949e02a9d5b8a104765
5
5
  SHA512:
6
- metadata.gz: 7908a7ac38deeeda95246ee30026f82b9f3c872ff26c4c8863de6b2f9a6157963d5230235bc616593b761603ce3291ee4608711417e88085ff5430f99e4f6c17
7
- data.tar.gz: '089da884741c789d424e8e11f55814b20ef2025567ba08b9c9f4a7231e4980fd306d5e6d621b8fcec1490faa0bcedc278b4b1325a1696a627966ae650d44316a'
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 contains a full Rails 8 + Grape API with a products CRUD — the same setup described in this README, ready to run:
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 db:migrate db:seed
253
- rails server
279
+ bin/rails db:prepare # create + migrate + seed (5 categories, 20 products)
280
+ bin/rails server
281
+ ```
254
282
 
255
- # API live at:
256
- # http://localhost:3000/api/v1/products
283
+ ### What to open
257
284
 
258
- # Generate the docs:
259
- bundle exec rake openapi:generate
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
- # View Swagger UI:
262
- # http://localhost:3000/swagger.html
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 = (info[:code] || info["code"]).to_s
44
- message = info[:message] || info["message"] || ""
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 = @route[:is_array] ? { "type" => "array", "items" => ref } : ref
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| success_entity_classes(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
- # The Grape::Entity class(es) referenced by a route's success response, if any.
46
- # Handles both the bare-class form and the hash form ({ model: SomeEntity }).
47
- def success_entity_classes(route_data)
48
- entity = route_data[:success_entity]
49
- klass = if entity.is_a?(Class)
50
- entity
51
- elsif entity.is_a?(Hash)
52
- entity[:model] || entity["model"]
53
- end
54
- klass.is_a?(Class) ? [klass] : []
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GrapeOpenapi3
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape_openapi3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - rodrigonbarreto