jekyll-openapi 0.1.0 → 0.3.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 +94 -24
- data/lib/jekyll_openapi/anchors.rb +45 -0
- data/lib/jekyll_openapi/content_type.rb +26 -0
- data/lib/jekyll_openapi/dereferencer.rb +23 -37
- data/lib/jekyll_openapi/document.rb +84 -17
- data/lib/jekyll_openapi/examples.rb +75 -0
- data/lib/jekyll_openapi/filters.rb +81 -4
- data/lib/jekyll_openapi/headers.rb +27 -0
- data/lib/jekyll_openapi/parameters.rb +21 -5
- data/lib/jekyll_openapi/reference.rb +143 -0
- data/lib/jekyll_openapi/schema.rb +189 -0
- data/lib/jekyll_openapi/schema_renderer/html.rb +186 -0
- data/lib/jekyll_openapi/schema_renderer/typescript.rb +59 -49
- data/lib/jekyll_openapi/schema_renderer/xml.rb +48 -29
- data/lib/jekyll_openapi/version.rb +1 -1
- data/lib/jekyll_openapi.rb +7 -0
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c592a43f900f36f1afbfc4432c1dc6b9e55302ec6e481bc6284596a728c36ea
|
|
4
|
+
data.tar.gz: 047ef58c76751ad0cc91415d838dc367ea302f83b4f88fa95f430397db3563b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a2fe7b1a1ca251142a4592cc1222aed94895e1dd8dbe4d13b7106436e4a50812e08c0756bb5051c24f3918ea99c4e0e348bb46ad9158d89848187c5a9e75c9d
|
|
7
|
+
data.tar.gz: 93542f7e0c5bdafa05d21442d67044ab751642163571dd01671165a6af69a21fd94461dd96150ea1bbf49b9e3594686194dbca97f3525fe1d33ccec68717b705
|
data/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# jekyll-openapi
|
|
2
2
|
|
|
3
3
|
A (mostly) spec-compliant [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) toolkit for
|
|
4
|
-
Liquid-based static site generators (Jekyll in particular)
|
|
4
|
+
Liquid-based static site generators (Jekyll in particular), with partial
|
|
5
|
+
[OpenAPI 3.2](https://spec.openapis.org/oas/v3.2.0) support (see
|
|
6
|
+
[OpenAPI 3.2 support](#openapi-32-support)).
|
|
5
7
|
|
|
6
8
|
It deliberately does **not** render HTML. The gem provides:
|
|
7
9
|
|
|
@@ -75,19 +77,20 @@ title: API Reference
|
|
|
75
77
|
operation, grouped by tag. That's a complete, working integration.
|
|
76
78
|
|
|
77
79
|
`oapi_operations` already does the tedious parts for you: it skips
|
|
78
|
-
non-operation path-item keys, handles `x-` extension methods
|
|
79
|
-
and merges path-level parameters into
|
|
80
|
+
non-operation path-item keys, handles `x-` extension methods and OpenAPI 3.2
|
|
81
|
+
`additionalOperations`, filters by tag, and merges path-level parameters into
|
|
82
|
+
each operation.
|
|
80
83
|
|
|
81
84
|
Each `op` in the loop is a plain Hash you can traverse further:
|
|
82
85
|
|
|
83
86
|
| key | content |
|
|
84
87
|
|------------------------|------------------------------------------------------|
|
|
85
88
|
| `"path"` | `"/pets/{petId}"` |
|
|
86
|
-
| `"method"` | normalized method, `x-` prefix stripped (`"mkcol"`)
|
|
87
|
-
| `"raw_method"` | the literal key (`"x-mkcol"`)
|
|
89
|
+
| `"method"` | normalized method, lowercase, `x-` prefix stripped (`"mkcol"`) |
|
|
90
|
+
| `"raw_method"` | the literal key (`"x-mkcol"`, `"MKCOL"`) |
|
|
88
91
|
| `"operation"` | the Operation object, dereferenced |
|
|
89
92
|
| `"parameters"` | path-level + operation-level parameters, merged (operation wins on same name/location) |
|
|
90
|
-
| `"grouped_parameters"` | same, grouped: `{"query" => [...], "path" => [...], "header" => [...], "cookie" => [...]}` |
|
|
93
|
+
| `"grouped_parameters"` | same, grouped: `{"query" => [...], "querystring" => [...], "path" => [...], "header" => [...], "cookie" => [...]}` |
|
|
91
94
|
|
|
92
95
|
### Going further
|
|
93
96
|
|
|
@@ -107,17 +110,28 @@ schemas — start from the complete examples and restyle them:
|
|
|
107
110
|
Registered automatically in Jekyll. (In a plain Liquid setup you register them
|
|
108
111
|
yourself — see [examples/ruby/README.md](examples/ruby/README.md#liquid-filters-outside-jekyll).)
|
|
109
112
|
|
|
110
|
-
| filter
|
|
111
|
-
|
|
112
|
-
| `oapi_dereference`
|
|
113
|
-
| `oapi_tags`
|
|
114
|
-
| `oapi_operations`
|
|
115
|
-
| `oapi_parameters`
|
|
116
|
-
| `oapi_merge_parameters`
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
| filter | input → output |
|
|
114
|
+
|--------------------------|----------------|
|
|
115
|
+
| `oapi_dereference` | document → document with local `$ref`s made navigable (lazy, name-preserving). Optional — every filter below dereferences internally; call it only if you also hand-walk the raw hash. Idempotent. |
|
|
116
|
+
| `oapi_tags` | document → tag list |
|
|
117
|
+
| `oapi_operations` | document (+ optional tag argument) → operation list |
|
|
118
|
+
| `oapi_parameters` | parameter list → grouped by location |
|
|
119
|
+
| `oapi_merge_parameters` | path params, operation params → merged list |
|
|
120
|
+
| `oapi_examples` | parameter or media-type object → normalized `{name, value, summary, description}` list, unifying `examples`/`example`/schema-level examples |
|
|
121
|
+
| `oapi_headers` | response object → normalized `{name, description, required, deprecated, schema}` list (`$ref`'d headers resolved) |
|
|
122
|
+
| `oapi_schemas` | document → named component-schema map (for a Models section) |
|
|
123
|
+
| `oapi_security_schemes` | document → named security-scheme map |
|
|
124
|
+
| `oapi_servers` | document → server list |
|
|
125
|
+
| `oapi_schema` | JSON-Schema → TypeScript-like string |
|
|
126
|
+
| `oapi_xml_schema` | JSON-Schema → simplified XML string |
|
|
127
|
+
| `oapi_html_schema` | JSON-Schema → semantic HTML; named component refs become links. Works on dereferenced *or* raw `{"$ref": …}` schemas (link mode needs only the pointer). |
|
|
128
|
+
| `oapi_schema_anchor` | schema name → `schema-<name>` anchor id |
|
|
129
|
+
| `oapi_operation_anchor` | operation entry (or operationId) → fragment id; falls back to method+path when operationId is absent |
|
|
130
|
+
| `oapi_tag_anchor` | tag object (or name) → fragment id |
|
|
131
|
+
| `oapi_representation` | media-type string → `"xml"`/`"json"`/`"text"`/`"binary"` (robust to `text/xml`, `*+xml`, `; charset=…`) |
|
|
132
|
+
| `oapi_is_xml` | media-type string → boolean (XML-family) |
|
|
133
|
+
|
|
134
|
+
The text schema filters turn a JSON-Schema into readable text, e.g.
|
|
121
135
|
`{{ content.schema | oapi_schema }}` produces:
|
|
122
136
|
|
|
123
137
|
```typescript
|
|
@@ -127,15 +141,71 @@ The two schema filters turn a JSON-Schema into readable text, e.g.
|
|
|
127
141
|
}
|
|
128
142
|
```
|
|
129
143
|
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
`oapi_html_schema` instead emits semantic, class-annotated HTML (`<dl>`/`<ul>`,
|
|
145
|
+
never tables) in which a `$ref` to a named component schema renders as a link to
|
|
146
|
+
`#schema-<name>` rather than being inlined — so operation bodies stay compact,
|
|
147
|
+
reused types are cross-linked, and recursive schemas terminate. Descriptions are
|
|
148
|
+
rendered as markdown when run inside Jekyll (it reuses the site's configured
|
|
149
|
+
converter) and as escaped plain text otherwise. Pair it with
|
|
150
|
+
`oapi_schemas` + `oapi_schema_anchor` to emit a matching "Models" section; see
|
|
151
|
+
[examples/jekyll/_includes/oapi/models.html](examples/jekyll/_includes/oapi/models.html)
|
|
152
|
+
and [schema.html](examples/jekyll/_includes/oapi/schema.html) (which picks a
|
|
153
|
+
renderer by content type), styled by
|
|
154
|
+
[_sass/oapi-schema.scss](examples/jekyll/_sass/oapi-schema.scss).
|
|
132
155
|
|
|
133
156
|
## Error handling
|
|
134
157
|
|
|
135
158
|
The library never raises on malformed input: it warns through
|
|
136
159
|
`JekyllOpenAPI.logger` (in Jekyll, routed to Jekyll's own logger) and degrades
|
|
137
|
-
gracefully — unresolvable
|
|
138
|
-
|
|
160
|
+
gracefully — unresolvable and external `$ref`s are left in place and warn only
|
|
161
|
+
when followed; recursive schemas terminate (rendered as a link, or their name in
|
|
162
|
+
the text renderers); invalid schemas render as `""`. Your build never breaks on
|
|
163
|
+
a bad spec.
|
|
164
|
+
|
|
165
|
+
## OpenAPI 3.2 support
|
|
166
|
+
|
|
167
|
+
Partial. Because the model hands templates plain hashes, most new 3.2 *fields*
|
|
168
|
+
flow through untouched and are usable today; "supported" below means the
|
|
169
|
+
library actively understands the feature, "passthrough" means the raw data
|
|
170
|
+
reaches your template but no helper interprets it.
|
|
171
|
+
|
|
172
|
+
Supported:
|
|
173
|
+
|
|
174
|
+
- [x] `additionalOperations` — enumerated by `oapi_operations` like any other
|
|
175
|
+
operation, with path-level parameters merged; `method` is the lowercased
|
|
176
|
+
method name, `raw_method` the literal key (`"PROPFIND"`). Gives webDAV-style
|
|
177
|
+
APIs first-class treatment (previously only reachable via `x-` methods).
|
|
178
|
+
- [x] The `query` HTTP method — recognized as a first-class path-item key.
|
|
179
|
+
- [x] Tag `summary` — passes through `oapi_tags` (tested); use it as a short
|
|
180
|
+
display name where `description` is prose.
|
|
181
|
+
- [x] `in: querystring` parameters — grouped under their own `"querystring"`
|
|
182
|
+
key by `oapi_parameters` / `grouped_parameters` (the parameter is
|
|
183
|
+
`content`-based: render its media-type schema, not a name/value pair). An
|
|
184
|
+
operation-level querystring parameter overrides a path-level one regardless
|
|
185
|
+
of name, and mixing it with `in: query` parameters warns (the spec forbids
|
|
186
|
+
it) but keeps both.
|
|
187
|
+
|
|
188
|
+
Passthrough (data reaches templates, no helper logic):
|
|
189
|
+
|
|
190
|
+
- [ ] Tag `parent` / `kind` — the fields are readable, but `oapi_operations`
|
|
191
|
+
and `oapi_tags` treat tags as a flat list: no hierarchy building, no
|
|
192
|
+
filtering by kind.
|
|
193
|
+
- [ ] Response `summary`, Server `name` — readable on the raw objects.
|
|
194
|
+
- [ ] Security scheme additions (`deviceAuthorization` flow,
|
|
195
|
+
`oauth2MetadataUrl`, `deprecated`) — readable via `oapi_security_schemes`.
|
|
196
|
+
|
|
197
|
+
Not supported (TODO):
|
|
198
|
+
|
|
199
|
+
- [ ] Streaming / sequential media types — `itemSchema`, `itemEncoding` and
|
|
200
|
+
`prefixEncoding` are ignored by the schema renderers and `oapi_examples`.
|
|
201
|
+
- [ ] Example Object `dataValue` / `serializedValue` — `oapi_examples` only
|
|
202
|
+
reads `value`.
|
|
203
|
+
- [ ] `xml.nodeType` — the XML renderer still keys off the 3.1 `attribute` /
|
|
204
|
+
`wrapped` flags.
|
|
205
|
+
- [ ] `$self` and multi-document descriptions — consistent with the existing
|
|
206
|
+
local-`$ref`s-only policy.
|
|
207
|
+
- [ ] `components.mediaTypes` reuse — untested; generic local `$ref`
|
|
208
|
+
resolution may already handle it, but no guarantees.
|
|
139
209
|
|
|
140
210
|
## Compliance notes
|
|
141
211
|
|
|
@@ -156,8 +226,8 @@ fidelity":
|
|
|
156
226
|
rake test # or: ruby -Ilib -Itest test/test_<name>.rb
|
|
157
227
|
```
|
|
158
228
|
|
|
159
|
-
Tests run against generic fixtures (`test/fixtures/petstore.yml
|
|
229
|
+
Tests run against generic fixtures (`test/fixtures/petstore.yml`, a pristine
|
|
230
|
+
OpenAPI 3.1 document, and `test/fixtures/filestore.yml`, a WebDAV-flavored
|
|
231
|
+
OpenAPI 3.2 one); when the gem
|
|
160
232
|
sources live inside the eCorpus_doc repository, an extra integration suite runs
|
|
161
233
|
the whole pipeline over the real eCorpus API definition.
|
|
162
|
-
</content>
|
|
163
|
-
</invoke>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require_relative "reference"
|
|
2
|
+
|
|
3
|
+
module JekyllOpenAPI
|
|
4
|
+
##
|
|
5
|
+
# Stable, deterministic fragment ids for operations and tags — the counterpart
|
|
6
|
+
# to SchemaRenderer::HTML.anchor for the parts of a reference page the schema
|
|
7
|
+
# renderer does not own.
|
|
8
|
+
#
|
|
9
|
+
# `operationId` is *optional* in OpenAPI, so a template that anchors on it
|
|
10
|
+
# alone emits `id=""` (and collisions) the day an operation omits it; here the
|
|
11
|
+
# method+path is the fallback. The slug matches Jekyll's default `slugify`
|
|
12
|
+
# (downcase, non-alphanumeric runs to single hyphens, trimmed) so these are
|
|
13
|
+
# drop-in replacements for a hand-rolled `| slugify` and keep existing anchor
|
|
14
|
+
# URLs stable.
|
|
15
|
+
module Anchors
|
|
16
|
+
# @param op [Hash, String] an operation entry (from Document#operations) or
|
|
17
|
+
# a bare operationId
|
|
18
|
+
def self.operation(op)
|
|
19
|
+
id =
|
|
20
|
+
if op.is_a?(Hash) || op.is_a?(Reference)
|
|
21
|
+
dig(op, "operation", "operationId") || dig(op, "operationId") ||
|
|
22
|
+
"#{op["method"]} #{op["path"]}"
|
|
23
|
+
else
|
|
24
|
+
op
|
|
25
|
+
end
|
|
26
|
+
slug(id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param tag [Hash, String] a tag object (from Document#tags) or a bare name
|
|
30
|
+
def self.tag(tag)
|
|
31
|
+
name = (tag.is_a?(Hash) || tag.is_a?(Reference)) ? tag["name"] : tag
|
|
32
|
+
slug(name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Jekyll-`slugify`-compatible (default mode) slug.
|
|
36
|
+
def self.slug(text)
|
|
37
|
+
text.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.dig(object, *keys)
|
|
41
|
+
object.respond_to?(:dig) ? object.dig(*keys) : nil
|
|
42
|
+
end
|
|
43
|
+
private_class_method :dig
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module JekyllOpenAPI
|
|
2
|
+
##
|
|
3
|
+
# Classifies a media-type string so templates pick a schema representation
|
|
4
|
+
# without brittle `== "application/xml"` comparisons that miss `text/xml`,
|
|
5
|
+
# `application/*+xml`, or a trailing `; charset=…` parameter.
|
|
6
|
+
module ContentType
|
|
7
|
+
# @return [String] one of "xml", "json", "text", "binary"
|
|
8
|
+
def self.representation(content_type)
|
|
9
|
+
ct = normalize(content_type)
|
|
10
|
+
return "xml" if ct == "application/xml" || ct == "text/xml" || ct.end_with?("+xml")
|
|
11
|
+
return "json" if ct == "application/json" || ct.end_with?("+json")
|
|
12
|
+
return "text" if ct.start_with?("text/")
|
|
13
|
+
"binary"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.xml?(content_type)
|
|
17
|
+
representation(content_type) == "xml"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Strip any `; charset=…`/parameters and normalize case/whitespace.
|
|
21
|
+
def self.normalize(content_type)
|
|
22
|
+
content_type.to_s.split(";").first.to_s.strip.downcase
|
|
23
|
+
end
|
|
24
|
+
private_class_method :normalize
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
require_relative "json_pointer"
|
|
2
|
+
require_relative "reference"
|
|
2
3
|
|
|
3
4
|
module JekyllOpenAPI
|
|
4
5
|
##
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Walks an OpenAPI document once and replaces every local `$ref` node with a
|
|
7
|
+
# lazy {Reference} instead of inlining its target. The reference graph and
|
|
8
|
+
# component names are preserved; targets are resolved on demand (see
|
|
9
|
+
# Reference), which keeps recursion finite and reused schemas shared.
|
|
10
|
+
#
|
|
11
|
+
# External references (other files, URLs) and unresolvable pointers surface a
|
|
12
|
+
# warning when they are first followed, not while linking.
|
|
8
13
|
class Dereferencer
|
|
9
14
|
def initialize(root, logger: JekyllOpenAPI.logger)
|
|
10
15
|
@root = root
|
|
@@ -15,49 +20,30 @@ module JekyllOpenAPI
|
|
|
15
20
|
new(root, logger: logger).dereference
|
|
16
21
|
end
|
|
17
22
|
|
|
18
|
-
# @return a deep copy of the document with every
|
|
19
|
-
|
|
23
|
+
# @return a deep copy of the document with every `$ref` replaced by a
|
|
24
|
+
# Reference that resolves lazily against the copy.
|
|
25
|
+
def dereference
|
|
26
|
+
context = Reference::Context.new(nil, @logger)
|
|
27
|
+
context.root = link(@root, context)
|
|
28
|
+
context.root
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def link(node, context)
|
|
20
34
|
case node
|
|
21
35
|
when Hash
|
|
22
36
|
if node.key?("$ref")
|
|
23
|
-
|
|
37
|
+
# summary/description may sit beside $ref and override the target (3.1)
|
|
38
|
+
Reference.new(node["$ref"], context, node.slice("summary", "description"))
|
|
24
39
|
else
|
|
25
|
-
node.transform_values { |value|
|
|
40
|
+
node.transform_values { |value| link(value, context) }
|
|
26
41
|
end
|
|
27
42
|
when Array
|
|
28
|
-
node.map { |value|
|
|
43
|
+
node.map { |value| link(value, context) }
|
|
29
44
|
else
|
|
30
45
|
node
|
|
31
46
|
end
|
|
32
47
|
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def resolve(node, stack)
|
|
37
|
-
ref = node["$ref"]
|
|
38
|
-
unless ref.is_a?(String) && ref.start_with?("#")
|
|
39
|
-
@logger.warn("external or invalid $ref left unresolved: #{ref.inspect}")
|
|
40
|
-
return node
|
|
41
|
-
end
|
|
42
|
-
if stack.include?(ref)
|
|
43
|
-
@logger.warn("circular $ref left unresolved: #{ref}")
|
|
44
|
-
return node
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
target = JSONPointer.resolve(@root, ref.delete_prefix("#"))
|
|
48
|
-
if target.nil?
|
|
49
|
-
@logger.warn("unresolvable $ref: #{ref}")
|
|
50
|
-
return node
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
resolved = dereference(target, stack + [ref])
|
|
54
|
-
# OpenAPI 3.1 allows summary/description as siblings of $ref, overriding the target's
|
|
55
|
-
overrides = node.slice("summary", "description")
|
|
56
|
-
if resolved.is_a?(Hash) && !overrides.empty?
|
|
57
|
-
resolved.merge(overrides)
|
|
58
|
-
else
|
|
59
|
-
resolved
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
48
|
end
|
|
63
49
|
end
|
|
@@ -9,9 +9,12 @@ module JekyllOpenAPI
|
|
|
9
9
|
#
|
|
10
10
|
# Absorbs the spec-aware traversal that would otherwise live in templates:
|
|
11
11
|
# enumerating operations (including `x-` extension methods), filtering by tag,
|
|
12
|
-
#
|
|
12
|
+
# merging path-level with operation-level parameters, and surfacing the
|
|
13
|
+
# component collections (schemas, security schemes, servers) that a reference
|
|
14
|
+
# page needs. `$ref`s stay navigable but keep their names (see Reference), so
|
|
15
|
+
# templates can link named schemas instead of inlining them.
|
|
13
16
|
class Document
|
|
14
|
-
HTTP_METHODS = %w[get put post delete options head patch trace].freeze
|
|
17
|
+
HTTP_METHODS = %w[get put post delete options head patch trace query].freeze
|
|
15
18
|
PATH_ITEM_FIELDS = %w[summary description servers parameters $ref].freeze
|
|
16
19
|
|
|
17
20
|
attr_reader :data
|
|
@@ -32,7 +35,8 @@ module JekyllOpenAPI
|
|
|
32
35
|
new(data, logger: logger)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
# @return [Hash] the document
|
|
38
|
+
# @return [Hash] a copy of the document in which every local `$ref` is a lazy
|
|
39
|
+
# Reference (navigable, name-preserving, resolved on demand); memoized
|
|
36
40
|
def dereferenced
|
|
37
41
|
@dereferenced ||= Dereferencer.new(@data, logger: @logger).dereference
|
|
38
42
|
end
|
|
@@ -49,6 +53,36 @@ module JekyllOpenAPI
|
|
|
49
53
|
dereferenced["paths"] || {}
|
|
50
54
|
end
|
|
51
55
|
|
|
56
|
+
# Declared servers (empty when the document omits them).
|
|
57
|
+
def servers
|
|
58
|
+
dereferenced["servers"] || []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Top-level security requirements applied to every operation by default.
|
|
62
|
+
def security
|
|
63
|
+
dereferenced["security"] || []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The whole `components` object. Prefer the named helpers below.
|
|
67
|
+
def components
|
|
68
|
+
dereferenced["components"] || {}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Named schema map, e.g. for a "Models" reference section. Values keep their
|
|
73
|
+
# nested `$ref`s as References, so a template can render each schema's body
|
|
74
|
+
# and cross-link by name (see Schema#name) rather than inline everything.
|
|
75
|
+
# @return [Hash{String => Object}]
|
|
76
|
+
def schemas
|
|
77
|
+
components["schemas"] || {}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Named security scheme map (apiKey/http/oauth2/openIdConnect definitions).
|
|
81
|
+
# @return [Hash{String => Object}]
|
|
82
|
+
def security_schemes
|
|
83
|
+
components["securitySchemes"] || {}
|
|
84
|
+
end
|
|
85
|
+
|
|
52
86
|
##
|
|
53
87
|
# Flat list of every operation in the document, in path order.
|
|
54
88
|
# `x-`-prefixed extension methods (e.g. webDAV verbs) are included,
|
|
@@ -78,23 +112,56 @@ module JekyllOpenAPI
|
|
|
78
112
|
|
|
79
113
|
def build_operations
|
|
80
114
|
paths.flat_map do |path, path_item|
|
|
115
|
+
# A path item (and, less regularly, an operation) may itself be a $ref;
|
|
116
|
+
# follow it so referenced operations are not silently dropped.
|
|
117
|
+
path_item = follow(path_item)
|
|
81
118
|
next [] unless path_item.is_a?(Hash)
|
|
82
|
-
path_item.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
path_item.flat_map do |key, value|
|
|
120
|
+
if key == "additionalOperations"
|
|
121
|
+
# OpenAPI 3.2: a map of custom HTTP method name => Operation Object.
|
|
122
|
+
value = follow(value)
|
|
123
|
+
next [] unless value.is_a?(Hash)
|
|
124
|
+
value.filter_map do |method, operation|
|
|
125
|
+
build_operation(path, path_item, method.downcase, method, operation)
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
next [] if PATH_ITEM_FIELDS.include?(key)
|
|
129
|
+
next [] unless HTTP_METHODS.include?(key) || key.start_with?("x-")
|
|
130
|
+
operation = follow(value)
|
|
131
|
+
# `x-` is OpenAPI's extension mechanism, not a method namespace: a
|
|
132
|
+
# path-item extension is only an operation when it actually carries an
|
|
133
|
+
# Operation Object. `operationId`/`responses` mark one; vendor metadata
|
|
134
|
+
# (x-internal, x-codegen, …) has neither and must not become a phantom
|
|
135
|
+
# method. Standard HTTP verbs are always operations.
|
|
136
|
+
next [] if operation.is_a?(Hash) && key.start_with?("x-") &&
|
|
137
|
+
!(operation.key?("operationId") || operation.key?("responses"))
|
|
138
|
+
|
|
139
|
+
[build_operation(path, path_item, key.delete_prefix("x-"), key, operation)].compact
|
|
140
|
+
end
|
|
96
141
|
end
|
|
97
142
|
end
|
|
98
143
|
end
|
|
144
|
+
|
|
145
|
+
# @return [Hash, nil] a normalized operation entry, or nil when `operation`
|
|
146
|
+
# is not an Operation Object
|
|
147
|
+
def build_operation(path, path_item, method, raw_method, operation)
|
|
148
|
+
operation = follow(operation)
|
|
149
|
+
return unless operation.is_a?(Hash)
|
|
150
|
+
|
|
151
|
+
params = Parameters.merge(path_item["parameters"], operation["parameters"])
|
|
152
|
+
{
|
|
153
|
+
"path" => path,
|
|
154
|
+
"method" => method,
|
|
155
|
+
"raw_method" => raw_method,
|
|
156
|
+
"operation" => operation,
|
|
157
|
+
"parameters" => params,
|
|
158
|
+
"grouped_parameters" => Parameters.group(params, logger: @logger),
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Resolve a $ref used where a mapping (path item / operation) is expected.
|
|
163
|
+
def follow(node)
|
|
164
|
+
node.is_a?(Reference) ? node.resolve : node
|
|
165
|
+
end
|
|
99
166
|
end
|
|
100
167
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require_relative "reference"
|
|
2
|
+
|
|
3
|
+
module JekyllOpenAPI
|
|
4
|
+
##
|
|
5
|
+
# Flattens the several places and shapes OpenAPI examples take into one list,
|
|
6
|
+
# so a template never has to care where an example came from.
|
|
7
|
+
#
|
|
8
|
+
# Sources, in priority order, for a Parameter or Media Type object:
|
|
9
|
+
# 1. +examples+ — a map of named Example Objects ({summary, description, value})
|
|
10
|
+
# 2. +example+ — a single, unnamed value
|
|
11
|
+
# 3. +schema.examples+ — JSON-Schema examples: an array of values (3.1), or
|
|
12
|
+
# (tolerated) a named map like (1)
|
|
13
|
+
# 4. +schema.example+ — a single, unnamed value
|
|
14
|
+
#
|
|
15
|
+
# The first non-empty source wins — this mirrors the spec's "example XOR
|
|
16
|
+
# examples" rule while still reaching the schema-level fallback, so callers get
|
|
17
|
+
# the same result whether examples live on the parameter (3.x style) or on its
|
|
18
|
+
# schema. Each entry is a plain, string-keyed Hash so it is Liquid-friendly:
|
|
19
|
+
# {"name", "value", "summary", "description"} (name is nil for unnamed values).
|
|
20
|
+
module Examples
|
|
21
|
+
# @param object [Hash, Reference] a Parameter or Media Type object
|
|
22
|
+
# @return [Array<Hash>] normalized examples ([] when there are none)
|
|
23
|
+
def self.normalize(object)
|
|
24
|
+
return [] unless mapping?(object)
|
|
25
|
+
|
|
26
|
+
from_map_or_array(object["examples"]) ||
|
|
27
|
+
from_value(object["example"]) ||
|
|
28
|
+
from_map_or_array(dig(object, "schema", "examples")) ||
|
|
29
|
+
from_value(dig(object, "schema", "example")) ||
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.mapping?(node)
|
|
34
|
+
node.is_a?(Hash) || node.is_a?(Reference)
|
|
35
|
+
end
|
|
36
|
+
private_class_method :mapping?
|
|
37
|
+
|
|
38
|
+
def self.dig(object, *keys)
|
|
39
|
+
object.respond_to?(:dig) ? object.dig(*keys) : nil
|
|
40
|
+
end
|
|
41
|
+
private_class_method :dig
|
|
42
|
+
|
|
43
|
+
# A map of named Example Objects, or a JSON-Schema array of raw values.
|
|
44
|
+
def self.from_map_or_array(examples)
|
|
45
|
+
case examples
|
|
46
|
+
when Hash
|
|
47
|
+
return nil if examples.empty?
|
|
48
|
+
examples.map do |name, example|
|
|
49
|
+
entry(name, value_of(example), example["summary"], example["description"])
|
|
50
|
+
end
|
|
51
|
+
when Array
|
|
52
|
+
return nil if examples.empty?
|
|
53
|
+
examples.map { |value| entry(nil, value, nil, nil) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
private_class_method :from_map_or_array
|
|
57
|
+
|
|
58
|
+
def self.from_value(example)
|
|
59
|
+
return nil if example.nil?
|
|
60
|
+
[entry(nil, example, nil, nil)]
|
|
61
|
+
end
|
|
62
|
+
private_class_method :from_value
|
|
63
|
+
|
|
64
|
+
# An Example Object wraps its payload under "value"; a bare scalar is itself.
|
|
65
|
+
def self.value_of(example)
|
|
66
|
+
mapping?(example) ? example["value"] : example
|
|
67
|
+
end
|
|
68
|
+
private_class_method :value_of
|
|
69
|
+
|
|
70
|
+
def self.entry(name, value, summary, description)
|
|
71
|
+
{ "name" => name, "value" => value, "summary" => summary, "description" => description }
|
|
72
|
+
end
|
|
73
|
+
private_class_method :entry
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -9,7 +9,8 @@ module JekyllOpenAPI
|
|
|
9
9
|
#
|
|
10
10
|
# or register only a subset by wrapping the methods they want.
|
|
11
11
|
module Filters
|
|
12
|
-
#
|
|
12
|
+
# Replace every local $ref with a lazy, name-preserving Reference. Reads
|
|
13
|
+
# walk through transparently; recursion and reuse are handled (see Reference).
|
|
13
14
|
def oapi_dereference(input)
|
|
14
15
|
Dereferencer.new(input).dereference
|
|
15
16
|
end
|
|
@@ -24,6 +25,13 @@ module JekyllOpenAPI
|
|
|
24
25
|
Parameters.merge(path_params, operation_params)
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
# Normalized example list for a parameter or media-type object, unifying
|
|
29
|
+
# `examples` (named Example Objects), `example` (single value) and the
|
|
30
|
+
# schema-level equivalents. Each entry is {name, value, summary, description}.
|
|
31
|
+
def oapi_examples(input)
|
|
32
|
+
Examples.normalize(input)
|
|
33
|
+
end
|
|
34
|
+
|
|
27
35
|
# Flat operation list from a full document, optionally filtered by tag.
|
|
28
36
|
# See Document#operations for the shape of each entry.
|
|
29
37
|
def oapi_operations(input, tag = nil)
|
|
@@ -35,6 +43,21 @@ module JekyllOpenAPI
|
|
|
35
43
|
document(input).tags
|
|
36
44
|
end
|
|
37
45
|
|
|
46
|
+
# Named schema map of a full document (for a "Models" reference section)
|
|
47
|
+
def oapi_schemas(input)
|
|
48
|
+
document(input).schemas
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Named security scheme map of a full document
|
|
52
|
+
def oapi_security_schemes(input)
|
|
53
|
+
document(input).security_schemes
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Declared servers of a full document
|
|
57
|
+
def oapi_servers(input)
|
|
58
|
+
document(input).servers
|
|
59
|
+
end
|
|
60
|
+
|
|
38
61
|
# Render a JSON-Schema as a TypeScript-like string
|
|
39
62
|
def oapi_schema(input)
|
|
40
63
|
SchemaRenderer::TypeScript.render(input)
|
|
@@ -45,13 +68,67 @@ module JekyllOpenAPI
|
|
|
45
68
|
SchemaRenderer::XML.render(input)
|
|
46
69
|
end
|
|
47
70
|
|
|
71
|
+
# Render a JSON-Schema as semantic HTML, linking named component schemas to
|
|
72
|
+
# their #schema-<name> anchor instead of inlining them. Descriptions are
|
|
73
|
+
# rendered as markdown when a converter is available (inside Jekyll), else as
|
|
74
|
+
# escaped plain text.
|
|
75
|
+
def oapi_html_schema(input)
|
|
76
|
+
SchemaRenderer::HTML.render(input, markdown: markdown_formatter)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Anchor id for a named schema, e.g. "Pet" => "schema-Pet". Use it for the
|
|
80
|
+
# matching `id` on a Models section so the renderer's links resolve.
|
|
81
|
+
def oapi_schema_anchor(name)
|
|
82
|
+
SchemaRenderer::HTML.anchor(name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Stable fragment id for an operation (entry from `oapi_operations`, or a
|
|
86
|
+
# bare operationId). Falls back to method+path when operationId is absent.
|
|
87
|
+
def oapi_operation_anchor(op)
|
|
88
|
+
Anchors.operation(op)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Stable fragment id for a tag (tag object from `oapi_tags`, or a bare name).
|
|
92
|
+
def oapi_tag_anchor(tag)
|
|
93
|
+
Anchors.tag(tag)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Classify a media-type string: "xml" | "json" | "text" | "binary".
|
|
97
|
+
# Robust to `text/xml`, `application/*+xml`, and `; charset=…` suffixes.
|
|
98
|
+
def oapi_representation(content_type)
|
|
99
|
+
ContentType.representation(content_type)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# True for XML-family media types (application/xml, text/xml, *+xml).
|
|
103
|
+
def oapi_is_xml(content_type)
|
|
104
|
+
ContentType.xml?(content_type)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Normalized header list for a response object: each entry is
|
|
108
|
+
# {name, description, required, deprecated, schema}. `$ref`'d headers resolve.
|
|
109
|
+
def oapi_headers(response)
|
|
110
|
+
Headers.list(response)
|
|
111
|
+
end
|
|
112
|
+
|
|
48
113
|
private
|
|
49
114
|
|
|
115
|
+
# A `text -> html` markdown converter, or nil when none is reachable. Reuses
|
|
116
|
+
# whatever `markdownify` filter is mounted alongside us (Jekyll registers
|
|
117
|
+
# one, and it resolves the site's configured converter via the Liquid
|
|
118
|
+
# context); outside Jekyll there is none, so descriptions stay plain text.
|
|
119
|
+
def markdown_formatter
|
|
120
|
+
return nil unless respond_to?(:markdownify)
|
|
121
|
+
->(text) { markdownify(text) }
|
|
122
|
+
end
|
|
123
|
+
|
|
50
124
|
# Memoize per input so several filter calls on the same document
|
|
51
|
-
# (tags, then operations per tag) only dereference it once.
|
|
125
|
+
# (tags, then operations per tag) only dereference it once. Keyed by object
|
|
126
|
+
# identity: the hash holds the input as key, so it can't be GC'd and have its
|
|
127
|
+
# object_id recycled onto a different document (which a bare object_id key
|
|
128
|
+
# would alias to a stale, wrong Document).
|
|
52
129
|
def document(input)
|
|
53
|
-
@oapi_documents ||= {}
|
|
54
|
-
@oapi_documents[input
|
|
130
|
+
@oapi_documents ||= {}.compare_by_identity
|
|
131
|
+
@oapi_documents[input] ||= Document.new(input)
|
|
55
132
|
end
|
|
56
133
|
end
|
|
57
134
|
end
|