rails_ninja 0.3.0 → 0.4.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 +4 -4
- data/README.md +67 -0
- data/lib/rails_ninja/api.rb +16 -0
- data/lib/rails_ninja/endpoint_group.rb +2 -1
- data/lib/rails_ninja/openapi/generator.rb +29 -9
- data/lib/rails_ninja/openapi/schema_ref.rb +2 -2
- data/lib/rails_ninja/operation.rb +29 -4
- data/lib/rails_ninja/schema/field.rb +10 -3
- data/lib/rails_ninja/schema.rb +12 -2
- data/lib/rails_ninja/version.rb +1 -1
- data/lib/rails_ninja.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ea5b2dadb986f6d8ccde0b94cef6e57a941cd5336a776182e3462afc15dcdd7
|
|
4
|
+
data.tar.gz: 344e6aa870b08bb554fc239cc0f5f583721c8fd73d2d0532a9206d01cbe47cf0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22c2dcadc5aba38d8847721739a8562b7266a54ef6ad286b6cd4e4d35508c5127c0731bac09331e9fd9b7e460c25e95e002ba4bd8f42470f4b978f34db06f2bb
|
|
7
|
+
data.tar.gz: 54889989cb0f46f6e0fb2e5745aa02786361a604cc2a5006d44737dbbfe4f677d12f4964d7106a2e748530a67478224e51567b627e6ca90f43ffd1c7e5f92f9c
|
data/README.md
CHANGED
|
@@ -264,6 +264,73 @@ Deprecated paths remain routable and are marked as deprecated in OpenAPI.
|
|
|
264
264
|
Set `server "https://api.example.com"` on an API to declare its server URL, or
|
|
265
265
|
`docs false` to disable `/docs` and `/openapi.json`.
|
|
266
266
|
|
|
267
|
+
## OpenAPI documentation in Ruby
|
|
268
|
+
|
|
269
|
+
Declare descriptions, examples and parameter constraints next to the API schemas.
|
|
270
|
+
The generator includes them in the JSON document without a separate overlay file.
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
class InventoryApi < RailsNinja::API
|
|
274
|
+
title "Inventory API"
|
|
275
|
+
description "Read inventory records."
|
|
276
|
+
openapi_tag "Records", description: "Inventory records and their availability."
|
|
277
|
+
|
|
278
|
+
schema :Record, description: "One inventory record", example: { name: "Widget" } do
|
|
279
|
+
field :name, RailsNinja::Types::String,
|
|
280
|
+
description: "Display name", example: "Widget", openapi: { minLength: 1 }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
schema :ReadError do
|
|
284
|
+
field :error, RailsNinja::Types::String, description: "Reason the read failed"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
get "/records/:id",
|
|
288
|
+
operation_id: "getRecord",
|
|
289
|
+
summary: "Read an inventory record",
|
|
290
|
+
description: "Returns the record identified by the supplied ID.",
|
|
291
|
+
tags: ["Records"],
|
|
292
|
+
path_params: { id: { description: "Record ID", example: "123", schema: { pattern: "^[0-9]+$" } } },
|
|
293
|
+
responses: {
|
|
294
|
+
200 => Record,
|
|
295
|
+
429 => {
|
|
296
|
+
schema: ReadError,
|
|
297
|
+
description: "Wait before retrying the request.",
|
|
298
|
+
example: { error: "Too many requests" },
|
|
299
|
+
headers: { "Retry-After" => { description: "Seconds to wait", schema: { type: "integer" }, example: 1 } }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
def read
|
|
303
|
+
# Fetch the record or render an error response.
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Schema classes can also declare `openapi description: ..., example: ...` inside
|
|
309
|
+
their class body. Field `openapi:` accepts `format`, `pattern`, `minLength`,
|
|
310
|
+
`maxLength`, `minimum`, `maximum`, `default`, `enum` and `oneOf`. These options
|
|
311
|
+
describe constraints enforced by the application; they do not add runtime
|
|
312
|
+
validation or change serialization. Field types and requiredness still come from
|
|
313
|
+
the existing `field` declaration.
|
|
314
|
+
|
|
315
|
+
GET, DELETE and HEAD request schemas supply query parameters. Their field
|
|
316
|
+
descriptions and examples appear on the parameter, and their constraints appear
|
|
317
|
+
in its schema. Header declarations accept `description:`, `example:` and
|
|
318
|
+
`schema:` alongside `name:` and `required:`. Path annotations refer to the full
|
|
319
|
+
mounted route, including parameters from a mount prefix. Path parameters remain
|
|
320
|
+
required and retain their route names.
|
|
321
|
+
|
|
322
|
+
For a request body, use `request_description:` on the endpoint. For each response,
|
|
323
|
+
either keep the existing schema shorthand or supply a hash containing `schema:`
|
|
324
|
+
and optional `description:`, `headers:` and `example:` or `examples:`. The two
|
|
325
|
+
example forms are mutually exclusive. Named examples use OpenAPI Example Objects,
|
|
326
|
+
for example `examples: { missing: { value: { error: "Not found" } } }`.
|
|
327
|
+
Schemas can still be arrays or `one_of(...)` declarations.
|
|
328
|
+
|
|
329
|
+
An explicit `operation_id:` keeps generated client method names stable when Ruby
|
|
330
|
+
handler names change. Authors must choose unique IDs within each API document;
|
|
331
|
+
deprecated route aliases omit the ID. API security descriptions belong in the
|
|
332
|
+
existing `openapi_security_scheme` declaration.
|
|
333
|
+
|
|
267
334
|
## Static OpenAPI files
|
|
268
335
|
|
|
269
336
|
Generate an OpenAPI 3.2 JSON file for every API:
|
data/lib/rails_ninja/api.rb
CHANGED
|
@@ -23,6 +23,20 @@ module RailsNinja
|
|
|
23
23
|
@_title
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def description(value = nil)
|
|
27
|
+
@_description = value unless value.nil?
|
|
28
|
+
@_description
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def openapi_tag(name, description:)
|
|
32
|
+
_openapi_tags.reject! { |tag| tag[:name] == name.to_s }
|
|
33
|
+
_openapi_tags << { name: name.to_s, description: description }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def _openapi_tags
|
|
37
|
+
@_openapi_tags ||= []
|
|
38
|
+
end
|
|
39
|
+
|
|
26
40
|
def version(value = nil)
|
|
27
41
|
if value
|
|
28
42
|
@_version = value
|
|
@@ -85,6 +99,8 @@ module RailsNinja
|
|
|
85
99
|
super
|
|
86
100
|
subclass.instance_variable_set(:@_openapi_security_schemes, _openapi_security_schemes.transform_values(&:dup))
|
|
87
101
|
subclass.instance_variable_set(:@_openapi_security, _openapi_security.dup)
|
|
102
|
+
subclass.instance_variable_set(:@_openapi_tags, _openapi_tags.deep_dup)
|
|
103
|
+
subclass.instance_variable_set(:@_description, description&.dup)
|
|
88
104
|
RailsNinja.registered_apis << subclass
|
|
89
105
|
end
|
|
90
106
|
|
|
@@ -22,8 +22,9 @@ module RailsNinja
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Schema definition DSL
|
|
25
|
-
def schema(name, &block)
|
|
25
|
+
def schema(name, **documentation, &block)
|
|
26
26
|
klass = Class.new(Schema::Base)
|
|
27
|
+
klass.openapi(**documentation)
|
|
27
28
|
klass.class_eval(&block)
|
|
28
29
|
|
|
29
30
|
# Register as a constant on this class so it's accessible by name
|
|
@@ -33,10 +33,12 @@ module RailsNinja
|
|
|
33
33
|
components: components,
|
|
34
34
|
}
|
|
35
35
|
spec[:servers] = [{ url: @api_class._server }] if @api_class._server
|
|
36
|
+
spec[:info][:description] = @api_class.description if @api_class.description
|
|
37
|
+
spec[:tags] = @api_class._openapi_tags if @api_class._openapi_tags.any?
|
|
36
38
|
if @api_class._openapi_security.any?
|
|
37
39
|
spec[:security] = @api_class._openapi_security.map { |name| { name => [] } }
|
|
38
40
|
end
|
|
39
|
-
spec
|
|
41
|
+
spec.deep_dup
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def to_json(*_args)
|
|
@@ -79,18 +81,22 @@ module RailsNinja
|
|
|
79
81
|
body_verb = %w[get delete head].exclude?(verb)
|
|
80
82
|
|
|
81
83
|
operation = { summary: endpoint.summary }
|
|
84
|
+
operation[:description] = endpoint.description if endpoint.description
|
|
82
85
|
operation[:tags] = endpoint.tags if endpoint.tags&.any?
|
|
83
86
|
operation[:responses] = build_responses(endpoint)
|
|
84
87
|
# Deprecated aliases share a handler with the primary endpoint; only the
|
|
85
88
|
# current path carries the operationId to keep it unique across the spec.
|
|
86
|
-
operation[:operationId] = build_operation_id(endpoint) unless endpoint.deprecated?
|
|
89
|
+
operation[:operationId] = endpoint.operation_id || build_operation_id(endpoint) unless endpoint.deprecated?
|
|
87
90
|
operation[:deprecated] = true if endpoint.deprecated?
|
|
88
91
|
|
|
89
92
|
if endpoint.request_schema && body_verb
|
|
90
93
|
operation[:requestBody] = build_request_body(endpoint.request_schema)
|
|
94
|
+
if endpoint.request_description
|
|
95
|
+
operation[:requestBody][:description] = endpoint.request_description
|
|
96
|
+
end
|
|
91
97
|
end
|
|
92
98
|
|
|
93
|
-
params = extract_path_params(path) + extract_header_params(endpoint)
|
|
99
|
+
params = extract_path_params(path, endpoint.path_params) + extract_header_params(endpoint)
|
|
94
100
|
params += extract_query_params(endpoint) unless body_verb
|
|
95
101
|
operation[:parameters] = params if params.any?
|
|
96
102
|
|
|
@@ -113,6 +119,9 @@ module RailsNinja
|
|
|
113
119
|
description: response_description_for(status),
|
|
114
120
|
content: { "application/json" => { schema: schema_node } },
|
|
115
121
|
}
|
|
122
|
+
documentation = endpoint.response_documentation.fetch(status, {})
|
|
123
|
+
out[status.to_s].merge!(documentation.slice(:description, :headers))
|
|
124
|
+
out[status.to_s][:content]["application/json"].merge!(documentation.slice(:example, :examples))
|
|
116
125
|
end
|
|
117
126
|
end
|
|
118
127
|
|
|
@@ -188,9 +197,16 @@ module RailsNinja
|
|
|
188
197
|
end
|
|
189
198
|
end
|
|
190
199
|
|
|
191
|
-
def extract_path_params(path)
|
|
192
|
-
path.scan(/:(\w+)/).flatten
|
|
193
|
-
|
|
200
|
+
def extract_path_params(path, documentation)
|
|
201
|
+
names = path.scan(/:(\w+)/).flatten
|
|
202
|
+
unknown = documentation.keys - names
|
|
203
|
+
raise ArgumentError, "Unknown path parameter annotations: #{unknown.join(', ')}" if unknown.any?
|
|
204
|
+
|
|
205
|
+
names.map do |param|
|
|
206
|
+
metadata = documentation.fetch(param, {})
|
|
207
|
+
{ name: param, in: "path", required: true,
|
|
208
|
+
schema: { type: "string" }.merge(metadata.fetch(:schema, {})) }
|
|
209
|
+
.merge(metadata.slice(:description, :example))
|
|
194
210
|
end
|
|
195
211
|
end
|
|
196
212
|
|
|
@@ -198,7 +214,9 @@ module RailsNinja
|
|
|
198
214
|
return [] unless endpoint.request_schema
|
|
199
215
|
|
|
200
216
|
endpoint.request_schema._fields.map do |name, field|
|
|
201
|
-
{ name: name.to_s, in: "query", required: field.required,
|
|
217
|
+
{ name: name.to_s, in: "query", required: field.required,
|
|
218
|
+
schema: schema_ref(field.type).merge(field.documentation.except(:description, :example)) }
|
|
219
|
+
.merge(field.documentation.slice(:description, :example))
|
|
202
220
|
end
|
|
203
221
|
end
|
|
204
222
|
|
|
@@ -207,6 +225,7 @@ module RailsNinja
|
|
|
207
225
|
|
|
208
226
|
endpoint.header_params.map do |h|
|
|
209
227
|
{ name: h[:name], in: "header", required: h[:required], schema: h[:schema] }
|
|
228
|
+
.merge(h.slice(:description, :example))
|
|
210
229
|
end
|
|
211
230
|
end
|
|
212
231
|
|
|
@@ -261,16 +280,17 @@ module RailsNinja
|
|
|
261
280
|
required = []
|
|
262
281
|
|
|
263
282
|
schema_class._fields.each do |name, field|
|
|
264
|
-
property = schema_ref(field.type)
|
|
283
|
+
property = schema_ref(field.type).deep_dup
|
|
265
284
|
property[:title] = schema_title(name)
|
|
266
285
|
property[:enum] = field.enum if field.enum
|
|
286
|
+
property.merge!(field.documentation)
|
|
267
287
|
properties[name.to_s] = property
|
|
268
288
|
required << name.to_s if field.required
|
|
269
289
|
end
|
|
270
290
|
|
|
271
291
|
result = { type: "object", title: schema_name(schema_class), properties: properties }
|
|
272
292
|
result[:required] = required if required.any?
|
|
273
|
-
result
|
|
293
|
+
result.merge(schema_class.openapi)
|
|
274
294
|
end
|
|
275
295
|
|
|
276
296
|
def schema_title(name)
|
|
@@ -24,13 +24,13 @@ module RailsNinja
|
|
|
24
24
|
required = []
|
|
25
25
|
|
|
26
26
|
schema_class._fields.each do |name, field|
|
|
27
|
-
properties[name.to_s] = to_json_schema(field.type)
|
|
27
|
+
properties[name.to_s] = to_json_schema(field.type).merge(field.documentation)
|
|
28
28
|
required << name.to_s if field.required
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
result = { type: "object", properties: properties }
|
|
32
32
|
result[:required] = required if required.any?
|
|
33
|
-
result
|
|
33
|
+
result.merge(schema_class.openapi).deep_dup
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def primitive_type(type)
|
|
@@ -3,21 +3,44 @@
|
|
|
3
3
|
module RailsNinja
|
|
4
4
|
class Operation
|
|
5
5
|
attr_reader :verb, :path, :handler, :display_handler, :api_class,
|
|
6
|
-
:request_schema, :responses_map, :tags, :summary, :header_params
|
|
6
|
+
:request_schema, :responses_map, :tags, :summary, :header_params,
|
|
7
|
+
:description, :operation_id, :path_params, :request_description, :response_documentation
|
|
7
8
|
|
|
8
9
|
def initialize(verb:, path:, handler:, api_class:, request: nil, response: nil, responses: nil,
|
|
9
|
-
tags: nil, summary: nil, headers: nil, deprecated: false
|
|
10
|
+
tags: nil, summary: nil, headers: nil, deprecated: false, description: nil,
|
|
11
|
+
operation_id: nil, path_params: {}, request_description: nil)
|
|
10
12
|
@verb = verb
|
|
11
13
|
@path = path
|
|
12
14
|
@handler = handler
|
|
13
15
|
@display_handler = handler
|
|
14
16
|
@api_class = api_class
|
|
15
17
|
@request_schema = request
|
|
16
|
-
@
|
|
18
|
+
@response_documentation = {}
|
|
19
|
+
@responses_map = (responses || (response ? { 200 => response } : {})).to_h do |status, declaration|
|
|
20
|
+
if declaration.is_a?(Hash)
|
|
21
|
+
unknown = declaration.keys - %i[schema description example examples headers]
|
|
22
|
+
raise ArgumentError, "Unsupported response #{status} keys: #{unknown.join(', ')}" if unknown.any?
|
|
23
|
+
if declaration.key?(:example) && declaration.key?(:examples)
|
|
24
|
+
raise ArgumentError, "Response #{status} cannot declare both example and examples"
|
|
25
|
+
end
|
|
26
|
+
@response_documentation[status] = declaration.except(:schema).deep_dup
|
|
27
|
+
[status, declaration.fetch(:schema)]
|
|
28
|
+
else
|
|
29
|
+
[status, declaration]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
17
32
|
@tags = tags || api_class._tags || []
|
|
18
33
|
@summary = summary || handler.to_s.tr("_", " ").capitalize
|
|
19
34
|
@header_params = merge_headers(api_class._headers, headers)
|
|
20
35
|
@deprecated = deprecated
|
|
36
|
+
@description = description
|
|
37
|
+
@operation_id = operation_id
|
|
38
|
+
@path_params = path_params.transform_keys(&:to_s).deep_dup
|
|
39
|
+
@path_params.each_value do |metadata|
|
|
40
|
+
unknown = metadata.keys - %i[description example schema]
|
|
41
|
+
raise ArgumentError, "Unsupported path parameter keys: #{unknown.join(', ')}" if unknown.any?
|
|
42
|
+
end
|
|
43
|
+
@request_description = request_description
|
|
21
44
|
end
|
|
22
45
|
|
|
23
46
|
def deprecated?
|
|
@@ -64,7 +87,9 @@ module RailsNinja
|
|
|
64
87
|
if h.is_a?(String)
|
|
65
88
|
{ name: h, required: true, schema: { type: "string" } }
|
|
66
89
|
elsif h.is_a?(Hash)
|
|
67
|
-
{ name: h[:name], required: h.fetch(:required, true),
|
|
90
|
+
{ name: h[:name], required: h.fetch(:required, true),
|
|
91
|
+
schema: h.fetch(:schema, { type: h.fetch(:type, "string") }) }
|
|
92
|
+
.merge(h.slice(:description, :example)).deep_dup
|
|
68
93
|
end
|
|
69
94
|
end
|
|
70
95
|
end
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsNinja
|
|
4
4
|
module Schema
|
|
5
|
-
Field = Struct.new(:name, :type, :required, :default, :enum, keyword_init: true) do
|
|
6
|
-
def initialize(name:, type:, required: true, default: nil, enum: nil)
|
|
7
|
-
|
|
5
|
+
Field = Struct.new(:name, :type, :required, :default, :enum, :documentation, keyword_init: true) do
|
|
6
|
+
def initialize(name:, type:, required: true, default: nil, enum: nil, openapi: {}, **documentation)
|
|
7
|
+
unknown = openapi.keys - %i[format pattern minLength maxLength minimum maximum default enum oneOf]
|
|
8
|
+
raise ArgumentError, "Unsupported field OpenAPI keys: #{unknown.join(', ')}" if unknown.any?
|
|
9
|
+
|
|
10
|
+
unknown = documentation.keys - %i[description example]
|
|
11
|
+
raise ArgumentError, "Unsupported field documentation keys: #{unknown.join(', ')}" if unknown.any?
|
|
12
|
+
|
|
13
|
+
super(name: name, type: type, required: required, default: default, enum: enum,
|
|
14
|
+
documentation: openapi.merge(documentation).deep_dup)
|
|
8
15
|
end
|
|
9
16
|
end
|
|
10
17
|
end
|
data/lib/rails_ninja/schema.rb
CHANGED
|
@@ -4,14 +4,15 @@ module RailsNinja
|
|
|
4
4
|
module Schema
|
|
5
5
|
class Base
|
|
6
6
|
class << self
|
|
7
|
-
def field(name, type, required: true, default: nil, enum: nil)
|
|
7
|
+
def field(name, type, required: true, default: nil, enum: nil, **documentation)
|
|
8
8
|
validate_type!(type)
|
|
9
9
|
_fields[name.to_sym] = Field.new(
|
|
10
10
|
name: name.to_sym,
|
|
11
11
|
type: type,
|
|
12
12
|
required: required,
|
|
13
13
|
default: default,
|
|
14
|
-
enum: enum
|
|
14
|
+
enum: enum,
|
|
15
|
+
**documentation
|
|
15
16
|
)
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -23,9 +24,18 @@ module RailsNinja
|
|
|
23
24
|
@_fields ||= {}
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
def openapi(**documentation)
|
|
28
|
+
unknown = documentation.keys - %i[description example]
|
|
29
|
+
raise ArgumentError, "Unsupported schema documentation keys: #{unknown.join(', ')}" if unknown.any?
|
|
30
|
+
|
|
31
|
+
@_openapi ||= {}
|
|
32
|
+
@_openapi.merge!(documentation.deep_dup)
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
def inherited(subclass)
|
|
27
36
|
super
|
|
28
37
|
subclass.instance_variable_set(:@_fields, _fields.dup)
|
|
38
|
+
subclass.instance_variable_set(:@_openapi, openapi.deep_dup)
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
def validate(data)
|
data/lib/rails_ninja/version.rb
CHANGED
data/lib/rails_ninja.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_ninja
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamin Urrutia
|
|
@@ -167,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
167
167
|
- !ruby/object:Gem::Version
|
|
168
168
|
version: '0'
|
|
169
169
|
requirements: []
|
|
170
|
-
rubygems_version: 4.0.
|
|
170
|
+
rubygems_version: 4.0.20
|
|
171
171
|
specification_version: 4
|
|
172
172
|
summary: Django Ninja-inspired API framework for Ruby/Rails
|
|
173
173
|
test_files: []
|