jsapi 1.2 → 1.3

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jsapi/configuration.rb +2 -2
  3. data/lib/jsapi/controller/methods.rb +17 -4
  4. data/lib/jsapi/controller/parameters.rb +27 -10
  5. data/lib/jsapi/controller/response.rb +29 -5
  6. data/lib/jsapi/meta/callback/base.rb +1 -1
  7. data/lib/jsapi/meta/definitions.rb +10 -6
  8. data/lib/jsapi/meta/example/base.rb +4 -6
  9. data/lib/jsapi/meta/link/base.rb +4 -2
  10. data/lib/jsapi/meta/oauth_flow.rb +11 -2
  11. data/lib/jsapi/meta/openapi/path_item.rb +46 -0
  12. data/lib/jsapi/meta/openapi/version.rb +6 -0
  13. data/lib/jsapi/meta/openapi.rb +2 -0
  14. data/lib/jsapi/meta/operation.rb +6 -14
  15. data/lib/jsapi/meta/parameter/base.rb +82 -59
  16. data/lib/jsapi/meta/request_body/base.rb +3 -2
  17. data/lib/jsapi/meta/response/base.rb +26 -8
  18. data/lib/jsapi/meta/schema/base.rb +1 -1
  19. data/lib/jsapi/meta/schema/discriminator.rb +14 -4
  20. data/lib/jsapi/meta/schema/object.rb +28 -9
  21. data/lib/jsapi/meta/schema/validation/maximum.rb +1 -1
  22. data/lib/jsapi/meta/schema/validation/minimum.rb +1 -1
  23. data/lib/jsapi/meta/security_scheme/api_key.rb +4 -5
  24. data/lib/jsapi/meta/security_scheme/base.rb +16 -0
  25. data/lib/jsapi/meta/security_scheme/http/basic.rb +3 -10
  26. data/lib/jsapi/meta/security_scheme/http/bearer.rb +8 -8
  27. data/lib/jsapi/meta/security_scheme/http/other.rb +3 -5
  28. data/lib/jsapi/meta/security_scheme/mutual_tls.rb +23 -0
  29. data/lib/jsapi/meta/security_scheme/oauth2.rb +29 -13
  30. data/lib/jsapi/meta/security_scheme/open_id_connect.rb +5 -8
  31. data/lib/jsapi/meta/security_scheme.rb +3 -0
  32. data/lib/jsapi/meta/server.rb +9 -1
  33. data/lib/jsapi/meta/tag.rb +34 -4
  34. data/lib/jsapi/version.rb +1 -1
  35. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40ceeb6c8774b09bfb3fbfe2bc4550444a179158b43cac27d6f9923cead5971c
4
- data.tar.gz: f6cc387571c73b93cdcc121c1c46267424a46362f4c400dee8bd53ec2e0af561
3
+ metadata.gz: 2abec73a53f67bfebe51e0466e662be1dfc393f3fe5946612a12c94fde8b463b
4
+ data.tar.gz: f9c69c6df40d4f88e5175beb185a528ad76f2c6b4cbf6a8f88ac15915943f706
5
5
  SHA512:
6
- metadata.gz: 437497757705da6bfeddab26b70ab0e4df3b7abe11f78b4433f6b5576b783f8d576c0c3b2e6612b4e941d58136e965b705e5db37176f59ee672c58bf64e5bb42
7
- data.tar.gz: d25820eec66069cba5bb25eb2a0c1cb262835a57ec60eef94199ab247332aa296ba5228ebf0a61c1bbc80c9b350aa19cfd2339cb11188c260973f3607cd6da5f
6
+ metadata.gz: ca369ab50a38016fa37e9199e5a38f9248d2cbcfa51d80c39461dd206d8b09fb6ae60ea29861d211b9a939e408b6e48f35008da577c9f3a789bb6ebb592fafda
7
+ data.tar.gz: 8093446b1d7601d9a0e89cae395fa0ced837901b1a9eb0866ebcfd721cebb9e07d611c851735c847a699d71cec4ca474d04e39710ebc244476f6deb635e7fdc7
@@ -4,11 +4,11 @@ module Jsapi
4
4
  # Holds the \Jsapi configuration.
5
5
  class Configuration
6
6
  # The path where the API definitions are located relative to +Rails.root+.
7
- # The default is <code>"app/api_defs"</code>.
7
+ # The default is <code>"jsapi/api_defs"</code>.
8
8
  attr_accessor :api_defs_path
9
9
 
10
10
  def initialize # :nodoc:
11
- @api_defs_path = 'app/api_defs'
11
+ @api_defs_path = 'jsapi/api_defs'
12
12
  end
13
13
 
14
14
  # Returns the absolute +Pathname+ for +args+ within +api_defs_path+.
@@ -15,7 +15,9 @@ module Jsapi
15
15
  # Performs an API operation by calling the given block. The request parameters are
16
16
  # passed as an instance of the operation's model class to the block. The object
17
17
  # returned by the block is implicitly rendered according to the appropriate +response+
18
- # specification when the content type is a JSON MIME type.
18
+ # specification when the content type is a JSON MIME type. When content type is
19
+ # <code>application/json-seq</code>, the object returned by the block is streamed in
20
+ # JSON sequence text format.
19
21
  #
20
22
  # api_operation('foo') do |api_params|
21
23
  # # ...
@@ -153,18 +155,29 @@ module Jsapi
153
155
  end
154
156
 
155
157
  # Write response
156
- return unless response_model.json_type?
158
+ return unless response_model.json_type? || response_model.json_seq_type?
157
159
 
158
160
  response = Response.new(result, response_model, definitions, omit: omit)
159
161
  self.content_type = response_model.content_type
160
- render(json: response, status: status)
162
+
163
+ if response_model.json_seq_type?
164
+ self.response.status = status
165
+
166
+ self.response.stream.tap do |stream|
167
+ response.write_json_seq_to(stream)
168
+ ensure
169
+ stream.close
170
+ end
171
+ else
172
+ render(json: response, status: status)
173
+ end
161
174
  end
162
175
 
163
176
  def _api_params(operation, definitions, strong:)
164
177
  (operation.model || Model::Base).new(
165
178
  Parameters.new(
166
179
  params.except(:action, :controller, :format).permit!,
167
- request.headers,
180
+ request,
168
181
  operation,
169
182
  definitions,
170
183
  strong: strong
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
4
+
3
5
  module Jsapi
4
6
  module Controller
5
7
  # Used to wrap request parameters.
@@ -14,18 +16,32 @@ module Jsapi
14
16
  # If +strong+ is true+ parameters that can be mapped are accepted only. That means that
15
17
  # the instance created is invalid if +params+ contains any parameters that can't be
16
18
  # mapped to a parameter or a request body property of +operation+.
17
- def initialize(params, headers, operation, definitions, strong: false)
18
- @params = params.to_h
19
- @strong = strong == true
19
+ def initialize(params, request, operation, definitions, strong: false)
20
+ params = params.to_h
21
+ unassigned_params = params.dup
22
+
23
+ @params_to_be_validated = strong == true ? params.dup : {}
20
24
  @raw_attributes = {}
21
- @raw_additional_attributes = {}
22
25
 
23
26
  # Parameters
24
27
  operation.parameters.each do |name, parameter_model|
25
28
  parameter_model = parameter_model.resolve(definitions)
26
29
 
27
30
  @raw_attributes[name] = JSON.wrap(
28
- parameter_model.in == 'header' ? headers[name] : @params[name],
31
+ case parameter_model.in
32
+ when 'header'
33
+ request.headers[name]
34
+ when 'querystring'
35
+ query_params = request.query_parameters
36
+ keys = query_params.keys
37
+
38
+ unassigned_params.except!(*keys)
39
+ @params_to_be_validated.except!(*keys)
40
+
41
+ parameter_model.object? ? params.slice(*keys) : query_params.to_query
42
+ else
43
+ unassigned_params.delete(name)
44
+ end,
29
45
  parameter_model.schema.resolve(definitions),
30
46
  definitions,
31
47
  context: :request
@@ -37,23 +53,24 @@ module Jsapi
37
53
  &.schema&.resolve(definitions)
38
54
  if request_body_schema&.object?
39
55
  request_body = JSON.wrap(
40
- @params.except(*operation.parameters.keys),
56
+ unassigned_params,
41
57
  request_body_schema,
42
58
  definitions,
43
59
  context: :request
44
60
  )
45
61
  @raw_attributes.merge!(request_body.raw_attributes)
46
62
  @raw_additional_attributes = request_body.raw_additional_attributes
63
+ @params_to_be_validated.except!(*@raw_additional_attributes.keys)
64
+ else
65
+ @raw_additional_attributes = {}
47
66
  end
48
67
  end
49
68
 
50
69
  # Validates the request parameters. Returns true if the parameters are valid, false
51
70
  # otherwise. Detected errors are added to +errors+.
52
71
  def validate(errors)
53
- [
54
- validate_attributes(errors),
55
- !@strong || validate_parameters(@params, attributes, errors)
56
- ].all?
72
+ validate_attributes(errors) &&
73
+ validate_parameters(@params_to_be_validated, attributes, errors)
57
74
  end
58
75
 
59
76
  private
@@ -50,11 +50,27 @@ module Jsapi
50
50
  # Returns the \JSON representation of the response as a string.
51
51
  def to_json(*)
52
52
  schema = @response.schema.resolve(@definitions)
53
- if @response.locale
54
- I18n.with_locale(@response.locale) { jsonify(@object, schema) }
55
- else
56
- jsonify(@object, schema)
57
- end.to_json
53
+ with_locale { jsonify(@object, schema) }.to_json
54
+ end
55
+
56
+ # Writes the response in \JSON sequence text format to +stream+.
57
+ def write_json_seq_to(stream)
58
+ schema = @response.schema.resolve(@definitions)
59
+ with_locale do
60
+ items, item_schema =
61
+ if schema.array? && @object.respond_to?(:each)
62
+ [@object, schema.items.resolve(@definitions)]
63
+ else
64
+ [[@object], schema]
65
+ end
66
+
67
+ items.each do |item|
68
+ stream.write("\u001E") # Record separator (see RFC 7464)
69
+ stream.write(jsonify(item, item_schema).to_json)
70
+ stream.write("\n")
71
+ end
72
+ end
73
+ nil
58
74
  end
59
75
 
60
76
  private
@@ -138,6 +154,14 @@ module Jsapi
138
154
 
139
155
  properties.presence
140
156
  end
157
+
158
+ def with_locale(&block)
159
+ if @response.locale
160
+ I18n.with_locale(@response.locale, &block)
161
+ else
162
+ yield
163
+ end
164
+ end
141
165
  end
142
166
  end
143
167
  end
@@ -23,7 +23,7 @@ module Jsapi
23
23
  # Returns a hash representing the \OpenAPI callback object.
24
24
  def to_openapi(version, definitions)
25
25
  operations.transform_values do |operation|
26
- { operation.method => operation.to_openapi(version, definitions) }
26
+ OpenAPI::PathItem.new([operation]).to_openapi(version, definitions)
27
27
  end
28
28
  end
29
29
  end
@@ -231,9 +231,7 @@ module Jsapi
231
231
  openapi_paths =
232
232
  operations.group_by { |operation| operation.path || default_operation_path }
233
233
  .transform_values do |operations_by_path|
234
- operations_by_path.index_by(&:method).transform_values do |operation|
235
- operation.to_openapi(version, self)
236
- end
234
+ OpenAPI::PathItem.new(operations_by_path).to_openapi(version, self)
237
235
  end.presence
238
236
 
239
237
  openapi_objects =
@@ -271,10 +269,16 @@ module Jsapi
271
269
  else
272
270
  {
273
271
  # Order according to the OpenAPI specification 3.x
274
- openapi: version.minor.zero? ? '3.0.3' : '3.1.1',
272
+ openapi:
273
+ case version.minor
274
+ when 0 then '3.0.3'
275
+ when 1 then '3.1.1'
276
+ when 2 then '3.2.0'
277
+ end,
275
278
  info: openapi_objects[:info],
276
- servers: openapi_objects[:servers] ||
277
- [default_server&.to_openapi].compact.presence,
279
+ servers:
280
+ openapi_objects[:servers] ||
281
+ [default_server&.to_openapi(version)].compact.presence,
278
282
  paths: openapi_paths,
279
283
  components: {
280
284
  schemas: openapi_objects[:schemas],
@@ -29,13 +29,11 @@ module Jsapi
29
29
 
30
30
  # Returns a hash representing the \OpenAPI example object.
31
31
  def to_openapi(*)
32
- with_openapi_extensions(summary: summary, description: description).tap do |result|
33
- if external?
34
- result[:external_value] = value
35
- else
36
- result[:value] = value
32
+ with_openapi_extensions(
33
+ { summary: summary, description: description }.tap do |result|
34
+ result[external? ? :externalValue : :value] = value
37
35
  end
38
- end
36
+ )
39
37
  end
40
38
  end
41
39
  end
@@ -33,13 +33,15 @@ module Jsapi
33
33
  attribute :server, Server
34
34
 
35
35
  # Returns a hash representing the \OpenAPI link object.
36
- def to_openapi(*)
36
+ def to_openapi(version, *)
37
+ version = OpenAPI::Version.from(version)
38
+
37
39
  with_openapi_extensions(
38
40
  operationId: operation_id,
39
41
  parameters: parameters.presence,
40
42
  requestBody: request_body,
41
43
  description: description,
42
- server: server&.to_openapi
44
+ server: server&.to_openapi(version)
43
45
  )
44
46
  end
45
47
  end
@@ -18,12 +18,20 @@ module Jsapi
18
18
  # The authorization URL to be used for the flow.
19
19
  attribute :authorization_url, String
20
20
 
21
+ ##
22
+ # :attr: device_authorization_url
23
+ # The device authorization URL to be used for the flow.
24
+ #
25
+ # Note that the device authorization URL was introduced with \OpenAPI 3.2.
26
+ # It is omitted when generating an \OpenAPI document with a lower version.
27
+ attribute :device_authorization_url, String
28
+
21
29
  ##
22
30
  # :attr: refresh_url
23
31
  # The refresh URL to be used for the flow.
24
32
  #
25
33
  # Note that the refresh URL was introduced with \OpenAPI 3.0. It is
26
- # skipped when generating an \OpenAPI 2.0 document.
34
+ # omitted when generating an \OpenAPI document with a lower version.
27
35
  attribute :refresh_url, String
28
36
 
29
37
  ##
@@ -42,8 +50,9 @@ module Jsapi
42
50
 
43
51
  with_openapi_extensions(
44
52
  authorizationUrl: authorization_url,
53
+ deviceAuthorizationUrl: (device_authorization_url if version >= OpenAPI::V3_2),
45
54
  tokenUrl: token_url,
46
- refreshUrl: (refresh_url if version.major > 2),
55
+ refreshUrl: (refresh_url if version >= OpenAPI::V3_0),
47
56
  scopes: scopes.transform_values(&:description)
48
57
  )
49
58
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jsapi
4
+ module Meta
5
+ module OpenAPI
6
+ class PathItem # :nodoc:
7
+ def initialize(operations)
8
+ @operations = operations
9
+ end
10
+
11
+ def to_openapi(version, definitions)
12
+ version = OpenAPI::Version.from(version)
13
+
14
+ {}.tap do |fields|
15
+ @operations.each do |operation|
16
+ method = operation.method
17
+ standardized_method = method.downcase
18
+
19
+ if standard_method?(standardized_method, version)
20
+ fields[standardized_method] = operation.to_openapi(version, definitions)
21
+ elsif version >= OpenAPI::V3_2
22
+ additional_operations = fields[:additionalOperations] ||= {}
23
+ additional_operations[method] = operation.to_openapi(version, definitions)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def standard_method?(method, version)
32
+ case method
33
+ when 'delete', 'get', 'head', 'options', 'patch', 'post', 'put'
34
+ true
35
+ when 'trace'
36
+ version >= OpenAPI::V3_0
37
+ when 'query'
38
+ version >= OpenAPI::V3_2
39
+ else
40
+ false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -19,6 +19,8 @@ module Jsapi
19
19
  new(3, 0)
20
20
  when '3.1'
21
21
  new(3, 1)
22
+ when '3.2'
23
+ new(3, 2)
22
24
  else
23
25
  raise ArgumentError, "unsupported OpenAPI version: #{version.inspect}"
24
26
  end
@@ -46,6 +48,10 @@ module Jsapi
46
48
  minor <=> other.minor
47
49
  end
48
50
 
51
+ def inspect
52
+ "<#{self.class.name} #{self}>"
53
+ end
54
+
49
55
  def to_s # :nodoc:
50
56
  "#{major}.#{minor}"
51
57
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'openapi/extensions'
4
4
  require_relative 'openapi/version'
5
+ require_relative 'openapi/path_item'
5
6
 
6
7
  module Jsapi
7
8
  module Meta
@@ -9,6 +10,7 @@ module Jsapi
9
10
  V2_0 = Version.new(2, 0)
10
11
  V3_0 = Version.new(3, 0)
11
12
  V3_1 = Version.new(3, 1)
13
+ V3_2 = Version.new(3, 2)
12
14
  end
13
15
  end
14
16
  end
@@ -28,18 +28,8 @@ module Jsapi
28
28
 
29
29
  ##
30
30
  # :attr: method
31
- # The HTTP verb of the operation. Possible values are:
32
- #
33
- # - <code>"delete"</code>
34
- # - <code>"get"</code>
35
- # - <code>"head"</code>
36
- # - <code>"options"</code>
37
- # - <code>"patch"</code>
38
- # - <code>"post"</code>
39
- # - <code>"put"</code>
40
- #
41
- # The default HTTP verb is <code>"get"</code>.
42
- attribute :method, values: %w[delete get head options patch post put], default: 'get'
31
+ # The HTTP method of the operation, <code>"get"</code> by default.
32
+ attribute :method, String, default: 'get'
43
33
 
44
34
  ##
45
35
  # :attr: model
@@ -97,7 +87,7 @@ module Jsapi
97
87
 
98
88
  ##
99
89
  # :attr: summary
100
- # The short summary of the operation.
90
+ # The short description of the operation.
101
91
  attribute :summary, String
102
92
 
103
93
  ##
@@ -148,7 +138,9 @@ module Jsapi
148
138
  end
149
139
  result[:schemes] = schemes if schemes.present?
150
140
  elsif servers.present?
151
- result[:servers] = servers.map(&:to_openapi)
141
+ result[:servers] = servers.map do |server|
142
+ server.to_openapi(version)
143
+ end
152
144
  end
153
145
  # Parameters (and request body)
154
146
  result[:parameters] = parameters.values.flat_map do |parameter|
@@ -9,6 +9,11 @@ module Jsapi
9
9
 
10
10
  delegate_missing_to :schema
11
11
 
12
+ ##
13
+ # :attr: content_type
14
+ # The content type used to describe complex parameters in \OpenAPI 3.0 and higher.
15
+ attribute :content_type, String
16
+
12
17
  ##
13
18
  # :attr: deprecated
14
19
  # Specifies whether or not the parameter is deprecated.
@@ -31,9 +36,10 @@ module Jsapi
31
36
  # - <code>"header"</code>
32
37
  # - <code>"path"</code>
33
38
  # - <code>"query"</code>
39
+ # - <code>"querystring"</code>
34
40
  #
35
41
  # The default location is <code>"query"</code>.
36
- attribute :in, String, values: %w[header path query], default: 'query'
42
+ attribute :in, String, values: %w[header path query querystring], default: 'query'
37
43
 
38
44
  ##
39
45
  # :attr_reader: name
@@ -54,8 +60,16 @@ module Jsapi
54
60
  @name = name.to_s
55
61
 
56
62
  keywords = keywords.dup
57
- super(keywords.extract!(:deprecated, :description, :examples, :in, :openapi_extensions))
58
-
63
+ super(
64
+ keywords.extract!(
65
+ :content_type,
66
+ :deprecated,
67
+ :description,
68
+ :examples,
69
+ :in,
70
+ :openapi_extensions
71
+ )
72
+ )
59
73
  add_example(value: keywords.delete(:example)) if keywords.key?(:example)
60
74
  keywords[:ref] = keywords.delete(:schema) if keywords.key?(:schema)
61
75
 
@@ -75,12 +89,13 @@ module Jsapi
75
89
  # Returns a hash representing the \OpenAPI parameter object.
76
90
  def to_openapi(version, definitions)
77
91
  version = OpenAPI::Version.from(version)
78
- schema = self.schema.resolve(definitions)
79
92
 
80
- openapi_parameter(
93
+ openapi_parameter_object(
81
94
  name,
82
- schema,
95
+ schema.resolve(definitions),
83
96
  version,
97
+ location: self.in,
98
+ content_type: content_type || ('text/plain' if self.in == 'querystring'),
84
99
  description: description,
85
100
  required: required?,
86
101
  deprecated: deprecated?,
@@ -92,39 +107,30 @@ module Jsapi
92
107
  # Returns an array of hashes representing the \OpenAPI parameter objects.
93
108
  def to_openapi_parameters(version, definitions)
94
109
  version = OpenAPI::Version.from(version)
110
+ is_querystring = self.in == 'querystring'
95
111
  schema = self.schema.resolve(definitions)
96
112
 
97
- if schema.object?
113
+ if schema.object? && (version < OpenAPI::V3_2 || !is_querystring)
98
114
  explode_parameter(
99
- name,
115
+ is_querystring ? nil : name,
100
116
  schema,
101
117
  version,
102
118
  definitions,
119
+ location: is_querystring ? 'query' : self.in,
103
120
  required: required?,
104
121
  deprecated: deprecated?
105
122
  )
106
123
  else
107
- [
108
- openapi_parameter(
109
- name,
110
- schema,
111
- version,
112
- description: description,
113
- required: required?,
114
- deprecated: deprecated?,
115
- allow_empty_value: allow_empty_value?,
116
- examples: examples
117
- )
118
- ]
119
- end
124
+ [to_openapi(version, definitions)]
125
+ end.compact
120
126
  end
121
127
 
122
128
  private
123
129
 
124
- def explode_parameter(name, schema, version, definitions, required:, deprecated:)
130
+ def explode_parameter(name, schema, version, definitions, location:, required:, deprecated:)
125
131
  schema.resolve_properties(definitions, context: :request).values.flat_map do |property|
126
132
  property_schema = property.schema.resolve(definitions)
127
- parameter_name = "#{name}[#{property.name}]"
133
+ parameter_name = name ? "#{name}[#{property.name}]" : property.name
128
134
  required = (required && property.required?).presence
129
135
  deprecated = (deprecated || property_schema.deprecated?).presence
130
136
 
@@ -134,62 +140,79 @@ module Jsapi
134
140
  property_schema,
135
141
  version,
136
142
  definitions,
143
+ location: location,
137
144
  required: required,
138
145
  deprecated: deprecated
139
146
  )
140
147
  else
141
148
  [
142
- openapi_parameter(
149
+ openapi_parameter_object(
143
150
  parameter_name,
144
151
  property_schema,
145
152
  version,
153
+ location: location,
146
154
  description: property_schema.description,
147
155
  required: required,
148
156
  deprecated: deprecated,
149
157
  allow_empty_value: property.schema.existence <= Existence::ALLOW_EMPTY
150
158
  )
151
159
  ]
152
- end
160
+ end.compact
153
161
  end
154
162
  end
155
163
 
156
- def openapi_parameter(name, schema, version,
157
- allow_empty_value:,
158
- deprecated:,
159
- description:,
160
- required:,
161
- examples: nil)
164
+ def openapi_parameter_object(name, schema, version,
165
+ allow_empty_value:,
166
+ deprecated:,
167
+ description:,
168
+ location:,
169
+ required:,
170
+ content_type: nil,
171
+ examples: nil)
162
172
 
163
- name = schema.array? ? "#{name}[]" : name
173
+ return if location == 'querystring' && version < OpenAPI::V3_2
174
+
175
+ if schema.object? && version == OpenAPI::V2_0
176
+ raise "OpenAPI 2.0 doesn't allow object parameters in #{location}"
177
+ end
178
+
179
+ name = "#{name}[]" if schema.array?
164
180
 
165
181
  with_openapi_extensions(
166
- if version.major == 2
167
- raise "OpenAPI 2.0 doesn't allow object parameters " \
168
- "in #{self.in}" if schema.object?
169
-
170
- {
171
- name: name,
172
- in: self.in,
173
- description: description,
174
- required: required.presence,
175
- allowEmptyValue: allow_empty_value.presence,
176
- collectionFormat: ('multi' if schema.array?)
177
- }.merge(schema.to_openapi(version))
178
- else
179
- {
180
- name: name,
181
- in: self.in,
182
- description: description,
183
- required: required.presence,
184
- allowEmptyValue: allow_empty_value.presence,
185
- deprecated: deprecated.presence,
186
- schema: schema.to_openapi(version).except(:deprecated),
187
- examples: examples&.transform_values(&:to_openapi).presence
188
-
189
- # NOTE: collectionFormat is replaced by 'style' and 'explode'.
190
- # The default values are equal to 'multi'.
191
- }
192
- end
182
+ name: name,
183
+ in: location,
184
+ description: description,
185
+ required: required.presence,
186
+ allowEmptyValue: allow_empty_value.presence,
187
+ **if version == OpenAPI::V2_0
188
+ {
189
+ collectionFormat: ('multi' if schema.array?),
190
+ **schema.to_openapi(version)
191
+ }
192
+ else
193
+ openapi_schema = schema.to_openapi(version).except(:deprecated)
194
+ openapi_examples = examples&.transform_values(&:to_openapi).presence
195
+ {
196
+ deprecated: deprecated.presence,
197
+ **if content_type.blank?
198
+ # simple scenario
199
+ {
200
+ schema: openapi_schema,
201
+ examples: openapi_examples
202
+ }
203
+ else
204
+ # complex scenario
205
+ {
206
+ content: {
207
+ content_type => {
208
+ schema: openapi_schema,
209
+ examples: openapi_examples
210
+ }.compact
211
+ }
212
+ }
213
+ end
214
+ }
215
+ end
193
216
  )
194
217
  end
195
218
  end
@@ -52,8 +52,9 @@ module Jsapi
52
52
  name: 'body',
53
53
  in: 'body',
54
54
  description: description,
55
- required: required?
56
- }.merge(schema.to_openapi(OpenAPI::V2_0))
55
+ required: required?,
56
+ **schema.to_openapi(OpenAPI::V2_0)
57
+ }
57
58
  )
58
59
  end
59
60
 
@@ -8,12 +8,13 @@ module Jsapi
8
8
  include OpenAPI::Extensions
9
9
 
10
10
  JSON_TYPE = %r{(^application/|^text/|\+)json$}.freeze # :nodoc:
11
+ JSON_SEQ_TYPE = 'application/json-seq' # :nodoc:
11
12
 
12
13
  delegate_missing_to :schema
13
14
 
14
15
  ##
15
16
  # :attr: content_type
16
- # The content type. <code>"application/json"</code> by default.
17
+ # The content type, <code>"application/json"</code> by default.
17
18
  attribute :content_type, String, default: 'application/json'
18
19
 
19
20
  ##
@@ -46,12 +47,17 @@ module Jsapi
46
47
  # The Schema of the response.
47
48
  attribute :schema, accessors: %i[reader]
48
49
 
50
+ ##
51
+ # :attr: summary
52
+ # The short description of the response. Applies to \OpenAPI 3.2 and higher.
53
+ attribute :summary, String
54
+
49
55
  def initialize(keywords = {})
50
56
  keywords = keywords.dup
51
57
  super(
52
58
  keywords.extract!(
53
59
  :content_type, :description, :examples, :headers,
54
- :links, :locale, :openapi_extensions
60
+ :links, :locale, :openapi_extensions, :summary
55
61
  )
56
62
  )
57
63
  add_example(value: keywords.delete(:example)) if keywords.key?(:example)
@@ -66,12 +72,17 @@ module Jsapi
66
72
  content_type.match?(JSON_TYPE)
67
73
  end
68
74
 
75
+ # Returns true if content type is <code>"application/json-seq"</code>.
76
+ def json_seq_type?
77
+ content_type == JSON_SEQ_TYPE
78
+ end
79
+
69
80
  # Returns a hash representing the \OpenAPI response object.
70
81
  def to_openapi(version, definitions)
71
82
  version = OpenAPI::Version.from(version)
72
83
 
73
84
  with_openapi_extensions(
74
- if version.major == 2
85
+ if version == OpenAPI::V2_0
75
86
  {
76
87
  description: description,
77
88
  schema: schema.to_openapi(version),
@@ -86,17 +97,24 @@ module Jsapi
86
97
  }
87
98
  else
88
99
  {
100
+ summary: (summary if version >= OpenAPI::V3_2),
89
101
  description: description,
102
+ headers: headers.transform_values do |header|
103
+ header.to_openapi(version)
104
+ end.presence,
90
105
  content: {
91
106
  content_type => {
92
- schema: schema.to_openapi(version),
107
+ **if json_seq_type? && schema.array? && version >= OpenAPI::V3_2
108
+ { itemSchema: schema.items.to_openapi(version) }
109
+ else
110
+ { schema: schema.to_openapi(version) }
111
+ end,
93
112
  examples: examples.transform_values(&:to_openapi).presence
94
113
  }.compact
95
114
  },
96
- headers: headers.transform_values do |header|
97
- header.to_openapi(version)
98
- end.presence,
99
- links: links.transform_values(&:to_openapi).presence
115
+ links: links.transform_values do |link|
116
+ link.to_openapi(version)
117
+ end.presence
100
118
  }
101
119
  end
102
120
  )
@@ -121,7 +121,7 @@ module Jsapi
121
121
  deprecated: deprecated?.presence
122
122
  }
123
123
  else
124
- # OpenAPI 3.1
124
+ # OpenAPI 3.1 and higher
125
125
  {
126
126
  type: nullable? ? [type, 'null'] : type,
127
127
  examples: examples.presence,
@@ -4,8 +4,16 @@ module Jsapi
4
4
  module Meta
5
5
  module Schema
6
6
  class Discriminator < Model::Base
7
+ include OpenAPI::Extensions
8
+
9
+ ##
10
+ # :attr: default_mapping
11
+ # Applies to \OpenAPI 3.2 and higher.
12
+ attribute :default_mapping, String
13
+
7
14
  ##
8
15
  # :attr: mappings
16
+ # Applies to \OpenAPI 3.0 and higher.
9
17
  attribute :mappings, { Object => String }
10
18
 
11
19
  ##
@@ -15,12 +23,14 @@ module Jsapi
15
23
  # Returns a hash representing the \OpenAPI discriminator object.
16
24
  def to_openapi(version, *)
17
25
  version = OpenAPI::Version.from(version)
18
- return property_name if version.major == 2
26
+ return property_name if version < OpenAPI::V3_0
19
27
 
20
- {
28
+ result = {
21
29
  propertyName: property_name,
22
- mapping: mappings.transform_keys(&:to_s).presence
23
- }.compact
30
+ mapping: mappings.transform_keys(&:to_s).presence,
31
+ defaultMapping: (default_mapping if version >= OpenAPI::V3_2)
32
+ }
33
+ version >= OpenAPI::V3_1 ? with_openapi_extensions(result) : result.compact
24
34
  end
25
35
  end
26
36
  end
@@ -57,17 +57,36 @@ module Jsapi
57
57
 
58
58
  properties = resolve_properties(definitions, context: context)
59
59
 
60
- property = properties[discriminator.property_name]
61
- raise InvalidValueError.new('discriminator property',
62
- discriminator.property_name,
63
- valid_values: properties.keys) if property.nil?
60
+ discriminating_property = properties[discriminator.property_name]
61
+ if discriminating_property.nil?
62
+ raise InvalidValueError.new('discriminator property',
63
+ discriminator.property_name,
64
+ valid_values: properties.keys)
65
+ end
66
+
67
+ discriminating_value = discriminating_property.reader.call(object)
68
+ if discriminating_value.nil?
69
+ discriminating_value = discriminating_property.default_value(
70
+ definitions,
71
+ context: context
72
+ )
73
+ if discriminating_value.nil? && discriminator.default_mapping.nil?
74
+ raise "discriminating value can't be nil"
75
+ end
76
+ end
64
77
 
65
- value = property.reader.call(object)
66
- value = property.default_value(definitions, context: context) if value.nil?
67
- raise "#{discriminator.property_name} can't be nil" if value.nil?
78
+ schema_name = discriminator.mapping(discriminating_value) || discriminating_value
68
79
 
69
- schema = definitions.find_schema(discriminator.mapping(value) || value)
70
- raise "inheriting schema couldn't be found: #{value.inspect}" if schema.nil?
80
+ schema = definitions.find_schema(schema_name)
81
+ if schema.nil?
82
+ default_mapping = discriminator.default_mapping
83
+ schema = definitions.find_schema(default_mapping) unless default_mapping.nil?
84
+
85
+ if schema.nil?
86
+ raise "inheriting schema couldn't be found: " \
87
+ "#{[schema_name, default_mapping].compact.map(&:inspect).join(' or ')}"
88
+ end
89
+ end
71
90
 
72
91
  schema.resolve(definitions).resolve_schema(object, definitions, context: context)
73
92
  end
@@ -43,7 +43,7 @@ module Jsapi
43
43
 
44
44
  def to_openapi_validation(version)
45
45
  version = OpenAPI::Version.from(version)
46
- return to_json_schema_validation if version.major == 3 && version.minor == 1
46
+ return to_json_schema_validation if version >= OpenAPI::V3_1
47
47
  return super unless exclusive
48
48
 
49
49
  { maximum: value, exclusiveMaximum: true }
@@ -43,7 +43,7 @@ module Jsapi
43
43
 
44
44
  def to_openapi_validation(version)
45
45
  version = OpenAPI::Version.from(version)
46
- return to_json_schema_validation if version.major == 3 && version.minor == 1
46
+ return to_json_schema_validation if version >= OpenAPI::V3_1
47
47
  return super unless exclusive
48
48
 
49
49
  { minimum: value, exclusiveMinimum: true }
@@ -24,12 +24,11 @@ module Jsapi
24
24
  attribute :name, String
25
25
 
26
26
  # Returns a hash representing the \OpenAPI security scheme object.
27
- def to_openapi(*)
27
+ def to_openapi(version, *)
28
+ version = OpenAPI::Version.from(version)
29
+
28
30
  with_openapi_extensions(
29
- type: 'apiKey',
30
- name: name,
31
- in: self.in,
32
- description: description
31
+ base_openapi_fields('apiKey', version).merge(name: name, in: self.in)
33
32
  )
34
33
  end
35
34
  end
@@ -8,6 +8,22 @@ module Jsapi
8
8
  # :attr: description
9
9
  # The description of the security scheme.
10
10
  attribute :description, String
11
+
12
+ ##
13
+ # :attr: deprecated
14
+ # Specifies whether or not the security scheme is deprecated.
15
+ # Applies to \OpenAPI 3.2 and higher.
16
+ attribute :deprecated, values: [true, false]
17
+
18
+ private
19
+
20
+ def base_openapi_fields(type, version)
21
+ {
22
+ type: type,
23
+ description: description,
24
+ deprecated: (deprecated?.presence if version >= OpenAPI::V3_2)
25
+ }
26
+ end
11
27
  end
12
28
  end
13
29
  end
@@ -13,17 +13,10 @@ module Jsapi
13
13
  version = OpenAPI::Version.from(version)
14
14
 
15
15
  with_openapi_extensions(
16
- if version.major == 2
17
- {
18
- type: 'basic',
19
- description: description
20
- }
16
+ if version < OpenAPI::V3_0
17
+ base_openapi_fields('basic', version)
21
18
  else
22
- {
23
- type: 'http',
24
- scheme: 'basic',
25
- description: description
26
- }
19
+ base_openapi_fields('http', version).merge(scheme: 'basic')
27
20
  end
28
21
  )
29
22
  end
@@ -7,7 +7,7 @@ module Jsapi
7
7
  # Specifies a security scheme based on bearer authentication.
8
8
  #
9
9
  # Note that Bearer authentication was introduced with \OpenAPI 3.0. Thus, a security
10
- # scheme of this class is skipped when generating an \OpenAPI 2.0 document.
10
+ # scheme of this class is omitted when generating an \OpenAPI 2.0 document.
11
11
  class Bearer < Base
12
12
  include OpenAPI::Extensions
13
13
 
@@ -16,17 +16,17 @@ module Jsapi
16
16
  # The format of the bearer token.
17
17
  attribute :bearer_format, String
18
18
 
19
- # Returns a hash representing the \OpenAPI security scheme object, or
20
- # +nil+ if <code>version.major</code> is 2.
19
+ # Returns a hash representing the \OpenAPI security scheme object, or +nil+
20
+ # if <code>version</code> is less than \OpenAPI 3.0.
21
21
  def to_openapi(version, *)
22
22
  version = OpenAPI::Version.from(version)
23
- return if version.major == 2
23
+ return if version < OpenAPI::V3_0
24
24
 
25
25
  with_openapi_extensions(
26
- type: 'http',
27
- scheme: 'bearer',
28
- bearerFormat: bearer_format,
29
- description: description
26
+ base_openapi_fields('http', version).merge(
27
+ scheme: 'bearer',
28
+ bearerFormat: bearer_format
29
+ )
30
30
  )
31
31
  end
32
32
  end
@@ -18,15 +18,13 @@ module Jsapi
18
18
  attribute :scheme, String
19
19
 
20
20
  # Returns a hash representing the \OpenAPI security scheme object, or +nil+
21
- # if <code>version.major</code> is 2.
21
+ # if <code>version</code> is less than \OpenAPI 3.0.
22
22
  def to_openapi(version, *)
23
23
  version = OpenAPI::Version.from(version)
24
- return if version.major == 2
24
+ return if version < OpenAPI::V3_0
25
25
 
26
26
  with_openapi_extensions(
27
- type: 'http',
28
- scheme: scheme,
29
- description: description
27
+ base_openapi_fields('http', version).merge(scheme: scheme)
30
28
  )
31
29
  end
32
30
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jsapi
4
+ module Meta
5
+ module SecurityScheme
6
+ # Specifies a security scheme based on MutualTLS.
7
+ class MutualTLS < Base
8
+ include OpenAPI::Extensions
9
+
10
+ # Returns a hash representing the \OpenAPI security scheme object, or +nil+
11
+ # if <code>version</code> is less than \OpenAPI 3.1.
12
+ def to_openapi(version, *)
13
+ version = OpenAPI::Version.from(version)
14
+ return if version < OpenAPI::V3_1
15
+
16
+ with_openapi_extensions(
17
+ base_openapi_fields('mutualTLS', version)
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -9,34 +9,50 @@ module Jsapi
9
9
 
10
10
  ##
11
11
  # :attr: oauth_flows
12
- # The hash containing the OAuth flows. Possible keys are:
12
+ # Maps one or more of the following keys to OAuthFlow objects.
13
13
  #
14
14
  # - <code>"authorization_code"</code>
15
15
  # - <code>"client_credentials"</code>
16
+ # - <code>"device_authorization"</code>
16
17
  # - <code>"implicit"</code>
17
18
  # - <code>"password"</code>
18
19
  #
19
- # Values are OAuthFlow objects.
20
+ # Note that <code>"device_authorization"</code> was introduced with \OpenAPI 3.2.
21
+ # This entry is omitted when generating an \OpenAPI document with a lower version.
20
22
  attribute :oauth_flows, { String => OAuthFlow },
21
- keys: %w[authorization_code client_credentials implicit password]
23
+ keys: %w[authorization_code
24
+ client_credentials
25
+ device_authorization
26
+ implicit
27
+ password]
28
+ ##
29
+ # :attr: oauth2_metadata_url
30
+ # The URL of the OAuth2 authorization server metadata.
31
+ # Applies to \OpenAPI 3.2 and higher.
32
+ attribute :oauth2_metadata_url, String
22
33
 
23
34
  # Returns a hash representing the \OpenAPI security scheme object.
24
35
  def to_openapi(version, *)
25
36
  version = OpenAPI::Version.from(version)
26
37
 
27
- with_openapi_extensions(type: 'oauth2', description: description).tap do |h|
28
- if oauth_flows.any?
29
- if version.major == 2
30
- key, oauth_flow = oauth_flows.first
31
- h[:flow] = key.to_s
32
- h.merge!(oauth_flow.to_openapi(version))
33
- else
34
- h[:flows] = oauth_flows.to_h do |key, value|
38
+ with_openapi_extensions(
39
+ base_openapi_fields('oauth2', version).tap do |fields|
40
+ flows = oauth_flows
41
+ flows = flows.except('device_authorization') unless version >= OpenAPI::V3_2
42
+
43
+ if version >= OpenAPI::V3_0
44
+ fields[:flows] = flows.to_h do |key, value|
35
45
  [key.to_s.camelize(:lower).to_sym, value.to_openapi(version)]
36
- end
46
+ end if flows.any?
47
+
48
+ fields[:oauth2MetadataUrl] = oauth2_metadata_url if version >= OpenAPI::V3_2
49
+ elsif flows.one?
50
+ key, flow = flows.first
51
+ fields[:flow] = key.to_s
52
+ fields.merge!(flow.to_openapi(version))
37
53
  end
38
54
  end
39
- end
55
+ )
40
56
  end
41
57
  end
42
58
  end
@@ -4,9 +4,6 @@ module Jsapi
4
4
  module Meta
5
5
  module SecurityScheme
6
6
  # Specifies a security scheme based on OpenID Connect.
7
- #
8
- # OpenID Connect was introduced with \OpenAPI 3.0. Thus, a security scheme of
9
- # this class is skipped when generating an \OpenAPI 2.0 document.
10
7
  class OpenIDConnect < Base
11
8
  include OpenAPI::Extensions
12
9
 
@@ -15,15 +12,15 @@ module Jsapi
15
12
  attribute :open_id_connect_url, String
16
13
 
17
14
  # Returns a hash representing the \OpenAPI security scheme object, or +nil+
18
- # if <code>version.major</code> is 2.
15
+ # if <code>version</code> is less than \OpenAPI 3.0.
19
16
  def to_openapi(version, *)
20
17
  version = OpenAPI::Version.from(version)
21
- return if version.major == 2
18
+ return if version < OpenAPI::V3_0
22
19
 
23
20
  with_openapi_extensions(
24
- type: 'openIdConnect',
25
- openIdConnectUrl: open_id_connect_url,
26
- description: description
21
+ base_openapi_fields('openIdConnect', version).merge(
22
+ openIdConnectUrl: open_id_connect_url
23
+ )
27
24
  )
28
25
  end
29
26
  end
@@ -3,6 +3,7 @@
3
3
  require_relative 'security_scheme/base'
4
4
  require_relative 'security_scheme/api_key'
5
5
  require_relative 'security_scheme/http'
6
+ require_relative 'security_scheme/mutual_tls'
6
7
  require_relative 'security_scheme/oauth2'
7
8
  require_relative 'security_scheme/open_id_connect'
8
9
 
@@ -31,6 +32,8 @@ module Jsapi
31
32
  HTTP.new(keywords.merge(scheme: 'basic'))
32
33
  when 'http' # OpenAPI 3.0 and higher
33
34
  HTTP.new(keywords)
35
+ when 'mutual_tls' # OpenAPI 3.1 and higher
36
+ MutualTLS.new(keywords)
34
37
  when 'oauth2'
35
38
  OAuth2.new(keywords)
36
39
  when 'open_id_connect' # OpenAPI 3.0 and higher
@@ -11,6 +11,11 @@ module Jsapi
11
11
  # The description of the server.
12
12
  attribute :description, String
13
13
 
14
+ ##
15
+ # :attr: name
16
+ # The optional unique name of the server. Applies to \OpenAPI 3.2 and higher.
17
+ attribute :name, String
18
+
14
19
  ##
15
20
  # :attr: url
16
21
  # The absolute or relative URL of the server.
@@ -22,10 +27,13 @@ module Jsapi
22
27
  attribute :variables, { String => ServerVariable }
23
28
 
24
29
  # Returns a hash representing the \OpenAPI server object.
25
- def to_openapi(*)
30
+ def to_openapi(version, *)
31
+ version = OpenAPI::Version.from(version)
32
+
26
33
  with_openapi_extensions(
27
34
  url: url,
28
35
  description: description,
36
+ name: (name if version >= OpenAPI::V3_2),
29
37
  variables: variables.transform_values(&:to_openapi).presence
30
38
  )
31
39
  end
@@ -16,17 +16,47 @@ module Jsapi
16
16
  # The ExternalDocumentation object.
17
17
  attribute :external_docs, ExternalDocumentation
18
18
 
19
+ ##
20
+ # :attr: kind
21
+ # The category of the tag. Applies to \OpenAPI 3.2 and higher.
22
+ attribute :kind, String
23
+
19
24
  ##
20
25
  # :attr: name
21
26
  # The name of the tag.
22
27
  attribute :name, String
23
28
 
29
+ ##
30
+ # :attr: parent
31
+ # The name of the parent tag. Applies to \OpenAPI 3.2 and higher.
32
+ attribute :parent, String
33
+
34
+ ##
35
+ # :attr: summary
36
+ # The short summary of the tag. Applies to \OpenAPI 3.2 and higher.
37
+ attribute :summary, String
38
+
24
39
  # Returns a hash representing the \OpenAPI tag object.
25
- def to_openapi(*)
40
+ def to_openapi(version, *)
41
+ version = OpenAPI::Version.from(version)
42
+
26
43
  with_openapi_extensions(
27
- name: name,
28
- description: description,
29
- externalDocs: external_docs&.to_openapi
44
+ if version >= OpenAPI::V3_2
45
+ {
46
+ name: name,
47
+ summary: summary,
48
+ description: description,
49
+ externalDocs: external_docs&.to_openapi,
50
+ parent: parent,
51
+ kind: kind
52
+ }
53
+ else
54
+ {
55
+ name: name,
56
+ description: description,
57
+ externalDocs: external_docs&.to_openapi
58
+ }
59
+ end
30
60
  )
31
61
  end
32
62
  end
data/lib/jsapi/version.rb CHANGED
@@ -5,6 +5,6 @@ module Jsapi
5
5
  # NOTE: See https://bundler.io/guides/creating_gem.html
6
6
 
7
7
  # The current GEM version.
8
- VERSION = '1.2'
8
+ VERSION = '1.3'
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsapi
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.2'
4
+ version: '1.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Göller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-30 00:00:00.000000000 Z
11
+ date: 2025-11-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: denis@dmgoeller.de
@@ -80,6 +80,7 @@ files:
80
80
  - lib/jsapi/meta/oauth_flow.rb
81
81
  - lib/jsapi/meta/openapi.rb
82
82
  - lib/jsapi/meta/openapi/extensions.rb
83
+ - lib/jsapi/meta/openapi/path_item.rb
83
84
  - lib/jsapi/meta/openapi/version.rb
84
85
  - lib/jsapi/meta/operation.rb
85
86
  - lib/jsapi/meta/parameter.rb
@@ -128,6 +129,7 @@ files:
128
129
  - lib/jsapi/meta/security_scheme/http/basic.rb
129
130
  - lib/jsapi/meta/security_scheme/http/bearer.rb
130
131
  - lib/jsapi/meta/security_scheme/http/other.rb
132
+ - lib/jsapi/meta/security_scheme/mutual_tls.rb
131
133
  - lib/jsapi/meta/security_scheme/oauth2.rb
132
134
  - lib/jsapi/meta/security_scheme/open_id_connect.rb
133
135
  - lib/jsapi/meta/server.rb