jekyll-openapi 0.1.0 → 0.2.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 +37 -15
- 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 +52 -2
- 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 +10 -2
- 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 +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 115a158c4677920431b493e1d8941d8bc84f7ac71f0d9813f386ed9a1391878d
|
|
4
|
+
data.tar.gz: 2444e2822f9181d34c4ade929b0903861d43ea6b9a9376db09baef3abe779906
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 800e3dd82c5c47f0b5b1e31319199ffc5045eb72ebc5285062ab2ecb0e831bcb56a842b7fd168bd239b9b4ac194056c1a2d059650361f6cf484857ac3d0704de
|
|
7
|
+
data.tar.gz: 677f5f4053e77ec018823e08e24cf89e3a2b44c70ea5267368eee22e83195407872487f134646743a2dcfca9bc78d284501b3140e12c771598e0bfa4bca0e1fc
|
data/README.md
CHANGED
|
@@ -107,17 +107,28 @@ schemas — start from the complete examples and restyle them:
|
|
|
107
107
|
Registered automatically in Jekyll. (In a plain Liquid setup you register them
|
|
108
108
|
yourself — see [examples/ruby/README.md](examples/ruby/README.md#liquid-filters-outside-jekyll).)
|
|
109
109
|
|
|
110
|
-
| filter
|
|
111
|
-
|
|
112
|
-
| `oapi_dereference`
|
|
113
|
-
| `oapi_tags`
|
|
114
|
-
| `oapi_operations`
|
|
115
|
-
| `oapi_parameters`
|
|
116
|
-
| `oapi_merge_parameters`
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
| filter | input → output |
|
|
111
|
+
|--------------------------|----------------|
|
|
112
|
+
| `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. |
|
|
113
|
+
| `oapi_tags` | document → tag list |
|
|
114
|
+
| `oapi_operations` | document (+ optional tag argument) → operation list |
|
|
115
|
+
| `oapi_parameters` | parameter list → grouped by location |
|
|
116
|
+
| `oapi_merge_parameters` | path params, operation params → merged list |
|
|
117
|
+
| `oapi_examples` | parameter or media-type object → normalized `{name, value, summary, description}` list, unifying `examples`/`example`/schema-level examples |
|
|
118
|
+
| `oapi_headers` | response object → normalized `{name, description, required, deprecated, schema}` list (`$ref`'d headers resolved) |
|
|
119
|
+
| `oapi_schemas` | document → named component-schema map (for a Models section) |
|
|
120
|
+
| `oapi_security_schemes` | document → named security-scheme map |
|
|
121
|
+
| `oapi_servers` | document → server list |
|
|
122
|
+
| `oapi_schema` | JSON-Schema → TypeScript-like string |
|
|
123
|
+
| `oapi_xml_schema` | JSON-Schema → simplified XML string |
|
|
124
|
+
| `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). |
|
|
125
|
+
| `oapi_schema_anchor` | schema name → `schema-<name>` anchor id |
|
|
126
|
+
| `oapi_operation_anchor` | operation entry (or operationId) → fragment id; falls back to method+path when operationId is absent |
|
|
127
|
+
| `oapi_tag_anchor` | tag object (or name) → fragment id |
|
|
128
|
+
| `oapi_representation` | media-type string → `"xml"`/`"json"`/`"text"`/`"binary"` (robust to `text/xml`, `*+xml`, `; charset=…`) |
|
|
129
|
+
| `oapi_is_xml` | media-type string → boolean (XML-family) |
|
|
130
|
+
|
|
131
|
+
The text schema filters turn a JSON-Schema into readable text, e.g.
|
|
121
132
|
`{{ content.schema | oapi_schema }}` produces:
|
|
122
133
|
|
|
123
134
|
```typescript
|
|
@@ -127,15 +138,26 @@ The two schema filters turn a JSON-Schema into readable text, e.g.
|
|
|
127
138
|
}
|
|
128
139
|
```
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
`oapi_html_schema` instead emits semantic, class-annotated HTML (`<dl>`/`<ul>`,
|
|
142
|
+
never tables) in which a `$ref` to a named component schema renders as a link to
|
|
143
|
+
`#schema-<name>` rather than being inlined — so operation bodies stay compact,
|
|
144
|
+
reused types are cross-linked, and recursive schemas terminate. Descriptions are
|
|
145
|
+
rendered as markdown when run inside Jekyll (it reuses the site's configured
|
|
146
|
+
converter) and as escaped plain text otherwise. Pair it with
|
|
147
|
+
`oapi_schemas` + `oapi_schema_anchor` to emit a matching "Models" section; see
|
|
148
|
+
[examples/jekyll/_includes/oapi/models.html](examples/jekyll/_includes/oapi/models.html)
|
|
149
|
+
and [schema.html](examples/jekyll/_includes/oapi/schema.html) (which picks a
|
|
150
|
+
renderer by content type), styled by
|
|
151
|
+
[_sass/oapi-schema.scss](examples/jekyll/_sass/oapi-schema.scss).
|
|
132
152
|
|
|
133
153
|
## Error handling
|
|
134
154
|
|
|
135
155
|
The library never raises on malformed input: it warns through
|
|
136
156
|
`JekyllOpenAPI.logger` (in Jekyll, routed to Jekyll's own logger) and degrades
|
|
137
|
-
gracefully — unresolvable
|
|
138
|
-
|
|
157
|
+
gracefully — unresolvable and external `$ref`s are left in place and warn only
|
|
158
|
+
when followed; recursive schemas terminate (rendered as a link, or their name in
|
|
159
|
+
the text renderers); invalid schemas render as `""`. Your build never breaks on
|
|
160
|
+
a bad spec.
|
|
139
161
|
|
|
140
162
|
## Compliance notes
|
|
141
163
|
|
|
@@ -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,7 +9,10 @@ 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
17
|
HTTP_METHODS = %w[get put post delete options head patch trace].freeze
|
|
15
18
|
PATH_ITEM_FIELDS = %w[summary description servers parameters $ref].freeze
|
|
@@ -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,11 +112,22 @@ 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
119
|
path_item.filter_map do |key, operation|
|
|
83
120
|
next if PATH_ITEM_FIELDS.include?(key)
|
|
84
121
|
next unless HTTP_METHODS.include?(key) || key.start_with?("x-")
|
|
122
|
+
operation = follow(operation)
|
|
85
123
|
next unless operation.is_a?(Hash)
|
|
124
|
+
# `x-` is OpenAPI's extension mechanism, not a method namespace: a
|
|
125
|
+
# path-item extension is only an operation when it actually carries an
|
|
126
|
+
# Operation Object. `operationId`/`responses` mark one; vendor metadata
|
|
127
|
+
# (x-internal, x-codegen, …) has neither and must not become a phantom
|
|
128
|
+
# method. Standard HTTP verbs are always operations.
|
|
129
|
+
next if key.start_with?("x-") &&
|
|
130
|
+
!(operation.key?("operationId") || operation.key?("responses"))
|
|
86
131
|
|
|
87
132
|
params = Parameters.merge(path_item["parameters"], operation["parameters"])
|
|
88
133
|
{
|
|
@@ -96,5 +141,10 @@ module JekyllOpenAPI
|
|
|
96
141
|
end
|
|
97
142
|
end
|
|
98
143
|
end
|
|
144
|
+
|
|
145
|
+
# Resolve a $ref used where a mapping (path item / operation) is expected.
|
|
146
|
+
def follow(node)
|
|
147
|
+
node.is_a?(Reference) ? node.resolve : node
|
|
148
|
+
end
|
|
99
149
|
end
|
|
100
150
|
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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require_relative "reference"
|
|
2
|
+
|
|
3
|
+
module JekyllOpenAPI
|
|
4
|
+
##
|
|
5
|
+
# Normalizes a Response object's `headers` map into a flat, ordered list —
|
|
6
|
+
# the Header-object counterpart to Parameters.group. `$ref`'d headers read
|
|
7
|
+
# transparently through their Reference, so the caller never special-cases them.
|
|
8
|
+
# @see https://spec.openapis.org/oas/v3.1.0.html#header-object
|
|
9
|
+
module Headers
|
|
10
|
+
# @param response [Hash, Reference] a Response object
|
|
11
|
+
# @return [Array<Hash>] {name, description, required, deprecated, schema}
|
|
12
|
+
def self.list(response)
|
|
13
|
+
map = (response.is_a?(Hash) || response.is_a?(Reference)) ? response["headers"] : nil
|
|
14
|
+
return [] unless map.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
map.map do |name, header|
|
|
17
|
+
{
|
|
18
|
+
"name" => name,
|
|
19
|
+
"description" => header["description"],
|
|
20
|
+
"required" => header["required"] || false,
|
|
21
|
+
"deprecated" => header["deprecated"] || false,
|
|
22
|
+
"schema" => header["schema"],
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "reference"
|
|
2
|
+
|
|
1
3
|
module JekyllOpenAPI
|
|
2
4
|
##
|
|
3
5
|
# Helpers for OpenAPI Parameter objects.
|
|
@@ -5,6 +7,12 @@ module JekyllOpenAPI
|
|
|
5
7
|
module Parameters
|
|
6
8
|
LOCATIONS = %w[query path header cookie].freeze
|
|
7
9
|
|
|
10
|
+
# A parameter may arrive inline (Hash) or as a $ref (Reference). Both read
|
|
11
|
+
# like a mapping, so both are valid parameter objects here.
|
|
12
|
+
def self.mapping?(param)
|
|
13
|
+
param.is_a?(Hash) || param.is_a?(Reference)
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
##
|
|
9
17
|
# Groups a parameter list by location ("in").
|
|
10
18
|
# Always returns all four location keys so templates can test emptiness.
|
|
@@ -13,7 +21,7 @@ module JekyllOpenAPI
|
|
|
13
21
|
def self.group(params, logger: JekyllOpenAPI.logger)
|
|
14
22
|
output = LOCATIONS.to_h { |location| [location, []] }
|
|
15
23
|
Array(params).each do |param|
|
|
16
|
-
location =
|
|
24
|
+
location = mapping?(param) ? param["in"] : nil
|
|
17
25
|
if output.key?(location)
|
|
18
26
|
output[location] << param
|
|
19
27
|
else
|
|
@@ -30,7 +38,7 @@ module JekyllOpenAPI
|
|
|
30
38
|
def self.merge(path_params, operation_params)
|
|
31
39
|
merged = {}
|
|
32
40
|
(Array(path_params) + Array(operation_params)).each do |param|
|
|
33
|
-
next unless
|
|
41
|
+
next unless mapping?(param)
|
|
34
42
|
merged[[param["name"], param["in"]]] = param
|
|
35
43
|
end
|
|
36
44
|
merged.values
|