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 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,3 @@
1
+ module JekyllOpenAPI
2
+ VERSION = "0.1.0"
3
+ 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: []