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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 989a754db0f9b16a6ddcdc250248e360400b0f7e1cee52c29879f666611036e9
4
- data.tar.gz: 1421051366aff8e848f3c451b966a128132ae001e26698e5ea22a99eb9b9afb1
3
+ metadata.gz: 115a158c4677920431b493e1d8941d8bc84f7ac71f0d9813f386ed9a1391878d
4
+ data.tar.gz: 2444e2822f9181d34c4ade929b0903861d43ea6b9a9376db09baef3abe779906
5
5
  SHA512:
6
- metadata.gz: d1a17b202abcf9defa2200d5c99df7dc8f21f563e0b8166bd5cadc605d91f48a4c56ba18ec37af4eb15f1c3fbf49022bb7e209281a457f7c172bc06b903c9905
7
- data.tar.gz: 70aa7e5ad448fc234a1d4c13e66c5cf0cf5ad06d7a8a236e28b90279578b5f36ef74ea316c9f7f05c811aeb4ac363cf0aba91050b4623b1bc570da124b83ddc6
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 | 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.
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
- See [examples/jekyll/_includes/oapi/schema.html](examples/jekyll/_includes/oapi/schema.html)
131
- for choosing the renderer by content type.
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, external, and circular `$ref`s are left in place;
138
- invalid schemas render as `""`. Your build never breaks on a bad spec.
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
- # 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,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
- # 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
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 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,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
- # 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
@@ -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 = param.is_a?(Hash) ? param["in"] : nil
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 param.is_a?(Hash)
41
+ next unless mapping?(param)
34
42
  merged[[param["name"], param["in"]]] = param
35
43
  end
36
44
  merged.values