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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 989a754db0f9b16a6ddcdc250248e360400b0f7e1cee52c29879f666611036e9
4
- data.tar.gz: 1421051366aff8e848f3c451b966a128132ae001e26698e5ea22a99eb9b9afb1
3
+ metadata.gz: 7c592a43f900f36f1afbfc4432c1dc6b9e55302ec6e481bc6284596a728c36ea
4
+ data.tar.gz: 047ef58c76751ad0cc91415d838dc367ea302f83b4f88fa95f430397db3563b3
5
5
  SHA512:
6
- metadata.gz: d1a17b202abcf9defa2200d5c99df7dc8f21f563e0b8166bd5cadc605d91f48a4c56ba18ec37af4eb15f1c3fbf49022bb7e209281a457f7c172bc06b903c9905
7
- data.tar.gz: 70aa7e5ad448fc234a1d4c13e66c5cf0cf5ad06d7a8a236e28b90279578b5f36ef74ea316c9f7f05c811aeb4ac363cf0aba91050b4623b1bc570da124b83ddc6
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, filters by tag,
79
- and merges path-level parameters into each operation.
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 | input → output |
111
- |-------------------------|----------------|
112
- | `oapi_dereference` | document → document with local `$ref`s resolved |
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_schema` | JSON-SchemaTypeScript-like string |
118
- | `oapi_xml_schema` | JSON-Schemasimplified XML string |
119
-
120
- The two schema filters turn a JSON-Schema into readable text, e.g.
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
- See [examples/jekyll/_includes/oapi/schema.html](examples/jekyll/_includes/oapi/schema.html)
131
- for choosing the renderer by content type.
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, external, and circular `$ref`s are left in place;
138
- invalid schemas render as `""`. Your build never breaks on a bad spec.
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`); when the gem
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
- # Recursively resolves local `$ref` pointers in an OpenAPI document.
6
- # External references (other files, URLs) are left in place with a warning.
7
- # Circular references are detected and left in place instead of recursing forever.
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 local $ref replaced by its target
19
- def dereference(node = @root, stack = [])
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
- resolve(node, stack)
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| dereference(value, stack) }
40
+ node.transform_values { |value| link(value, context) }
26
41
  end
27
42
  when Array
28
- node.map { |value| dereference(value, stack) }
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
- # and merging path-level with operation-level parameters.
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 with all local $refs resolved (memoized)
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.filter_map do |key, operation|
83
- next if PATH_ITEM_FIELDS.include?(key)
84
- next unless HTTP_METHODS.include?(key) || key.start_with?("x-")
85
- next unless operation.is_a?(Hash)
86
-
87
- params = Parameters.merge(path_item["parameters"], operation["parameters"])
88
- {
89
- "path" => path,
90
- "method" => key.delete_prefix("x-"),
91
- "raw_method" => key,
92
- "operation" => operation,
93
- "parameters" => params,
94
- "grouped_parameters" => Parameters.group(params, logger: @logger),
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
- # Resolve every local $ref in an OpenAPI document (circular-safe)
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.object_id] ||= Document.new(input)
130
+ @oapi_documents ||= {}.compare_by_identity
131
+ @oapi_documents[input] ||= Document.new(input)
55
132
  end
56
133
  end
57
134
  end