jekyll-openapi 0.1.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 +7 -0
- data/README.md +163 -0
- data/lib/jekyll-openapi.rb +9 -0
- data/lib/jekyll_openapi/dereferencer.rb +63 -0
- data/lib/jekyll_openapi/document.rb +100 -0
- data/lib/jekyll_openapi/filters.rb +57 -0
- data/lib/jekyll_openapi/jekyll_plugin.rb +30 -0
- data/lib/jekyll_openapi/json_pointer.rb +33 -0
- data/lib/jekyll_openapi/parameters.rb +39 -0
- data/lib/jekyll_openapi/schema_renderer/typescript.rb +108 -0
- data/lib/jekyll_openapi/schema_renderer/xml.rb +111 -0
- data/lib/jekyll_openapi/version.rb +3 -0
- data/lib/jekyll_openapi.rb +26 -0
- metadata +100 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 989a754db0f9b16a6ddcdc250248e360400b0f7e1cee52c29879f666611036e9
|
|
4
|
+
data.tar.gz: 1421051366aff8e848f3c451b966a128132ae001e26698e5ea22a99eb9b9afb1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d1a17b202abcf9defa2200d5c99df7dc8f21f563e0b8166bd5cadc605d91f48a4c56ba18ec37af4eb15f1c3fbf49022bb7e209281a457f7c172bc06b903c9905
|
|
7
|
+
data.tar.gz: 70aa7e5ad448fc234a1d4c13e66c5cf0cf5ad06d7a8a236e28b90279578b5f36ef74ea316c9f7f05c811aeb4ac363cf0aba91050b4623b1bc570da124b83ddc6
|
data/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# jekyll-openapi
|
|
2
|
+
|
|
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).
|
|
5
|
+
|
|
6
|
+
It deliberately does **not** render HTML. The gem provides:
|
|
7
|
+
|
|
8
|
+
- a navigable **object model** over a parsed OpenAPI document — dereferencing,
|
|
9
|
+
tag and operation enumeration, parameter merging;
|
|
10
|
+
- **schema-to-text renderers** (TypeScript-like and simplified XML) exposed as
|
|
11
|
+
opt-in Liquid filters;
|
|
12
|
+
|
|
13
|
+
…and your site provides the templates. See [examples/](examples/) for complete,
|
|
14
|
+
copy-paste-able templates.
|
|
15
|
+
|
|
16
|
+
> The gem works as a plain Ruby library too — without Jekyll or Liquid. That
|
|
17
|
+
> usage, the object model, and the renderer API are documented in
|
|
18
|
+
> [examples/ruby/README.md](examples/ruby/README.md).
|
|
19
|
+
|
|
20
|
+
## Jekyll integration
|
|
21
|
+
|
|
22
|
+
Add the gem to your `Gemfile` and list it as a plugin — nothing else to do,
|
|
23
|
+
the filters register themselves and warnings are routed to Jekyll's logger:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# Gemfile
|
|
27
|
+
group :plugins do
|
|
28
|
+
gem "jekyll-openapi"
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# _config.yml
|
|
34
|
+
plugins:
|
|
35
|
+
- jekyll-openapi
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Walkthrough: a minimal API page
|
|
39
|
+
|
|
40
|
+
Four steps take you from a spec file to a rendered reference page.
|
|
41
|
+
|
|
42
|
+
**1. Enable the plugin** as shown above (`Gemfile` + `plugins:` entry). No
|
|
43
|
+
`_plugins/` file is needed.
|
|
44
|
+
|
|
45
|
+
**2. Drop your spec into `_data/`** so Jekyll parses it into `site.data`:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
_data/openapi.yml # YAML or JSON; becomes site.data.openapi
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**3. Write a page** that walks the tags and operations with the filters:
|
|
52
|
+
|
|
53
|
+
```liquid
|
|
54
|
+
---
|
|
55
|
+
title: API Reference
|
|
56
|
+
---
|
|
57
|
+
{% assign api = site.data.openapi %}
|
|
58
|
+
|
|
59
|
+
<h1>{{ api.info.title }}</h1>
|
|
60
|
+
|
|
61
|
+
{% assign tags = api | oapi_tags %}
|
|
62
|
+
{% for tag in tags %}
|
|
63
|
+
<h2>{{ tag.name }}</h2>
|
|
64
|
+
{{ tag.description | markdownify }}
|
|
65
|
+
|
|
66
|
+
{% assign ops = api | oapi_operations: tag.name %}
|
|
67
|
+
{% for op in ops %}
|
|
68
|
+
<h3>{{ op.method | upcase }} {{ op.path }}</h3>
|
|
69
|
+
{{ op.operation.description | markdownify }}
|
|
70
|
+
{% endfor %}
|
|
71
|
+
{% endfor %}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**4. Build** (`bundle exec jekyll serve`) and you have a page listing every
|
|
75
|
+
operation, grouped by tag. That's a complete, working integration.
|
|
76
|
+
|
|
77
|
+
`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
|
+
|
|
81
|
+
Each `op` in the loop is a plain Hash you can traverse further:
|
|
82
|
+
|
|
83
|
+
| key | content |
|
|
84
|
+
|------------------------|------------------------------------------------------|
|
|
85
|
+
| `"path"` | `"/pets/{petId}"` |
|
|
86
|
+
| `"method"` | normalized method, `x-` prefix stripped (`"mkcol"`) |
|
|
87
|
+
| `"raw_method"` | the literal key (`"x-mkcol"`) |
|
|
88
|
+
| `"operation"` | the Operation object, dereferenced |
|
|
89
|
+
| `"parameters"` | path-level + operation-level parameters, merged (operation wins on same name/location) |
|
|
90
|
+
| `"grouped_parameters"` | same, grouped: `{"query" => [...], "path" => [...], "header" => [...], "cookie" => [...]}` |
|
|
91
|
+
|
|
92
|
+
### Going further
|
|
93
|
+
|
|
94
|
+
The walkthrough above is intentionally bare. For a production-quality page —
|
|
95
|
+
tag index/navigation, parameter tables, request/response bodies with rendered
|
|
96
|
+
schemas — start from the complete examples and restyle them:
|
|
97
|
+
|
|
98
|
+
- [examples/jekyll/api-doc.html](examples/jekyll/api-doc.html) — a full API
|
|
99
|
+
reference page: tag cloud, operations grouped by tag, path/query parameter
|
|
100
|
+
tables, request and response schemas.
|
|
101
|
+
- [examples/jekyll/_includes/oapi/schema.html](examples/jekyll/_includes/oapi/schema.html)
|
|
102
|
+
— a reusable include that picks the XML or TypeScript renderer based on the
|
|
103
|
+
content type.
|
|
104
|
+
|
|
105
|
+
## Liquid filters
|
|
106
|
+
|
|
107
|
+
Registered automatically in Jekyll. (In a plain Liquid setup you register them
|
|
108
|
+
yourself — see [examples/ruby/README.md](examples/ruby/README.md#liquid-filters-outside-jekyll).)
|
|
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-Schema → TypeScript-like string |
|
|
118
|
+
| `oapi_xml_schema` | JSON-Schema → simplified XML string |
|
|
119
|
+
|
|
120
|
+
The two schema filters turn a JSON-Schema into readable text, e.g.
|
|
121
|
+
`{{ content.schema | oapi_schema }}` produces:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
{
|
|
125
|
+
name: "string"
|
|
126
|
+
age?: integer
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
See [examples/jekyll/_includes/oapi/schema.html](examples/jekyll/_includes/oapi/schema.html)
|
|
131
|
+
for choosing the renderer by content type.
|
|
132
|
+
|
|
133
|
+
## Error handling
|
|
134
|
+
|
|
135
|
+
The library never raises on malformed input: it warns through
|
|
136
|
+
`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.
|
|
139
|
+
|
|
140
|
+
## Compliance notes
|
|
141
|
+
|
|
142
|
+
Intentional simplifications, in the spirit of "readable docs over exhaustive
|
|
143
|
+
fidelity":
|
|
144
|
+
|
|
145
|
+
- Only **local** (`#/...`) references are resolved; external files/URLs are left as-is.
|
|
146
|
+
- The TypeScript renderer supports `oneOf`/`anyOf` (`|`), `allOf` (`&`),
|
|
147
|
+
3.1 type arrays (`type: [string, "null"]`), enums, `additionalProperties`,
|
|
148
|
+
and infers `object`/`array` when `type` is omitted — but does not merge
|
|
149
|
+
`allOf` members into a single object literal.
|
|
150
|
+
- The XML renderer honors `xml.name`, `xml.prefix`, `xml.namespace`,
|
|
151
|
+
`xml.attribute` and `xml.wrapped`, and renders example values where provided.
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
rake test # or: ruby -Ilib -Itest test/test_<name>.rb
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Tests run against generic fixtures (`test/fixtures/petstore.yml`); when the gem
|
|
160
|
+
sources live inside the eCorpus_doc repository, an extra integration suite runs
|
|
161
|
+
the whole pipeline over the real eCorpus API definition.
|
|
162
|
+
</content>
|
|
163
|
+
</invoke>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Gem entry point, matching the gem name so Jekyll's plugin manager can
|
|
3
|
+
# `require "jekyll-openapi"` directly from a `plugins:` config entry.
|
|
4
|
+
require_relative "jekyll_openapi"
|
|
5
|
+
|
|
6
|
+
# When loaded inside Jekyll, install the filters and logger automatically.
|
|
7
|
+
# Outside Jekyll this is a no-op: register filters yourself with
|
|
8
|
+
# Liquid::Template.register_filter(JekyllOpenAPI::Filters).
|
|
9
|
+
require_relative "jekyll_openapi/jekyll_plugin" if defined?(::Jekyll)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require_relative "json_pointer"
|
|
2
|
+
|
|
3
|
+
module JekyllOpenAPI
|
|
4
|
+
##
|
|
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.
|
|
8
|
+
class Dereferencer
|
|
9
|
+
def initialize(root, logger: JekyllOpenAPI.logger)
|
|
10
|
+
@root = root
|
|
11
|
+
@logger = logger
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.dereference(root, logger: JekyllOpenAPI.logger)
|
|
15
|
+
new(root, logger: logger).dereference
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return a deep copy of the document with every local $ref replaced by its target
|
|
19
|
+
def dereference(node = @root, stack = [])
|
|
20
|
+
case node
|
|
21
|
+
when Hash
|
|
22
|
+
if node.key?("$ref")
|
|
23
|
+
resolve(node, stack)
|
|
24
|
+
else
|
|
25
|
+
node.transform_values { |value| dereference(value, stack) }
|
|
26
|
+
end
|
|
27
|
+
when Array
|
|
28
|
+
node.map { |value| dereference(value, stack) }
|
|
29
|
+
else
|
|
30
|
+
node
|
|
31
|
+
end
|
|
32
|
+
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
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "dereferencer"
|
|
4
|
+
require_relative "parameters"
|
|
5
|
+
|
|
6
|
+
module JekyllOpenAPI
|
|
7
|
+
##
|
|
8
|
+
# Navigable object model over a parsed OpenAPI document.
|
|
9
|
+
#
|
|
10
|
+
# Absorbs the spec-aware traversal that would otherwise live in templates:
|
|
11
|
+
# enumerating operations (including `x-` extension methods), filtering by tag,
|
|
12
|
+
# and merging path-level with operation-level parameters.
|
|
13
|
+
class Document
|
|
14
|
+
HTTP_METHODS = %w[get put post delete options head patch trace].freeze
|
|
15
|
+
PATH_ITEM_FIELDS = %w[summary description servers parameters $ref].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :data
|
|
18
|
+
|
|
19
|
+
# @param data [Hash] a parsed OpenAPI document (dereferenced or not)
|
|
20
|
+
def initialize(data, logger: JekyllOpenAPI.logger)
|
|
21
|
+
@data = data
|
|
22
|
+
@logger = logger
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Convenience loader for YAML or JSON files
|
|
26
|
+
def self.load_file(path, logger: JekyllOpenAPI.logger)
|
|
27
|
+
data = if File.extname(path) == ".json"
|
|
28
|
+
JSON.parse(File.read(path))
|
|
29
|
+
else
|
|
30
|
+
YAML.safe_load_file(path, aliases: true)
|
|
31
|
+
end
|
|
32
|
+
new(data, logger: logger)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Hash] the document with all local $refs resolved (memoized)
|
|
36
|
+
def dereferenced
|
|
37
|
+
@dereferenced ||= Dereferencer.new(@data, logger: @logger).dereference
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def info
|
|
41
|
+
dereferenced["info"] || {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def tags
|
|
45
|
+
dereferenced["tags"] || []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def paths
|
|
49
|
+
dereferenced["paths"] || {}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Flat list of every operation in the document, in path order.
|
|
54
|
+
# `x-`-prefixed extension methods (e.g. webDAV verbs) are included,
|
|
55
|
+
# with the prefix stripped in "method" and kept in "raw_method".
|
|
56
|
+
#
|
|
57
|
+
# Each entry is a string-keyed Hash (Liquid-friendly):
|
|
58
|
+
# "path" [String] e.g. "/scenes/{scene}"
|
|
59
|
+
# "method" [String] e.g. "get" or "mkcol"
|
|
60
|
+
# "raw_method" [String] e.g. "get" or "x-mkcol"
|
|
61
|
+
# "operation" [Hash] the Operation object
|
|
62
|
+
# "parameters" [Array] path-level and operation-level parameters, merged
|
|
63
|
+
# "grouped_parameters" [Hash] same, grouped by location (see Parameters.group)
|
|
64
|
+
#
|
|
65
|
+
# @param tag [String, nil] only operations tagged with this tag
|
|
66
|
+
def operations(tag: nil)
|
|
67
|
+
@operations ||= build_operations
|
|
68
|
+
return @operations unless tag
|
|
69
|
+
@operations.select { |op| Array(op["operation"]["tags"]).include?(tag) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Hash{String => Array}] tag name => operations, following the tags declaration order
|
|
73
|
+
def operations_by_tag
|
|
74
|
+
tags.to_h { |t| [t["name"], operations(tag: t["name"])] }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def build_operations
|
|
80
|
+
paths.flat_map do |path, path_item|
|
|
81
|
+
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
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module JekyllOpenAPI
|
|
2
|
+
##
|
|
3
|
+
# Liquid filters over the JekyllOpenAPI model and renderers.
|
|
4
|
+
#
|
|
5
|
+
# This module has no dependency on the liquid gem itself: it is a plain
|
|
6
|
+
# module of filter methods. Consumers opt in with:
|
|
7
|
+
#
|
|
8
|
+
# Liquid::Template.register_filter(JekyllOpenAPI::Filters)
|
|
9
|
+
#
|
|
10
|
+
# or register only a subset by wrapping the methods they want.
|
|
11
|
+
module Filters
|
|
12
|
+
# Resolve every local $ref in an OpenAPI document (circular-safe)
|
|
13
|
+
def oapi_dereference(input)
|
|
14
|
+
Dereferencer.new(input).dereference
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Group a parameter list by location: {"query" => [...], "path" => [...], ...}
|
|
18
|
+
def oapi_parameters(input)
|
|
19
|
+
Parameters.group(input)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Merge path-level and operation-level parameter lists (operation wins)
|
|
23
|
+
def oapi_merge_parameters(path_params, operation_params)
|
|
24
|
+
Parameters.merge(path_params, operation_params)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Flat operation list from a full document, optionally filtered by tag.
|
|
28
|
+
# See Document#operations for the shape of each entry.
|
|
29
|
+
def oapi_operations(input, tag = nil)
|
|
30
|
+
document(input).operations(tag: tag)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Tag list of a full document
|
|
34
|
+
def oapi_tags(input)
|
|
35
|
+
document(input).tags
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Render a JSON-Schema as a TypeScript-like string
|
|
39
|
+
def oapi_schema(input)
|
|
40
|
+
SchemaRenderer::TypeScript.render(input)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Render a JSON-Schema as a simplified XML example
|
|
44
|
+
def oapi_xml_schema(input)
|
|
45
|
+
SchemaRenderer::XML.render(input)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Memoize per input so several filter calls on the same document
|
|
51
|
+
# (tags, then operations per tag) only dereference it once.
|
|
52
|
+
def document(input)
|
|
53
|
+
@oapi_documents ||= {}
|
|
54
|
+
@oapi_documents[input.object_id] ||= Document.new(input)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative "../jekyll_openapi"
|
|
2
|
+
|
|
3
|
+
module JekyllOpenAPI
|
|
4
|
+
##
|
|
5
|
+
# Jekyll integration: registers the Liquid filters and routes warnings to
|
|
6
|
+
# Jekyll's logger. Loaded automatically when the gem is required inside a
|
|
7
|
+
# Jekyll site (see lib/jekyll-openapi.rb); kept separate so the core
|
|
8
|
+
# library stays usable and testable without Jekyll.
|
|
9
|
+
module JekyllPlugin
|
|
10
|
+
class LoggerAdapter
|
|
11
|
+
def warn(message)
|
|
12
|
+
::Jekyll.logger.warn("OpenAPI:", message)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.register
|
|
17
|
+
JekyllOpenAPI.logger = LoggerAdapter.new
|
|
18
|
+
# Liquid 5.x moved filter registration onto the Environment; the
|
|
19
|
+
# Template method still works but warns. Fall back to it on Liquid 4.x,
|
|
20
|
+
# which has no Environment.
|
|
21
|
+
if defined?(::Liquid::Environment)
|
|
22
|
+
::Liquid::Environment.default.register_filter(JekyllOpenAPI::Filters)
|
|
23
|
+
else
|
|
24
|
+
::Liquid::Template.register_filter(JekyllOpenAPI::Filters)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
JekyllOpenAPI::JekyllPlugin.register
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module JekyllOpenAPI
|
|
2
|
+
##
|
|
3
|
+
# RFC 6901 JSON Pointer resolution.
|
|
4
|
+
module JSONPointer
|
|
5
|
+
##
|
|
6
|
+
# Resolves a JSON Pointer against a document.
|
|
7
|
+
# @param doc [Hash, Array] root document
|
|
8
|
+
# @param pointer [String] e.g. "/components/schemas/Scene"
|
|
9
|
+
# @return [Object, nil] the referenced value, or nil if not found
|
|
10
|
+
def self.resolve(doc, pointer)
|
|
11
|
+
return doc if pointer.nil? || pointer.empty?
|
|
12
|
+
return nil unless pointer.start_with?("/")
|
|
13
|
+
|
|
14
|
+
pointer.split("/", -1).drop(1).reduce(doc) do |node, token|
|
|
15
|
+
token = unescape(token)
|
|
16
|
+
case node
|
|
17
|
+
when Hash
|
|
18
|
+
node.fetch(token) { return nil }
|
|
19
|
+
when Array
|
|
20
|
+
return nil unless /\A\d+\z/.match?(token)
|
|
21
|
+
node.fetch(Integer(token)) { return nil }
|
|
22
|
+
else
|
|
23
|
+
return nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# "~1" then "~0", in that order (RFC 6901 §4)
|
|
29
|
+
def self.unescape(token)
|
|
30
|
+
token.gsub("~1", "/").gsub("~0", "~")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module JekyllOpenAPI
|
|
2
|
+
##
|
|
3
|
+
# Helpers for OpenAPI Parameter objects.
|
|
4
|
+
# @see https://spec.openapis.org/oas/v3.1.0.html#parameter-object
|
|
5
|
+
module Parameters
|
|
6
|
+
LOCATIONS = %w[query path header cookie].freeze
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# Groups a parameter list by location ("in").
|
|
10
|
+
# Always returns all four location keys so templates can test emptiness.
|
|
11
|
+
# @param params [Array<Hash>, nil]
|
|
12
|
+
# @return [Hash{String => Array<Hash>}]
|
|
13
|
+
def self.group(params, logger: JekyllOpenAPI.logger)
|
|
14
|
+
output = LOCATIONS.to_h { |location| [location, []] }
|
|
15
|
+
Array(params).each do |param|
|
|
16
|
+
location = param.is_a?(Hash) ? param["in"] : nil
|
|
17
|
+
if output.key?(location)
|
|
18
|
+
output[location] << param
|
|
19
|
+
else
|
|
20
|
+
logger.warn("invalid parameter location #{location.inspect} in #{param.inspect}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
output
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Merges path-item level and operation level parameter lists.
|
|
28
|
+
# An operation parameter overrides a path one with the same (name, in) pair.
|
|
29
|
+
# @return [Array<Hash>]
|
|
30
|
+
def self.merge(path_params, operation_params)
|
|
31
|
+
merged = {}
|
|
32
|
+
(Array(path_params) + Array(operation_params)).each do |param|
|
|
33
|
+
next unless param.is_a?(Hash)
|
|
34
|
+
merged[[param["name"], param["in"]]] = param
|
|
35
|
+
end
|
|
36
|
+
merged.values
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module JekyllOpenAPI
|
|
2
|
+
module SchemaRenderer
|
|
3
|
+
##
|
|
4
|
+
# Renders a JSON-Schema as a simplified TypeScript-like textual representation.
|
|
5
|
+
# @see https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00
|
|
6
|
+
class TypeScript
|
|
7
|
+
def initialize(logger: JekyllOpenAPI.logger)
|
|
8
|
+
@logger = logger
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.render(schema, logger: JekyllOpenAPI.logger)
|
|
12
|
+
new(logger: logger).render(schema)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param input [Hash] a JSON-Schema
|
|
16
|
+
# @param prefix [String] current indentation
|
|
17
|
+
# @return [String]
|
|
18
|
+
def render(input, prefix = "")
|
|
19
|
+
unless input.is_a?(Hash)
|
|
20
|
+
@logger.warn("not a valid schema object: #{input.inspect}")
|
|
21
|
+
return ""
|
|
22
|
+
end
|
|
23
|
+
str = base(input, prefix)
|
|
24
|
+
return "" if str.nil?
|
|
25
|
+
annotate(str, input)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def base(input, prefix)
|
|
31
|
+
return union(input["oneOf"], prefix, " | ") if input["oneOf"]
|
|
32
|
+
return union(input["anyOf"], prefix, " | ") if input["anyOf"]
|
|
33
|
+
return union(input["allOf"], prefix, " & ") if input["allOf"]
|
|
34
|
+
|
|
35
|
+
type = input["type"]
|
|
36
|
+
return type_union(input, prefix) if type.is_a?(Array)
|
|
37
|
+
# infer the type when omitted, as most tooling does
|
|
38
|
+
type ||= "object" if input["properties"] || input["additionalProperties"]
|
|
39
|
+
type ||= "array" if input["items"]
|
|
40
|
+
type ||= "string" if input["enum"]
|
|
41
|
+
|
|
42
|
+
case type
|
|
43
|
+
when "object" then object(input, prefix)
|
|
44
|
+
when "array" then "[ #{render(input["items"], prefix)} ]"
|
|
45
|
+
when "string" then string(input)
|
|
46
|
+
when "null" then "null"
|
|
47
|
+
when nil
|
|
48
|
+
@logger.warn("not a valid schema object: #{input.inspect}")
|
|
49
|
+
nil
|
|
50
|
+
else
|
|
51
|
+
primitive(input, type)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def union(schemas, prefix, separator)
|
|
56
|
+
schemas.map { |schema| render(schema, prefix) }.join(separator)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# OpenAPI 3.1 / JSON-Schema type arrays, e.g. type: [string, "null"]
|
|
60
|
+
def type_union(input, prefix)
|
|
61
|
+
types = input["type"]
|
|
62
|
+
rendered = (types - ["null"]).map { |t| base(input.merge("type" => t), prefix) }
|
|
63
|
+
rendered.push("null") if types.include?("null")
|
|
64
|
+
rendered.join(" | ")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def object(input, prefix)
|
|
68
|
+
str = "{"
|
|
69
|
+
(input["properties"] || {}).each do |key, schema|
|
|
70
|
+
required = Array(input["required"]).include?(key)
|
|
71
|
+
str += "\n#{prefix} #{key}#{required ? "" : "?"}: #{render(schema, prefix + " ")}"
|
|
72
|
+
end
|
|
73
|
+
props = input["additionalProperties"]
|
|
74
|
+
if props.is_a?(Hash)
|
|
75
|
+
str += "\n#{prefix} [#{props["type"]}]?: #{render(props, prefix + " ")}"
|
|
76
|
+
end
|
|
77
|
+
"#{str}\n#{prefix}}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def string(input)
|
|
81
|
+
if input["enum"]
|
|
82
|
+
input["enum"].map { |v| "\"#{v}\"" }.join("|")
|
|
83
|
+
elsif input["format"] == "binary"
|
|
84
|
+
"Buffer"
|
|
85
|
+
else
|
|
86
|
+
"\"string\""
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def primitive(input, type)
|
|
91
|
+
str = type.to_s
|
|
92
|
+
str += " (#{input["format"]})" if input["format"]
|
|
93
|
+
str
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def annotate(str, input)
|
|
97
|
+
str = "#{str} // /#{input["pattern"]}/" if input["pattern"]
|
|
98
|
+
if input["summary"]
|
|
99
|
+
str = "#{str} //#{input["summary"]}"
|
|
100
|
+
elsif input["description"]
|
|
101
|
+
padding = "".rjust(str.length, " ")
|
|
102
|
+
str = "#{str} //#{input["description"].split("\n").join("\n#{padding} //")}"
|
|
103
|
+
end
|
|
104
|
+
str
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
module JekyllOpenAPI
|
|
2
|
+
module SchemaRenderer
|
|
3
|
+
##
|
|
4
|
+
# Renders a JSON-Schema as a simplified XML representation of the object,
|
|
5
|
+
# honoring the OpenAPI `xml` metadata object (name, prefix, namespace,
|
|
6
|
+
# attribute, wrapped).
|
|
7
|
+
# @see https://spec.openapis.org/oas/v3.1.0.html#xml-object
|
|
8
|
+
class XML
|
|
9
|
+
def initialize(logger: JekyllOpenAPI.logger)
|
|
10
|
+
@logger = logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.render(schema, logger: JekyllOpenAPI.logger)
|
|
14
|
+
new(logger: logger).render(schema)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param input [Hash] a JSON-Schema
|
|
18
|
+
# @param prefix [String] current indentation
|
|
19
|
+
# @param name [String] element name when the schema has no xml.name
|
|
20
|
+
# @return [String]
|
|
21
|
+
def render(input, prefix = "", name = "xml")
|
|
22
|
+
unless input.is_a?(Hash) && (input["type"] || input["properties"] || input["items"])
|
|
23
|
+
@logger.warn("not a valid schema object: #{input.inspect}")
|
|
24
|
+
return ""
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
xml_def = input["xml"] || {}
|
|
28
|
+
name = xml_def["name"] || name
|
|
29
|
+
name = "#{xml_def["prefix"]}:#{name}" if xml_def["prefix"]
|
|
30
|
+
attrs = {}
|
|
31
|
+
if xml_def["namespace"]
|
|
32
|
+
ns_attr = xml_def["prefix"] ? "xmlns:#{xml_def["prefix"]}" : "xmlns"
|
|
33
|
+
attrs[ns_attr] = xml_def["namespace"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
type = input["type"]
|
|
37
|
+
type ||= "object" if input["properties"]
|
|
38
|
+
type ||= "array" if input["items"]
|
|
39
|
+
|
|
40
|
+
case type
|
|
41
|
+
when "array"
|
|
42
|
+
array(input, prefix, name, attrs, wrapped: xml_def["wrapped"])
|
|
43
|
+
when "object"
|
|
44
|
+
content = ""
|
|
45
|
+
(input["properties"] || {}).each do |key, schema|
|
|
46
|
+
if schema.is_a?(Hash) && schema.dig("xml", "attribute")
|
|
47
|
+
attrs[schema.dig("xml", "name") || key] = attribute_value(schema)
|
|
48
|
+
else
|
|
49
|
+
content += "\n#{render(schema, prefix + " ", key)}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
element(name, attrs, content, prefix)
|
|
53
|
+
when "string"
|
|
54
|
+
element(name, attrs, string_content(input), prefix)
|
|
55
|
+
else
|
|
56
|
+
content = type.to_s
|
|
57
|
+
content += comment(" (#{input["format"]})") if input["format"]
|
|
58
|
+
content += comment(input["summary"]) if input["summary"]
|
|
59
|
+
element(name, attrs, content, prefix)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def array(input, prefix, name, attrs, wrapped: false)
|
|
66
|
+
unless wrapped
|
|
67
|
+
return "#{render(input["items"], prefix, name)}\n#{prefix}#{comment("Array of <#{name}>")}"
|
|
68
|
+
end
|
|
69
|
+
item = render(input["items"], prefix + " ", singular(name))
|
|
70
|
+
content = "\n#{item}\n#{prefix + " "}#{comment("Array of <#{singular(name)}>")}"
|
|
71
|
+
element(name, attrs, content, prefix)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def element(name, attrs, content, prefix)
|
|
75
|
+
str_attrs = attrs.map { |key, value| " #{key}=\"#{value}\"" }.join
|
|
76
|
+
closing = content.include?("\n") ? "\n#{prefix}" : ""
|
|
77
|
+
"#{prefix}<#{name}#{str_attrs}>#{content}#{closing}</#{name}>"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def string_content(input)
|
|
81
|
+
content = if input["format"] == "binary"
|
|
82
|
+
"Buffer"
|
|
83
|
+
elsif input["example"]
|
|
84
|
+
input["example"].to_s
|
|
85
|
+
elsif input["examples"].is_a?(Hash) && !input["examples"].empty?
|
|
86
|
+
input["examples"].values.first["value"].to_s
|
|
87
|
+
else
|
|
88
|
+
"string"
|
|
89
|
+
end
|
|
90
|
+
if input["summary"]
|
|
91
|
+
content += " #{comment(input["summary"])}"
|
|
92
|
+
elsif input["pattern"]
|
|
93
|
+
content += comment("/#{input["pattern"]}/")
|
|
94
|
+
end
|
|
95
|
+
content
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def attribute_value(schema)
|
|
99
|
+
(schema["example"] || schema["type"] || "string").to_s
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def singular(name)
|
|
103
|
+
name.end_with?("s") && name.length > 1 ? name[0..-2] : "item"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def comment(str)
|
|
107
|
+
"<!-- #{str} -->"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require_relative "jekyll_openapi/version"
|
|
2
|
+
require_relative "jekyll_openapi/json_pointer"
|
|
3
|
+
require_relative "jekyll_openapi/dereferencer"
|
|
4
|
+
require_relative "jekyll_openapi/parameters"
|
|
5
|
+
require_relative "jekyll_openapi/document"
|
|
6
|
+
require_relative "jekyll_openapi/schema_renderer/typescript"
|
|
7
|
+
require_relative "jekyll_openapi/schema_renderer/xml"
|
|
8
|
+
require_relative "jekyll_openapi/filters"
|
|
9
|
+
|
|
10
|
+
module JekyllOpenAPI
|
|
11
|
+
# Default logger writes to stderr. Replace with any object responding to
|
|
12
|
+
# #warn(message), e.g. an adapter around Jekyll.logger.
|
|
13
|
+
class << self
|
|
14
|
+
attr_writer :logger
|
|
15
|
+
|
|
16
|
+
def logger
|
|
17
|
+
@logger ||= StderrLogger.new
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class StderrLogger
|
|
22
|
+
def warn(message)
|
|
23
|
+
Kernel.warn("JekyllOpenAPI: #{message}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jekyll-openapi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sebastien Dumetz
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: liquid
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '4.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
description: |
|
|
55
|
+
A (mostly) spec-compliant OpenAPI 3.1 toolkit for Jekyll and other
|
|
56
|
+
Liquid-based static site generators: a navigable document model
|
|
57
|
+
(dereferencing, operations, tags, parameters) and schema-to-text
|
|
58
|
+
renderers (TypeScript-like, XML), exposed as Liquid filters.
|
|
59
|
+
Self-registers when listed in a Jekyll site's plugins; usable as a
|
|
60
|
+
plain library elsewhere. HTML rendering is left to the consumer.
|
|
61
|
+
email:
|
|
62
|
+
- s.dumetz@holusion.com
|
|
63
|
+
executables: []
|
|
64
|
+
extensions: []
|
|
65
|
+
extra_rdoc_files: []
|
|
66
|
+
files:
|
|
67
|
+
- README.md
|
|
68
|
+
- lib/jekyll-openapi.rb
|
|
69
|
+
- lib/jekyll_openapi.rb
|
|
70
|
+
- lib/jekyll_openapi/dereferencer.rb
|
|
71
|
+
- lib/jekyll_openapi/document.rb
|
|
72
|
+
- lib/jekyll_openapi/filters.rb
|
|
73
|
+
- lib/jekyll_openapi/jekyll_plugin.rb
|
|
74
|
+
- lib/jekyll_openapi/json_pointer.rb
|
|
75
|
+
- lib/jekyll_openapi/parameters.rb
|
|
76
|
+
- lib/jekyll_openapi/schema_renderer/typescript.rb
|
|
77
|
+
- lib/jekyll_openapi/schema_renderer/xml.rb
|
|
78
|
+
- lib/jekyll_openapi/version.rb
|
|
79
|
+
homepage: https://github.com/sdumetz/jekyll-openapi
|
|
80
|
+
licenses:
|
|
81
|
+
- Apache-2.0
|
|
82
|
+
metadata: {}
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '3.0'
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 4.0.15
|
|
98
|
+
specification_version: 4
|
|
99
|
+
summary: OpenAPI 3.1 object model and schema renderers for Jekyll
|
|
100
|
+
test_files: []
|