rage-rb 1.10.1 → 1.12.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,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "yaml"
5
+
6
+ if !defined?(Prism)
7
+ fail <<~ERR
8
+
9
+ rage-rb depends on Prism to build OpenAPI specifications. Add the following line to your Gemfile:
10
+ gem "prism"
11
+
12
+ ERR
13
+ end
14
+
15
+ module Rage::OpenAPI
16
+ # Create a new OpenAPI application.
17
+ #
18
+ # @param namespace [String, Module] limit the parser to a specific namespace
19
+ # @example
20
+ # map "/publicapi" do
21
+ # run Rage.openapi.application
22
+ # end
23
+ # @example
24
+ # map "/publicapi/v1" do
25
+ # run Rage.openapi.application(namespace: "Api::V1")
26
+ # end
27
+ #
28
+ # map "/publicapi/v2" do
29
+ # run Rage.openapi.application(namespace: "Api::V2")
30
+ # end
31
+ def self.application(namespace: nil)
32
+ html_app = ->(env) do
33
+ __data_cache[[:page, namespace]] ||= begin
34
+ scheme, host, path = env["rack.url_scheme"], env["HTTP_HOST"], env["SCRIPT_NAME"]
35
+ spec_url = "#{scheme}://#{host}#{path}/json"
36
+ page = ERB.new(File.read("#{__dir__}/index.html.erb")).result(binding)
37
+
38
+ [200, { "Content-Type" => "text/html; charset=UTF-8" }, [page]]
39
+ end
40
+ end
41
+
42
+ json_app = ->(env) do
43
+ spec = (__data_cache[[:spec, namespace]] ||= build(namespace:).to_json)
44
+ [200, { "Content-Type" => "application/json" }, [spec]]
45
+ end
46
+
47
+ app = ->(env) do
48
+ if env["PATH_INFO"] == ""
49
+ html_app.call(env)
50
+ elsif env["PATH_INFO"] == "/json"
51
+ json_app.call(env)
52
+ else
53
+ [404, {}, ["Not Found"]]
54
+ end
55
+ end
56
+
57
+ if Rage.config.middleware.include?(Rage::Reloader)
58
+ Rage.with_middlewares(app, [Rage::Reloader])
59
+ elsif defined?(ActionDispatch::Reloader) && Rage.config.middleware.include?(ActionDispatch::Reloader)
60
+ Rage.with_middlewares(app, [ActionDispatch::Reloader])
61
+ else
62
+ app
63
+ end
64
+ end
65
+
66
+ # Build an OpenAPI specification for the application.
67
+ # @param namespace [String, Module] limit the parser to a specific namespace
68
+ # @return [Hash]
69
+ def self.build(namespace: nil)
70
+ Builder.new(namespace:).run
71
+ end
72
+
73
+ # @private
74
+ def self.__shared_components
75
+ __data_cache[:shared_components] ||= begin
76
+ components_file = Rage.root.join("config").glob("openapi_components.*")[0]
77
+
78
+ if components_file.nil?
79
+ {}
80
+ else
81
+ case components_file.extname
82
+ when ".yml", ".yaml"
83
+ YAML.safe_load(components_file.read)
84
+ when ".json"
85
+ JSON.parse(components_file.read)
86
+ else
87
+ Rage::OpenAPI.__log_warn "unrecognized file extension: #{components_file.relative_path_from(Rage.root)}; expected either .yml or .json"
88
+ {}
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # @private
95
+ def self.__data_cache
96
+ @__data_cache ||= {}
97
+ end
98
+
99
+ # @private
100
+ def self.__reset_data_cache
101
+ __data_cache.clear
102
+ end
103
+
104
+ # @private
105
+ def self.__try_parse_collection(str)
106
+ if str =~ /^Array<([\w\s:\(\)]+)>$/ || str =~ /^\[([\w\s:\(\)]+)\]$/
107
+ [true, $1]
108
+ else
109
+ [false, str]
110
+ end
111
+ end
112
+
113
+ # @private
114
+ def self.__module_parent(klass)
115
+ klass.name =~ /::[^:]+\z/ ? Object.const_get($`) : Object
116
+ rescue NameError
117
+ Object
118
+ end
119
+
120
+ # @private
121
+ def self.__log_warn(log)
122
+ puts "WARNING: #{log}"
123
+ end
124
+
125
+ module Nodes
126
+ end
127
+
128
+ module Parsers
129
+ module Ext
130
+ end
131
+ end
132
+ end
133
+
134
+ require_relative "builder"
135
+ require_relative "collector"
136
+ require_relative "parser"
137
+ require_relative "converter"
138
+ require_relative "nodes/root"
139
+ require_relative "nodes/parent"
140
+ require_relative "nodes/method"
141
+ require_relative "parsers/ext/alba"
142
+ require_relative "parsers/ext/active_record"
143
+ require_relative "parsers/yaml"
144
+ require_relative "parsers/shared_reference"
145
+ require_relative "parsers/request"
146
+ require_relative "parsers/response"
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OpenAPI::Parser
4
+ # @param node [Rage::OpenAPI::Nodes::Parent]
5
+ # @param comments [Array<Prism::InlineComment>]
6
+ def parse_dangling_comments(node, comments)
7
+ i = 0
8
+
9
+ while i < comments.length
10
+ children = nil
11
+ expression = comments[i].slice.delete_prefix("#").strip
12
+
13
+ if expression =~ /@deprecated\b/
14
+ if node.deprecated
15
+ Rage::OpenAPI.__log_warn "duplicate @deprecated tag detected at #{location_msg(comments[i])}"
16
+ else
17
+ node.deprecated = true
18
+ end
19
+ children = find_children(comments[i + 1..])
20
+
21
+ elsif expression =~ /@private\b/
22
+ if node.private
23
+ Rage::OpenAPI.__log_warn "duplicate @private tag detected at #{location_msg(comments[i])}"
24
+ else
25
+ node.private = true
26
+ end
27
+ children = find_children(comments[i + 1..])
28
+
29
+ elsif expression =~ /@version\s/
30
+ if node.root.version
31
+ Rage::OpenAPI.__log_warn "duplicate @version tag detected at #{location_msg(comments[i])}"
32
+ else
33
+ node.root.version = expression[9..]
34
+ end
35
+
36
+ elsif expression =~ /@title\s/
37
+ if node.root.title
38
+ Rage::OpenAPI.__log_warn "duplicate @title tag detected at #{location_msg(comments[i])}"
39
+ else
40
+ node.root.title = expression[7..]
41
+ end
42
+
43
+ elsif expression =~ /@response\s/
44
+ parse_response_tag(expression, node, comments[i])
45
+
46
+ elsif expression =~ /@auth\s/
47
+ method, name, tail_name = expression[6..].split(" ", 3)
48
+ children = find_children(comments[i + 1..])
49
+
50
+ if tail_name
51
+ Rage::OpenAPI.__log_warn "incorrect `@auth` name detected at #{location_msg(comments[i])}; security scheme name cannot contain spaces"
52
+ end
53
+
54
+ auth_entry = {
55
+ method:,
56
+ name: name || method,
57
+ definition: children.any? ? YAML.safe_load(children.join("\n")) : { "type" => "http", "scheme" => "bearer" }
58
+ }
59
+
60
+ if !node.controller.__before_action_exists?(method.to_sym)
61
+ Rage::OpenAPI.__log_warn "referenced before action `#{method}` is not defined in #{node.controller} at #{location_msg(comments[i])}; ensure a corresponding `before_action` call exists"
62
+ elsif node.auth.include?(auth_entry) || node.root.parent_nodes.any? { |parent_node| parent_node.auth.include?(auth_entry) }
63
+ Rage::OpenAPI.__log_warn "duplicate @auth tag detected at #{location_msg(comments[i])}"
64
+ else
65
+ node.auth << auth_entry
66
+ end
67
+ end
68
+
69
+ if children&.any?
70
+ i += children.length + 1
71
+ else
72
+ i += 1
73
+ end
74
+ end
75
+ end
76
+
77
+ # @param node [Rage::OpenAPI::Nodes::Method]
78
+ # @param comments [Array<Prism::InlineComment>]
79
+ def parse_method_comments(node, comments)
80
+ i = 0
81
+
82
+ while i < comments.length
83
+ children = nil
84
+ expression = comments[i].slice.delete_prefix("#").strip
85
+
86
+ if !expression.start_with?("@")
87
+ if node.summary
88
+ Rage::OpenAPI.__log_warn "invalid summary entry detected at #{location_msg(comments[i])}; summary should only be one line"
89
+ else
90
+ node.summary = expression
91
+ end
92
+
93
+ elsif expression =~ /@deprecated\b/
94
+ if node.parents.any?(&:deprecated)
95
+ Rage::OpenAPI.__log_warn "duplicate `@deprecated` tag detected at #{location_msg(comments[i])}; tag already exists in a parent class"
96
+ else
97
+ node.deprecated = true
98
+ end
99
+ children = find_children(comments[i + 1..])
100
+
101
+ elsif expression =~ /@private\b/
102
+ if node.parents.any?(&:private)
103
+ Rage::OpenAPI.__log_warn "duplicate `@private` tag detected at #{location_msg(comments[i])}; tag already exists in a parent class"
104
+ else
105
+ node.private = true
106
+ end
107
+ children = find_children(comments[i + 1..])
108
+
109
+ elsif expression =~ /@description\s/
110
+ children = find_children(comments[i + 1..])
111
+ node.description = [expression[13..]] + children
112
+
113
+ elsif expression =~ /@response\s/
114
+ parse_response_tag(expression, node, comments[i])
115
+
116
+ elsif expression =~ /@request\s/
117
+ request = expression[9..]
118
+ if node.request
119
+ Rage::OpenAPI.__log_warn "duplicate `@request` tag detected at #{location_msg(comments[i])}"
120
+ else
121
+ parsed = Rage::OpenAPI::Parsers::Request.parse(
122
+ request,
123
+ namespace: Rage::OpenAPI.__module_parent(node.controller)
124
+ )
125
+
126
+ if parsed
127
+ node.request = parsed
128
+ else
129
+ Rage::OpenAPI.__log_warn "unrecognized `@request` tag detected at #{location_msg(comments[i])}"
130
+ end
131
+ end
132
+
133
+ elsif expression =~ /@internal\b/
134
+ # no-op
135
+ children = find_children(comments[i + 1..])
136
+
137
+ else
138
+ Rage::OpenAPI.__log_warn "unrecognized `#{expression.split(" ")[0]}` tag detected at #{location_msg(comments[i])}"
139
+ end
140
+
141
+ if children&.any?
142
+ i += children.length + 1
143
+ else
144
+ i += 1
145
+ end
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def find_children(comments)
152
+ children = []
153
+
154
+ comments.each do |comment|
155
+ expression = comment.slice.sub(/^#\s/, "")
156
+
157
+ if expression.start_with?(/\s{2}/)
158
+ children << expression.strip
159
+ elsif expression.start_with?("@")
160
+ break
161
+ else
162
+ Rage::OpenAPI.__log_warn "unrecognized expression detected at #{location_msg(comment)}; use two spaces to mark multi-line expressions"
163
+ break
164
+ end
165
+ end
166
+
167
+ children
168
+ end
169
+
170
+ def location_msg(comment)
171
+ location = comment.location
172
+ relative_path = Pathname.new(location.__source_path).relative_path_from(Rage.root)
173
+
174
+ "#{relative_path}:#{location.start_line}"
175
+ end
176
+
177
+ def parse_response_tag(expression, node, comment)
178
+ response = expression[10..].strip
179
+ status, response_data = if response =~ /^\d{3}$/
180
+ [response, nil]
181
+ elsif response =~ /^\d{3}/
182
+ response.split(" ", 2)
183
+ else
184
+ ["200", response]
185
+ end
186
+
187
+ if node.responses.has_key?(status)
188
+ Rage::OpenAPI.__log_warn "duplicate `@response` tag detected at #{location_msg(comment)}"
189
+ elsif response_data.nil?
190
+ node.responses[status] = nil
191
+ else
192
+ parsed = Rage::OpenAPI::Parsers::Response.parse(
193
+ response_data,
194
+ namespace: Rage::OpenAPI.__module_parent(node.controller)
195
+ )
196
+
197
+ if parsed
198
+ node.responses[status] = parsed
199
+ else
200
+ Rage::OpenAPI.__log_warn "unrecognized `@response` tag detected at #{location_msg(comment)}"
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OpenAPI::Parsers::Ext::ActiveRecord
4
+ BLACKLISTED_ATTRIBUTES = %w(id created_at updated_at)
5
+
6
+ def initialize(namespace: Object, **)
7
+ @namespace = namespace
8
+ end
9
+
10
+ def known_definition?(str)
11
+ _, str = Rage::OpenAPI.__try_parse_collection(str)
12
+ defined?(ActiveRecord::Base) && @namespace.const_get(str).ancestors.include?(ActiveRecord::Base)
13
+ rescue NameError
14
+ false
15
+ end
16
+
17
+ def parse(klass_str)
18
+ is_collection, klass_str = Rage::OpenAPI.__try_parse_collection(klass_str)
19
+ klass = @namespace.const_get(klass_str)
20
+
21
+ schema = {}
22
+
23
+ klass.attribute_types.each do |attr_name, attr_type|
24
+ next if BLACKLISTED_ATTRIBUTES.include?(attr_name) ||
25
+ attr_name.end_with?("_id") ||
26
+ attr_name == klass.inheritance_column ||
27
+ klass.defined_enums.include?(attr_name)
28
+
29
+ schema[attr_name] = case attr_type.type
30
+ when :integer
31
+ { "type" => "integer" }
32
+ when :boolean
33
+ { "type" => "boolean" }
34
+ when :binary
35
+ { "type" => "string", "format" => "binary" }
36
+ when :date
37
+ { "type" => "string", "format" => "date" }
38
+ when :datetime, :time
39
+ { "type" => "string", "format" => "date-time" }
40
+ when :float
41
+ { "type" => "number", "format" => "float" }
42
+ when :decimal
43
+ { "type" => "number" }
44
+ when :json
45
+ { "type" => "object" }
46
+ else
47
+ { "type" => "string" }
48
+ end
49
+ end
50
+
51
+ klass.defined_enums.each do |attr_name, mapping|
52
+ schema[attr_name] = { "type" => "string", "enum" => mapping.keys }
53
+ end
54
+
55
+ result = { "type" => "object" }
56
+ result["properties"] = schema if schema.any?
57
+
58
+ result = { "type" => "array", "items" => result } if is_collection
59
+
60
+ result
61
+ end
62
+ end
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OpenAPI::Parsers::Ext::Alba
4
+ attr_reader :namespace
5
+
6
+ def initialize(namespace: Object, **)
7
+ @namespace = namespace
8
+ end
9
+
10
+ def known_definition?(str)
11
+ _, str = Rage::OpenAPI.__try_parse_collection(str)
12
+ defined?(Alba::Resource) && @namespace.const_get(str).ancestors.include?(Alba::Resource)
13
+ rescue NameError
14
+ false
15
+ end
16
+
17
+ def parse(klass_str)
18
+ __parse(klass_str).build_schema
19
+ end
20
+
21
+ def __parse_nested(klass_str)
22
+ __parse(klass_str).tap { |visitor|
23
+ visitor.root_key = visitor.root_key_for_collection = visitor.key_transformer = nil
24
+ }.build_schema
25
+ end
26
+
27
+ def __parse(klass_str)
28
+ is_collection, klass_str = Rage::OpenAPI.__try_parse_collection(klass_str)
29
+
30
+ klass = @namespace.const_get(klass_str)
31
+ source_path, _ = Object.const_source_location(klass.name)
32
+ ast = Prism.parse_file(source_path)
33
+
34
+ visitor = Visitor.new(self, is_collection)
35
+ ast.value.accept(visitor)
36
+
37
+ visitor
38
+ end
39
+
40
+ class VisitorContext
41
+ attr_accessor :symbols, :hashes, :keywords, :consts, :nil
42
+
43
+ def initialize
44
+ @symbols = []
45
+ @hashes = []
46
+ @keywords = {}
47
+ @consts = []
48
+ @nil = false
49
+ end
50
+ end
51
+
52
+ class Visitor < Prism::Visitor
53
+ attr_accessor :schema, :root_key, :root_key_for_collection, :key_transformer, :collection_key, :meta
54
+
55
+ def initialize(parser, is_collection)
56
+ @parser = parser
57
+ @is_collection = is_collection
58
+
59
+ @schema = {}
60
+ @segment = @schema
61
+ @context = nil
62
+ @prev_contexts = []
63
+
64
+ @self_name = nil
65
+ @root_key = nil
66
+ @root_key_for_collection = nil
67
+ @key_transformer = nil
68
+ @collection_key = false
69
+ @meta = {}
70
+ end
71
+
72
+ def visit_class_node(node)
73
+ @self_name ||= node.name.to_s
74
+
75
+ if node.name =~ /Resource$|Serializer$/ && node.superclass
76
+ visitor = @parser.__parse(node.superclass.name)
77
+ @root_key, @root_key_for_collection = visitor.root_key, visitor.root_key_for_collection
78
+ @key_transformer, @collection_key, @meta = visitor.key_transformer, visitor.collection_key, visitor.meta
79
+ @schema.merge!(visitor.schema)
80
+ end
81
+
82
+ super
83
+ end
84
+
85
+ def build_schema
86
+ result = { "type" => "object" }
87
+
88
+ result["properties"] = @schema if @schema.any?
89
+
90
+ if @is_collection
91
+ result = if @collection_key && @root_key_for_collection
92
+ { "type" => "object", "properties" => { @root_key_for_collection => { "type" => "object", "additionalProperties" => result }, **@meta } }
93
+ elsif @collection_key
94
+ { "type" => "object", "additionalProperties" => result }
95
+ elsif @root_key_for_collection
96
+ { "type" => "object", "properties" => { @root_key_for_collection => { "type" => "array", "items" => result }, **@meta } }
97
+ else
98
+ { "type" => "array", "items" => result }
99
+ end
100
+ elsif @root_key
101
+ result = { "type" => "object", "properties" => { @root_key => result, **@meta } }
102
+ end
103
+
104
+ result = deep_transform_keys(result) if @key_transformer
105
+
106
+ result
107
+ end
108
+
109
+ def visit_call_node(node)
110
+ case node.name
111
+ when :root_key
112
+ context = with_context { visit(node.arguments) }
113
+ @root_key, @root_key_for_collection = context.symbols
114
+
115
+ when :attributes, :attribute
116
+ context = with_context { visit(node.arguments) }
117
+ context.symbols.each { |symbol| @segment[symbol] = { "type" => "string" } }
118
+ context.keywords.except("if").each { |key, type| @segment[key] = get_type_definition(type) }
119
+
120
+ when :nested, :nested_attribute
121
+ context = with_context { visit(node.arguments) }
122
+ with_inner_segment(context.symbols[0]) { visit(node.block) }
123
+
124
+ when :meta
125
+ context = with_context do
126
+ visit(node.arguments)
127
+ visit(node.block)
128
+ end
129
+
130
+ key = context.symbols[0] || "meta"
131
+ unless context.nil
132
+ @meta = { key => hash_to_openapi_schema(context.hashes[0]) }
133
+ end
134
+
135
+ when :many, :has_many, :one, :has_one, :association
136
+ is_array = node.name == :many || node.name == :has_many
137
+ context = with_context { visit(node.arguments) }
138
+ key = context.keywords["key"] || context.symbols[0]
139
+
140
+ if node.block
141
+ with_inner_segment(key, is_array:) { visit(node.block) }
142
+ else
143
+ resource = context.keywords["resource"] || (::Alba.inflector && "#{::Alba.inflector.classify(key.to_s)}Resource")
144
+ is_valid_resource = @parser.namespace.const_get(resource) rescue false
145
+
146
+ @segment[key] = if is_array
147
+ @parser.__parse_nested(is_valid_resource ? "[#{resource}]" : "[Rage]") # TODO
148
+ else
149
+ @parser.__parse_nested(is_valid_resource ? resource : "Rage")
150
+ end
151
+ end
152
+
153
+ when :transform_keys
154
+ context = with_context { visit(node.arguments) }
155
+ @key_transformer = get_key_transformer(context.symbols[0])
156
+
157
+ when :collection_key
158
+ @collection_key = true
159
+
160
+ when :root_key!
161
+ if (inflector = ::Alba.inflector)
162
+ suffix = @self_name.end_with?("Resource") ? "Resource" : "Serializer"
163
+ name = inflector.demodulize(@self_name).delete_suffix(suffix)
164
+ @root_key = inflector.underscore(name)
165
+ @root_key_for_collection = inflector.pluralize(@root_key) if @is_collection
166
+ end
167
+ end
168
+ end
169
+
170
+ def visit_hash_node(node)
171
+ parsed_hash = YAML.safe_load(node.slice) rescue nil
172
+ @context.hashes << parsed_hash if parsed_hash
173
+ end
174
+
175
+ def visit_assoc_node(node)
176
+ value = case node.value
177
+ when Prism::StringNode
178
+ node.value.content
179
+ when Prism::ArrayNode
180
+ context = with_context { visit(node.value) }
181
+ context.symbols[0] || context.consts[0]
182
+ else
183
+ node.value.slice
184
+ end
185
+
186
+ @context.keywords[node.key.value] = value
187
+ end
188
+
189
+ def visit_constant_read_node(node)
190
+ return unless @context
191
+ @context.consts << node.name.to_s
192
+ end
193
+
194
+ def visit_symbol_node(node)
195
+ @context.symbols << node.value
196
+ end
197
+
198
+ def visit_nil_node(node)
199
+ @context.nil = true
200
+ end
201
+
202
+ private
203
+
204
+ def with_inner_segment(key, is_array: false)
205
+ prev_segment = @segment
206
+
207
+ properties = {}
208
+ if is_array
209
+ @segment[key] = { "type" => "array", "items" => { "type" => "object", "properties" => properties } }
210
+ else
211
+ @segment[key] = { "type" => "object", "properties" => properties }
212
+ end
213
+ @segment = properties
214
+
215
+ yield
216
+ @segment = prev_segment
217
+ end
218
+
219
+ def with_context
220
+ @prev_contexts << @context if @context
221
+ @context = VisitorContext.new
222
+ yield
223
+ current_context = @context
224
+ @context = @prev_contexts.pop
225
+ current_context
226
+ end
227
+
228
+ def hash_to_openapi_schema(hash)
229
+ return { "type" => "object" } unless hash
230
+
231
+ schema = hash.each_with_object({}) do |(key, value), memo|
232
+ memo[key.to_s] = if value.is_a?(Hash)
233
+ hash_to_openapi_schema(value)
234
+ elsif value.is_a?(Array)
235
+ { "type" => "array", "items" => { "type" => "string" } }
236
+ else
237
+ { "type" => "string" }
238
+ end
239
+ end
240
+
241
+ { "type" => "object", "properties" => schema }
242
+ end
243
+
244
+ def deep_transform_keys(schema)
245
+ schema.each_with_object({}) do |(key, value), memo|
246
+ transformed_key = %w(type properties items additionalProperties).include?(key) ? key : @key_transformer.call(key)
247
+ memo[transformed_key] = value.is_a?(Hash) ? deep_transform_keys(value) : value
248
+ end
249
+ end
250
+
251
+ def get_key_transformer(transformer_id)
252
+ return nil unless ::Alba.inflector
253
+
254
+ case transformer_id
255
+ when "camel"
256
+ ->(key) { ::Alba.inflector.camelize(key) }
257
+ when "lower_camel"
258
+ ->(key) { ::Alba.inflector.camelize_lower(key) }
259
+ when "dash"
260
+ ->(key) { ::Alba.inflector.dasherize(key) }
261
+ when "snake"
262
+ ->(key) { ::Alba.inflector.underscore(key) }
263
+ end
264
+ end
265
+
266
+ def get_type_definition(type_id)
267
+ case type_id
268
+ when "Integer"
269
+ { "type" => "integer" }
270
+ when "Boolean", ":Boolean"
271
+ { "type" => "boolean" }
272
+ when "Numeric"
273
+ { "type" => "number" }
274
+ when "Float"
275
+ { "type" => "number", "format" => "float" }
276
+ when "Date"
277
+ { "type" => "string", "format" => "date" }
278
+ when "DateTime", "Time"
279
+ { "type" => "string", "format" => "date-time" }
280
+ else
281
+ { "type" => "string" }
282
+ end
283
+ end
284
+ end
285
+ end