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.
@@ -0,0 +1,143 @@
1
+ require "json"
2
+ require_relative "json_pointer"
3
+
4
+ module JekyllOpenAPI
5
+ ##
6
+ # Lazy, name-preserving stand-in for a JSON `$ref`.
7
+ #
8
+ # The Dereferencer replaces each `{"$ref" => ...}` node with a Reference
9
+ # rather than inlining its target. This keeps the reference *graph* — and the
10
+ # component *name* — intact, which buys three things the old inlining threw
11
+ # away:
12
+ #
13
+ # * renderers/templates can link to a named schema instead of always
14
+ # expanding it (see Schema#name);
15
+ # * a schema reused N times resolves to one shared object, not N deep copies;
16
+ # * recursion terminates, because targets are only followed on demand.
17
+ #
18
+ # For read access a Reference transparently delegates to its resolved target
19
+ # (`ref["type"]`, `ref.dig("schema", "pattern")`, iteration), so plain
20
+ # hash-walking templates keep working unchanged. `#reference?`, `#name` and
21
+ # `#ref` expose the pointer for code that wants to link rather than follow.
22
+ class Reference
23
+ # Shared per-dereference state: the linked root (for pointer resolution) and
24
+ # the logger. Populated by the Dereferencer once the whole tree is linked.
25
+ Context = Struct.new(:root, :logger)
26
+
27
+ attr_reader :ref, :overrides
28
+
29
+ # @param ref [String] the raw pointer, e.g. "#/components/schemas/Pet"
30
+ # @param context [Context] shared resolution state
31
+ # @param overrides [Hash] sibling summary/description overriding the target
32
+ def initialize(ref, context, overrides = {})
33
+ @ref = ref
34
+ @context = context
35
+ @overrides = overrides
36
+ end
37
+
38
+ def reference?
39
+ true
40
+ end
41
+
42
+ # True when the pointer is local to the current document (starts with "#").
43
+ def local?
44
+ @ref.is_a?(String) && @ref.start_with?("#")
45
+ end
46
+
47
+ # True when the pointer targets another file or URL (unsupported for now).
48
+ def external?
49
+ !local?
50
+ end
51
+
52
+ # Component name, e.g. "Pet" for "#/components/schemas/Pet"; nil when external.
53
+ def name
54
+ return nil unless local?
55
+ @name ||= JSONPointer.unescape(@ref.delete_prefix("#").split("/").last)
56
+ end
57
+
58
+ # The target node, lazily resolved and memoized, with `summary`/`description`
59
+ # overrides applied. Nested `$ref`s inside it are themselves References.
60
+ # Returns nil (warning once) for external or unresolvable pointers.
61
+ def resolve
62
+ return @resolved if @resolved_computed
63
+ @resolved_computed = true
64
+ @resolved = compute_resolution
65
+ end
66
+
67
+ # --- hash-like read delegation to the resolved target --------------------
68
+
69
+ def [](key)
70
+ return @ref if key == "$ref"
71
+ target = resolve
72
+ mapping?(target) ? target[key] : nil
73
+ end
74
+
75
+ def key?(key)
76
+ return true if key == "$ref"
77
+ target = resolve
78
+ mapping?(target) && target.key?(key)
79
+ end
80
+ alias_method :include?, :key?
81
+
82
+ def fetch(key, *default)
83
+ return self[key] if key?(key)
84
+ return yield(key) if block_given?
85
+ return default.first unless default.empty?
86
+ raise KeyError, "key not found: #{key.inspect}"
87
+ end
88
+
89
+ def dig(key, *rest)
90
+ value = self[key]
91
+ return value if rest.empty? || value.nil?
92
+ value.respond_to?(:dig) ? value.dig(*rest) : nil
93
+ end
94
+
95
+ def each(&block)
96
+ target = resolve
97
+ target.each(&block) if target.respond_to?(:each)
98
+ end
99
+ alias_method :each_pair, :each
100
+
101
+ # Liquid indexes us like the hash we stand for (it calls #to_liquid on every
102
+ # value and does not patch arbitrary objects), so hand it back ourselves.
103
+ def to_liquid
104
+ self
105
+ end
106
+
107
+ # A Reference merges like the hash it stands for (used by Parameters.merge).
108
+ def to_hash
109
+ target = resolve
110
+ target.is_a?(Hash) ? target : { "$ref" => @ref }
111
+ end
112
+ alias_method :to_h, :to_hash
113
+
114
+ # Serialize back to a $ref node. Finite by construction (it never expands the
115
+ # target), so `oapi_dereference | jsonify` stays bounded even for recursive
116
+ # schemas, and round-trips to a valid OpenAPI reference.
117
+ def to_json(*args)
118
+ { "$ref" => @ref }.to_json(*args)
119
+ end
120
+
121
+ private
122
+
123
+ def mapping?(node)
124
+ node.is_a?(Hash) || node.is_a?(Reference)
125
+ end
126
+
127
+ def compute_resolution
128
+ unless local?
129
+ @context.logger.warn("external or invalid $ref left unresolved: #{@ref.inspect}")
130
+ return nil
131
+ end
132
+ target = JSONPointer.resolve(@context.root, @ref.delete_prefix("#"))
133
+ if target.nil?
134
+ @context.logger.warn("unresolvable $ref: #{@ref}")
135
+ return nil
136
+ end
137
+ # Apply OpenAPI 3.1 sibling overrides without disturbing the shared target
138
+ # (so diamond reuse keeps object identity when there are no overrides).
139
+ return target unless target.is_a?(Hash) && !@overrides.empty?
140
+ target.merge(@overrides)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,189 @@
1
+ require_relative "reference"
2
+ require_relative "json_pointer"
3
+
4
+ module JekyllOpenAPI
5
+ ##
6
+ # Normalized, ref-aware view over a JSON-Schema node, shared by every schema
7
+ # renderer.
8
+ #
9
+ # It centralises the two things that used to be copy-pasted (and drift) across
10
+ # the TypeScript and XML renderers: the type *inference* that fills in an
11
+ # omitted `type`, and the `$ref` *following* with cycle detection. A new
12
+ # renderer (JSON example, HTML, …) implements formatting only — it never walks
13
+ # raw hashes or re-infers types.
14
+ #
15
+ # Schemas are immutable and carry the set of `$ref` pointers already followed
16
+ # on the current path (`seen`), so following a self-referential schema stops
17
+ # instead of looping, while genuine reuse (the same schema in two branches)
18
+ # is never mistaken for a cycle.
19
+ class Schema
20
+ # Sentinel returned by #follow when a `$ref` points back onto the path.
21
+ CYCLE = :cycle
22
+
23
+ def self.wrap(node, seen = [])
24
+ node.is_a?(Schema) ? node : new(node, seen)
25
+ end
26
+
27
+ attr_reader :seen
28
+
29
+ def initialize(node, seen = [])
30
+ @node = node
31
+ @seen = seen
32
+ end
33
+
34
+ def raw
35
+ @node
36
+ end
37
+
38
+ # Whether this wraps something readable as a schema object.
39
+ def mapping?
40
+ @node.is_a?(Hash) || @node.is_a?(Reference)
41
+ end
42
+
43
+ # A $ref node — either a linked {Reference} (produced by the Dereferencer)
44
+ # or a raw, un-dereferenced `{"$ref" => "..."}` hash. Link-mode rendering
45
+ # only needs the pointer string, so it works on either form (see #ref/#name);
46
+ # *following* a ref to inline its target still requires a linked Reference
47
+ # (a raw ref carries no root to resolve against — see #follow).
48
+ def reference?
49
+ @node.is_a?(Reference) || raw_reference?
50
+ end
51
+
52
+ # A plain `{"$ref" => "..."}` hash that never went through the Dereferencer.
53
+ def raw_reference?
54
+ @node.is_a?(Hash) && @node["$ref"].is_a?(String)
55
+ end
56
+
57
+ # The raw pointer string, e.g. "#/components/schemas/Pet"; nil when not a ref.
58
+ def ref
59
+ return @node.ref if @node.is_a?(Reference)
60
+ return @node["$ref"] if raw_reference?
61
+ nil
62
+ end
63
+
64
+ # Component name of a local ref, e.g. "Pet" for "#/components/schemas/Pet";
65
+ # nil for external refs or non-refs. Derived from the pointer alone, so it
66
+ # needs no dereferencing.
67
+ def name
68
+ pointer = ref
69
+ return nil unless pointer.is_a?(String) && pointer.start_with?("#")
70
+ JSONPointer.unescape(pointer.delete_prefix("#").split("/").last)
71
+ end
72
+
73
+ # A local $ref to a named component — the kind an HTML renderer links to
74
+ # rather than inlines. External refs (no local name) return false.
75
+ def named_reference?
76
+ reference? && !name.nil?
77
+ end
78
+
79
+ # The use-site description sitting beside a $ref (a 3.1 sibling override),
80
+ # if any. Lets a renderer annotate a link without expanding its target.
81
+ def override_description
82
+ return @node.overrides["description"] if @node.is_a?(Reference)
83
+ return @node["description"] if raw_reference?
84
+ nil
85
+ end
86
+
87
+ # Follow a `$ref` one hop.
88
+ # @return [Schema] the target when resolvable,
89
+ # CYCLE when the ref is already on the current path,
90
+ # nil when external, unresolvable, or a raw (never-dereferenced) ref that
91
+ # carries no root to resolve against (a warning is emitted by Reference).
92
+ def follow
93
+ return self unless reference?
94
+ pointer = ref
95
+ return CYCLE if @seen.include?(pointer)
96
+ target = @node.is_a?(Reference) ? @node.resolve : nil
97
+ target.nil? ? nil : Schema.new(target, @seen + [pointer])
98
+ end
99
+
100
+ # Raw read, delegating through a Reference transparently.
101
+ def [](key)
102
+ mapping? ? @node[key] : nil
103
+ end
104
+
105
+ def dig(*keys)
106
+ mapping? ? @node.dig(*keys) : nil
107
+ end
108
+
109
+ # --- composition ---------------------------------------------------------
110
+
111
+ # @return [Array(String, Array<Schema>), nil] e.g. ["oneOf", [Schema, ...]]
112
+ def composition
113
+ %w[oneOf anyOf allOf].each do |keyword|
114
+ members = self[keyword]
115
+ return [keyword, members.map { |m| child(m) }] if members.is_a?(Array)
116
+ end
117
+ nil
118
+ end
119
+
120
+ # --- type ----------------------------------------------------------------
121
+
122
+ def declared_type
123
+ self["type"]
124
+ end
125
+
126
+ def type_union?
127
+ declared_type.is_a?(Array)
128
+ end
129
+
130
+ # Single inferred type, mirroring what most tooling assumes when `type` is
131
+ # omitted. Returns nil for a type union (see #type_union?) or the unknown.
132
+ def type
133
+ declared = declared_type
134
+ return declared if declared.is_a?(String)
135
+ return nil if declared.is_a?(Array)
136
+ return "object" if self["properties"] || self["additionalProperties"]
137
+ return "array" if self["items"]
138
+ return "string" if self["enum"]
139
+ nil
140
+ end
141
+
142
+ # A copy of this schema with `type` pinned to one member of a type union.
143
+ def with_type(single)
144
+ node = @node.is_a?(Hash) ? @node.merge("type" => single) : @node
145
+ Schema.new(node, @seen)
146
+ end
147
+
148
+ # --- object --------------------------------------------------------------
149
+
150
+ def properties
151
+ (self["properties"] || {}).map { |key, node| [key, child(node)] }
152
+ end
153
+
154
+ def required?(key)
155
+ Array(self["required"]).include?(key)
156
+ end
157
+
158
+ # @return [Schema, nil] the additionalProperties schema, only when it is a
159
+ # schema object (the boolean `true`/`false` forms carry no shape to render)
160
+ def additional_properties
161
+ value = self["additionalProperties"]
162
+ value.is_a?(Hash) || value.is_a?(Reference) ? child(value) : nil
163
+ end
164
+
165
+ # --- array ---------------------------------------------------------------
166
+
167
+ def items
168
+ child(self["items"])
169
+ end
170
+
171
+ # --- leaf metadata -------------------------------------------------------
172
+
173
+ def enum = self["enum"]
174
+ def format = self["format"]
175
+ def pattern = self["pattern"]
176
+ def summary = self["summary"]
177
+ def description = self["description"]
178
+ def example = self["example"]
179
+ def examples = self["examples"]
180
+ def xml = self["xml"] || {}
181
+
182
+ private
183
+
184
+ # Descending into a member keeps the current cycle path (no ref followed).
185
+ def child(node)
186
+ Schema.wrap(node, @seen)
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,186 @@
1
+ require "cgi"
2
+ require_relative "../schema"
3
+
4
+ module JekyllOpenAPI
5
+ module SchemaRenderer
6
+ ##
7
+ # Renders a JSON-Schema as semantic, linkable HTML.
8
+ #
9
+ # Unlike the TypeScript/XML renderers — which always follow `$ref`s and
10
+ # inline everything — this renderer works in *link mode*: a reference to a
11
+ # named component schema becomes an anchor to that schema's definition
12
+ # (`#schema-<name>`) instead of being expanded. Anonymous inline schemas are
13
+ # rendered structurally. That keeps operation bodies compact, cross-links
14
+ # reused types, and makes recursion terminate for free (a link is never
15
+ # followed).
16
+ #
17
+ # Markup is semantic and class-annotated (`<dl>`/`<ul>`/`<div>`, never
18
+ # tables); all styling is left to the consumer. Every class is `oapi-`
19
+ # prefixed. Emitted text is HTML-escaped.
20
+ #
21
+ # Descriptions are plain-escaped by default. Pass a `markdown:` callable
22
+ # (`text -> html`) to render them as markdown instead — the core library has
23
+ # no markdown engine of its own, so the caller supplies one (the Jekyll
24
+ # filter wires in the site's converter; see Filters#oapi_html_schema).
25
+ class HTML
26
+ # Fragment prefix for a named schema, e.g. "schema-Pet". A Models section
27
+ # anchors each definition with the matching id (see oapi_schema_anchor).
28
+ ANCHOR_PREFIX = "schema-"
29
+
30
+ def self.anchor(name)
31
+ "#{ANCHOR_PREFIX}#{name}"
32
+ end
33
+
34
+ # @param markdown [#call, nil] a `text -> html` converter for descriptions;
35
+ # when nil, descriptions are HTML-escaped plain text
36
+ def initialize(logger: JekyllOpenAPI.logger, markdown: nil)
37
+ @logger = logger
38
+ @markdown = markdown
39
+ end
40
+
41
+ def self.render(schema, logger: JekyllOpenAPI.logger, markdown: nil)
42
+ new(logger: logger, markdown: markdown).render(schema)
43
+ end
44
+
45
+ # @param input [Hash, Reference, Schema] a JSON-Schema
46
+ # @return [String] HTML
47
+ def render(input)
48
+ schema = Schema.wrap(input)
49
+
50
+ if schema.named_reference?
51
+ # Link, don't follow. Any use-site (sibling) description still shows.
52
+ return annotate(link(schema), schema.override_description)
53
+ end
54
+ if schema.reference?
55
+ # External/unnamed ref: surface the pointer, don't fake a type.
56
+ return ref_code(schema)
57
+ end
58
+ unless schema.mapping?
59
+ @logger.warn("not a valid schema object: #{schema.raw.inspect}")
60
+ return ""
61
+ end
62
+
63
+ html = base(schema)
64
+ return "" if html.nil?
65
+ annotate(html, schema.description)
66
+ end
67
+
68
+ private
69
+
70
+ def base(schema)
71
+ return composition(schema) if schema.composition
72
+ return type_union(schema) if schema.type_union?
73
+
74
+ case schema.type
75
+ when "object" then object(schema)
76
+ when "array" then array(schema)
77
+ when "string" then string(schema)
78
+ when "null" then span("oapi-type", "null")
79
+ when nil
80
+ @logger.warn("not a valid schema object: #{schema.raw.inspect}")
81
+ nil
82
+ else
83
+ primitive(schema)
84
+ end
85
+ end
86
+
87
+ def object(schema)
88
+ rows = schema.properties.map do |key, property|
89
+ required = schema.required?(key)
90
+ name = "#{span("oapi-prop-name", key)}#{required ? required_marker : ""}"
91
+ <<~ROW.chomp
92
+ <div class="oapi-prop#{required ? " oapi-prop-required" : ""}">
93
+ <dt>#{name}</dt>
94
+ <dd>#{render(property)}</dd>
95
+ </div>
96
+ ROW
97
+ end
98
+ rows << additional_row(schema)
99
+ "<dl class=\"oapi-object\">#{rows.compact.join}</dl>"
100
+ end
101
+
102
+ # additionalProperties: a schema (typed map), `true` (open) or absent.
103
+ def additional_row(schema)
104
+ value =
105
+ if (additional = schema.additional_properties)
106
+ render(additional)
107
+ elsif schema["additionalProperties"] == true
108
+ span("oapi-type", "any")
109
+ end
110
+ return nil unless value
111
+ <<~ROW.chomp
112
+ <div class="oapi-prop oapi-prop-additional">
113
+ <dt>#{span("oapi-additional-key", "additional properties")}</dt>
114
+ <dd>#{value}</dd>
115
+ </div>
116
+ ROW
117
+ end
118
+
119
+ def array(schema)
120
+ "<div class=\"oapi-array\">#{span("oapi-array-label", "array of")} #{render(schema.items)}</div>"
121
+ end
122
+
123
+ def string(schema)
124
+ if schema.enum
125
+ values = schema.enum.map { |value| "<li>#{code("oapi-enum-value", value)}</li>" }.join
126
+ "<ul class=\"oapi-enum\">#{values}</ul>"
127
+ else
128
+ primitive(schema, "string")
129
+ end
130
+ end
131
+
132
+ def primitive(schema, type = schema.type)
133
+ html = span("oapi-type", type)
134
+ html += " #{span("oapi-format", "(#{schema.format})")}" if schema.format
135
+ html += " #{code("oapi-pattern", "/#{schema.pattern}/")}" if schema.pattern
136
+ html
137
+ end
138
+
139
+ def composition(schema)
140
+ keyword, members = schema.composition
141
+ items = members.map { |member| "<li>#{render(member)}</li>" }.join
142
+ "<ul class=\"oapi-union\" data-kind=\"#{keyword}\">#{items}</ul>"
143
+ end
144
+
145
+ # OpenAPI 3.1 type array, e.g. ["string", "null"]
146
+ def type_union(schema)
147
+ types = schema.declared_type
148
+ parts = (types - ["null"]).map { |type| base(schema.with_type(type)) }
149
+ parts << span("oapi-nullable", "null") if types.include?("null")
150
+ parts.join(" #{span("oapi-or", "|")} ")
151
+ end
152
+
153
+ def link(schema)
154
+ name = schema.name
155
+ "<a class=\"oapi-ref\" href=\"##{ANCHOR_PREFIX}#{CGI.escapeHTML(name)}\">#{escape(name)}</a>"
156
+ end
157
+
158
+ def ref_code(schema)
159
+ code("oapi-ref-external", schema.ref)
160
+ end
161
+
162
+ def annotate(html, description)
163
+ return html if description.nil? || description.empty?
164
+ # A markdown converter returns trusted HTML; without one, escape the text.
165
+ body = @markdown ? @markdown.call(description) : escape(description)
166
+ "#{html}<div class=\"oapi-desc\">#{body}</div>"
167
+ end
168
+
169
+ def required_marker
170
+ "<span class=\"oapi-required\">required</span>"
171
+ end
172
+
173
+ def span(cls, text)
174
+ "<span class=\"#{cls}\">#{escape(text)}</span>"
175
+ end
176
+
177
+ def code(cls, text)
178
+ "<code class=\"#{cls}\">#{escape(text)}</code>"
179
+ end
180
+
181
+ def escape(text)
182
+ CGI.escapeHTML(text.to_s)
183
+ end
184
+ end
185
+ end
186
+ end
@@ -1,7 +1,11 @@
1
+ require_relative "../schema"
2
+
1
3
  module JekyllOpenAPI
2
4
  module SchemaRenderer
3
5
  ##
4
6
  # Renders a JSON-Schema as a simplified TypeScript-like textual representation.
7
+ # Traversal, type inference and `$ref` following live in {Schema}; this class
8
+ # only decides how each shape prints.
5
9
  # @see https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00
6
10
  class TypeScript
7
11
  def initialize(logger: JekyllOpenAPI.logger)
@@ -12,94 +16,100 @@ module JekyllOpenAPI
12
16
  new(logger: logger).render(schema)
13
17
  end
14
18
 
15
- # @param input [Hash] a JSON-Schema
19
+ # @param input [Hash, Reference, Schema] a JSON-Schema
16
20
  # @param prefix [String] current indentation
17
21
  # @return [String]
18
22
  def render(input, prefix = "")
19
- unless input.is_a?(Hash)
20
- @logger.warn("not a valid schema object: #{input.inspect}")
23
+ schema = Schema.wrap(input)
24
+
25
+ if schema.reference?
26
+ followed = schema.follow
27
+ # A cycle prints the schema's name (e.g. "Node") instead of looping.
28
+ return schema.name.to_s if followed == Schema::CYCLE
29
+ return "" if followed.nil?
30
+ return render(followed, prefix)
31
+ end
32
+
33
+ unless schema.mapping?
34
+ @logger.warn("not a valid schema object: #{schema.raw.inspect}")
21
35
  return ""
22
36
  end
23
- str = base(input, prefix)
37
+
38
+ str = base(schema, prefix)
24
39
  return "" if str.nil?
25
- annotate(str, input)
40
+ annotate(str, schema)
26
41
  end
27
42
 
28
43
  private
29
44
 
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"]
45
+ def base(schema, prefix)
46
+ if (composition = schema.composition)
47
+ keyword, members = composition
48
+ separator = keyword == "allOf" ? " & " : " | "
49
+ return members.map { |member| render(member, prefix) }.join(separator)
50
+ end
51
+ return type_union(schema, prefix) if schema.type_union?
41
52
 
42
- case type
43
- when "object" then object(input, prefix)
44
- when "array" then "[ #{render(input["items"], prefix)} ]"
45
- when "string" then string(input)
53
+ case schema.type
54
+ when "object" then object(schema, prefix)
55
+ when "array" then "[ #{render(schema.items, prefix)} ]"
56
+ when "string" then string(schema)
46
57
  when "null" then "null"
47
58
  when nil
48
- @logger.warn("not a valid schema object: #{input.inspect}")
59
+ @logger.warn("not a valid schema object: #{schema.raw.inspect}")
49
60
  nil
50
61
  else
51
- primitive(input, type)
62
+ primitive(schema)
52
63
  end
53
64
  end
54
65
 
55
- def union(schemas, prefix, separator)
56
- schemas.map { |schema| render(schema, prefix) }.join(separator)
57
- end
58
-
59
66
  # 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) }
67
+ def type_union(schema, prefix)
68
+ types = schema.declared_type
69
+ rendered = (types - ["null"]).map { |t| base(schema.with_type(t), prefix) }
63
70
  rendered.push("null") if types.include?("null")
64
71
  rendered.join(" | ")
65
72
  end
66
73
 
67
- def object(input, prefix)
74
+ def object(schema, prefix)
68
75
  str = "{"
69
- (input["properties"] || {}).each do |key, schema|
70
- required = Array(input["required"]).include?(key)
71
- str += "\n#{prefix} #{key}#{required ? "" : "?"}: #{render(schema, prefix + " ")}"
76
+ schema.properties.each do |key, property|
77
+ optional = schema.required?(key) ? "" : "?"
78
+ str += "\n#{prefix} #{key}#{optional}: #{render(property, prefix + " ")}"
72
79
  end
73
- props = input["additionalProperties"]
74
- if props.is_a?(Hash)
75
- str += "\n#{prefix} [#{props["type"]}]?: #{render(props, prefix + " ")}"
80
+ if (additional = schema.additional_properties)
81
+ str += "\n#{prefix} [#{additional.type}]?: #{render(additional, prefix + " ")}"
82
+ elsif schema["additionalProperties"] == true
83
+ # open map with an unconstrained value type; `false` is the closed
84
+ # default and needs no annotation
85
+ str += "\n#{prefix} [string]?: any"
76
86
  end
77
87
  "#{str}\n#{prefix}}"
78
88
  end
79
89
 
80
- def string(input)
81
- if input["enum"]
82
- input["enum"].map { |v| "\"#{v}\"" }.join("|")
83
- elsif input["format"] == "binary"
90
+ def string(schema)
91
+ if schema.enum
92
+ schema.enum.map { |value| "\"#{value}\"" }.join("|")
93
+ elsif schema.format == "binary"
84
94
  "Buffer"
85
95
  else
86
96
  "\"string\""
87
97
  end
88
98
  end
89
99
 
90
- def primitive(input, type)
91
- str = type.to_s
92
- str += " (#{input["format"]})" if input["format"]
100
+ def primitive(schema)
101
+ str = schema.type.to_s
102
+ str += " (#{schema.format})" if schema.format
93
103
  str
94
104
  end
95
105
 
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"]
106
+ def annotate(str, schema)
107
+ str = "#{str} // /#{schema.pattern}/" if schema.pattern
108
+ if schema.summary
109
+ str = "#{str} //#{schema.summary}"
110
+ elsif schema.description
101
111
  padding = "".rjust(str.length, " ")
102
- str = "#{str} //#{input["description"].split("\n").join("\n#{padding} //")}"
112
+ str = "#{str} //#{schema.description.split("\n").join("\n#{padding} //")}"
103
113
  end
104
114
  str
105
115
  end