Skip to content

Commit 8745c52

Browse files
authored
Add support for parameter format specification in schema building (#254)
1 parent 68659cd commit 8745c52

File tree

13 files changed

+107
-26
lines changed

13 files changed

+107
-26
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ RSpec::OpenAPI.summary_builder = ->(example) { example.metadata.dig(:example_gro
188188
# This example uses the tags from the parent_example_group
189189
RSpec::OpenAPI.tags_builder = -> (example) { example.metadata.dig(:example_group, :parent_example_group, :openapi, :tags) }
190190

191+
# Configure custom format for specific properties
192+
# This example assigns 'date-time' format to properties with names ending in '_at'
193+
RSpec::OpenAPI.formats_builder = ->(_example, key) { key.end_with?('_at') ? 'date-time' : nil }
194+
191195
# Change the example type(s) that will generate schema
192196
RSpec::OpenAPI.example_types = %i[request]
193197

lib/rspec/openapi.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def load_environment_settings
3333
@description_builder = ->(example) { example.description }
3434
@summary_builder = ->(example) { example.metadata[:summary] }
3535
@tags_builder = ->(example) { example.metadata[:tags] }
36+
@formats_builder = ->(example) { example.metadata[:formats] }
3637
@info = {}
3738
@application_version = '1.0.0'
3839
@request_headers = []
@@ -56,6 +57,7 @@ class << self
5657
:description_builder,
5758
:summary_builder,
5859
:tags_builder,
60+
:formats_builder,
5961
:info,
6062
:application_version,
6163
:request_headers,

lib/rspec/openapi/extractors/hanami.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def request_attributes(request, example)
5959
metadata = example.metadata[:openapi] || {}
6060
summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
6161
tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
62+
formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
6263
operation_id = metadata[:operation_id]
6364
required_request_params = metadata[:required_request_params] || []
6465
security = metadata[:security]
@@ -76,7 +77,18 @@ def request_attributes(request, example)
7677

7778
raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params))
7879

79-
[path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
80+
[
81+
path,
82+
summary,
83+
tags,
84+
operation_id,
85+
required_request_params,
86+
raw_path_params,
87+
description,
88+
security,
89+
deprecated,
90+
formats,
91+
]
8092
end
8193

8294
# @param [RSpec::ExampleGroups::*] context

lib/rspec/openapi/extractors/rack.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def request_attributes(request, example)
99
metadata = example.metadata[:openapi] || {}
1010
summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
1111
tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
12+
formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
1213
operation_id = metadata[:operation_id]
1314
required_request_params = metadata[:required_request_params] || []
1415
security = metadata[:security]
@@ -17,7 +18,18 @@ def request_attributes(request, example)
1718
raw_path_params = request.path_parameters
1819
path = request.path
1920
summary ||= "#{request.method} #{path}"
20-
[path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
21+
[
22+
path,
23+
summary,
24+
tags,
25+
operation_id,
26+
required_request_params,
27+
raw_path_params,
28+
description,
29+
security,
30+
deprecated,
31+
formats,
32+
]
2133
end
2234

2335
# @param [RSpec::ExampleGroups::*] context

lib/rspec/openapi/extractors/rails.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def request_attributes(request, example)
1919
metadata = example.metadata[:openapi] || {}
2020
summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
2121
tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
22+
formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
23+
2224
operation_id = metadata[:operation_id]
2325
required_request_params = metadata[:required_request_params] || []
2426
security = metadata[:security]
@@ -34,7 +36,18 @@ def request_attributes(request, example)
3436

3537
summary ||= "#{request.method} #{path}"
3638

37-
[path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated]
39+
[
40+
path,
41+
summary,
42+
tags,
43+
operation_id,
44+
required_request_params,
45+
raw_path_params,
46+
description,
47+
security,
48+
deprecated,
49+
formats,
50+
]
3851
end
3952

4053
# @param [RSpec::ExampleGroups::*] context

lib/rspec/openapi/record.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
:request_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
1313
:summary, # @param [String] - "v1/statuses #show"
1414
:tags, # @param [Array] - ["Status"]
15+
:formats, # @param [Proc] - ->(key) { key.end_with?('_at') ? 'date-time' : nil }
1516
:operation_id, # @param [String] - "request-1234"
1617
:description, # @param [String] - "returns a status"
1718
:security, # @param [Array] - [{securityScheme1: []}]

lib/rspec/openapi/record_builder.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def build(context, example:, extractor:)
1212
return if request.nil?
1313

1414
title = RSpec::OpenAPI.title.then { |t| t.is_a?(Proc) ? t.call(example) : t }
15-
path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated =
15+
path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated, formats =
1616
extractor.request_attributes(request, example)
1717

1818
return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| path.match?(ignored_path) }
@@ -31,6 +31,7 @@ def build(context, example:, extractor:)
3131
request_headers: request_headers,
3232
summary: summary,
3333
tags: tags,
34+
formats: formats,
3435
operation_id: operation_id,
3536
description: description,
3637
security: security,

lib/rspec/openapi/schema_builder.rb

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def build(record)
1818
if has_content
1919
response[:content] = {
2020
normalize_content_type(record.response_content_type) => {
21-
schema: build_property(record.response_body, disposition: disposition),
21+
schema: build_property(record.response_body, disposition: disposition, record: record),
2222
example: response_example(record, disposition: disposition),
2323
}.compact,
2424
}
@@ -73,7 +73,7 @@ def build_parameters(record)
7373
name: build_parameter_name(key, value),
7474
in: 'path',
7575
required: true,
76-
schema: build_property(try_cast(value)),
76+
schema: build_property(try_cast(value), key: key, record: record),
7777
example: (try_cast(value) if example_enabled?),
7878
}.compact
7979
end
@@ -83,7 +83,7 @@ def build_parameters(record)
8383
name: build_parameter_name(key, value),
8484
in: 'query',
8585
required: record.required_request_params.include?(key),
86-
schema: build_property(try_cast(value)),
86+
schema: build_property(try_cast(value), key: key, record: record),
8787
example: (try_cast(value) if example_enabled?),
8888
}.compact
8989
end
@@ -93,7 +93,7 @@ def build_parameters(record)
9393
name: build_parameter_name(key, value),
9494
in: 'header',
9595
required: true,
96-
schema: build_property(try_cast(value)),
96+
schema: build_property(try_cast(value), key: key, record: record),
9797
example: (try_cast(value) if example_enabled?),
9898
}.compact
9999
end
@@ -110,7 +110,7 @@ def build_response_headers(record)
110110

111111
record.response_headers.each do |key, value|
112112
headers[key] = {
113-
schema: build_property(try_cast(value)),
113+
schema: build_property(try_cast(value), key: key, record: record),
114114
}.compact
115115
end
116116

@@ -134,36 +134,38 @@ def build_request_body(record)
134134
{
135135
content: {
136136
normalize_content_type(record.request_content_type) => {
137-
schema: build_property(record.request_params),
137+
schema: build_property(record.request_params, record: record),
138138
example: (build_example(record.request_params) if example_enabled?),
139139
}.compact,
140140
},
141141
}
142142
end
143143

144-
def build_property(value, disposition: nil)
145-
property = build_type(value, disposition)
144+
def build_property(value, disposition: nil, key: nil, record: nil)
145+
format = disposition ? 'binary' : infer_format(key, record)
146+
147+
property = build_type(value, format: format)
146148

147149
case value
148150
when Array
149151
property[:items] = if value.empty?
150152
{} # unknown
151153
else
152-
build_property(value.first)
154+
build_property(value.first, record: record)
153155
end
154156
when Hash
155157
property[:properties] = {}.tap do |properties|
156158
value.each do |key, v|
157-
properties[key] = build_property(v)
159+
properties[key] = build_property(v, record: record, key: key)
158160
end
159161
end
160162
property = enrich_with_required_keys(property)
161163
end
162164
property
163165
end
164166

165-
def build_type(value, disposition)
166-
return { type: 'string', format: 'binary' } if disposition
167+
def build_type(value, format: nil)
168+
return { type: 'string', format: format } if format
167169

168170
case value
169171
when String
@@ -187,6 +189,12 @@ def build_type(value, disposition)
187189
end
188190
end
189191

192+
def infer_format(key, record)
193+
return nil if !key || !record || !record.formats
194+
195+
record.formats[key]
196+
end
197+
190198
# Convert an always-String param to an appropriate type
191199
def try_cast(value)
192200
Integer(value)

spec/apps/rails/doc/openapi.json

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -659,10 +659,12 @@
659659
"format": "float"
660660
},
661661
"created_at": {
662-
"type": "string"
662+
"type": "string",
663+
"format": "date-time"
663664
},
664665
"updated_at": {
665-
"type": "string"
666+
"type": "string",
667+
"format": "date-time"
666668
}
667669
},
668670
"required": [
@@ -763,10 +765,12 @@
763765
"format": "float"
764766
},
765767
"created_at": {
766-
"type": "string"
768+
"type": "string",
769+
"format": "date-time"
767770
},
768771
"updated_at": {
769-
"type": "string"
772+
"type": "string",
773+
"format": "date-time"
770774
}
771775
},
772776
"required": [
@@ -907,10 +911,12 @@
907911
"format": "float"
908912
},
909913
"created_at": {
910-
"type": "string"
914+
"type": "string",
915+
"format": "date-time"
911916
},
912917
"updated_at": {
913-
"type": "string"
918+
"type": "string",
919+
"format": "date-time"
914920
}
915921
},
916922
"required": [
@@ -1001,10 +1007,12 @@
10011007
"format": "float"
10021008
},
10031009
"created_at": {
1004-
"type": "string"
1010+
"type": "string",
1011+
"format": "date-time"
10051012
},
10061013
"updated_at": {
1007-
"type": "string"
1014+
"type": "string",
1015+
"format": "date-time"
10081016
}
10091017
},
10101018
"required": [
@@ -1154,10 +1162,12 @@
11541162
"format": "float"
11551163
},
11561164
"created_at": {
1157-
"type": "string"
1165+
"type": "string",
1166+
"format": "date-time"
11581167
},
11591168
"updated_at": {
1160-
"type": "string"
1169+
"type": "string",
1170+
"format": "date-time"
11611171
}
11621172
},
11631173
"required": [

spec/apps/rails/doc/openapi.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,10 @@ paths:
416416
format: float
417417
created_at:
418418
type: string
419+
format: date-time
419420
updated_at:
420421
type: string
422+
format: date-time
421423
required:
422424
- id
423425
- name
@@ -486,8 +488,10 @@ paths:
486488
format: float
487489
created_at:
488490
type: string
491+
format: date-time
489492
updated_at:
490493
type: string
494+
format: date-time
491495
required:
492496
- id
493497
- name
@@ -584,8 +588,10 @@ paths:
584588
format: float
585589
created_at:
586590
type: string
591+
format: date-time
587592
updated_at:
588593
type: string
594+
format: date-time
589595
required:
590596
- id
591597
- name
@@ -650,8 +656,10 @@ paths:
650656
format: float
651657
created_at:
652658
type: string
659+
format: date-time
653660
updated_at:
654661
type: string
662+
format: date-time
655663
required:
656664
- id
657665
- name
@@ -752,8 +760,10 @@ paths:
752760
format: float
753761
created_at:
754762
type: string
763+
format: date-time
755764
updated_at:
756765
type: string
766+
format: date-time
757767
required:
758768
- id
759769
- name

0 commit comments

Comments
 (0)