jsapi 1.4 → 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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jsapi/controller/actions/class_methods.rb +61 -0
  3. data/lib/jsapi/controller/actions.rb +13 -0
  4. data/lib/jsapi/controller/authentication/class_methods.rb +65 -0
  5. data/lib/jsapi/controller/authentication/credentials/api_key.rb +24 -0
  6. data/lib/jsapi/controller/authentication/credentials/http/base.rb +25 -0
  7. data/lib/jsapi/controller/authentication/credentials/http/basic.rb +34 -0
  8. data/lib/jsapi/controller/authentication/credentials/http/bearer.rb +30 -0
  9. data/lib/jsapi/controller/authentication/credentials/http.rb +5 -0
  10. data/lib/jsapi/controller/authentication/credentials.rb +38 -0
  11. data/lib/jsapi/controller/authentication.rb +70 -0
  12. data/lib/jsapi/controller/base.rb +5 -4
  13. data/lib/jsapi/controller/methods/callbacks/callback.rb +80 -0
  14. data/lib/jsapi/controller/methods/callbacks/class_methods.rb +62 -0
  15. data/lib/jsapi/controller/methods/callbacks.rb +54 -0
  16. data/lib/jsapi/controller/methods.rb +209 -116
  17. data/lib/jsapi/controller/parameters.rb +24 -20
  18. data/lib/jsapi/controller/response.rb +71 -39
  19. data/lib/jsapi/controller.rb +2 -1
  20. data/lib/jsapi/dsl/base.rb +38 -5
  21. data/lib/jsapi/dsl/class_methods.rb +2 -2
  22. data/lib/jsapi/dsl/definitions.rb +41 -27
  23. data/lib/jsapi/dsl/operation.rb +10 -109
  24. data/lib/jsapi/dsl/parameter.rb +1 -1
  25. data/lib/jsapi/dsl/path.rb +41 -18
  26. data/lib/jsapi/dsl/request_body.rb +1 -1
  27. data/lib/jsapi/dsl/response.rb +9 -6
  28. data/lib/jsapi/dsl/schema.rb +11 -5
  29. data/lib/jsapi/dsl/shared_operation_methods.rb +140 -0
  30. data/lib/jsapi/dsl.rb +1 -2
  31. data/lib/jsapi/json/array.rb +2 -2
  32. data/lib/jsapi/json/object.rb +6 -6
  33. data/lib/jsapi/json.rb +4 -6
  34. data/lib/jsapi/media/range.rb +102 -0
  35. data/lib/jsapi/media/type.rb +70 -0
  36. data/lib/jsapi/media/type_and_subtype.rb +38 -0
  37. data/lib/jsapi/media.rb +9 -0
  38. data/lib/jsapi/messages.rb +19 -0
  39. data/lib/jsapi/meta/callback/base.rb +63 -8
  40. data/lib/jsapi/meta/content.rb +59 -0
  41. data/lib/jsapi/meta/definitions.rb +299 -153
  42. data/lib/jsapi/meta/example/base.rb +41 -8
  43. data/lib/jsapi/meta/existence.rb +4 -2
  44. data/lib/jsapi/meta/header/base.rb +4 -2
  45. data/lib/jsapi/meta/info.rb +3 -1
  46. data/lib/jsapi/meta/license.rb +11 -5
  47. data/lib/jsapi/meta/model/attributes/class_methods.rb +150 -0
  48. data/lib/jsapi/meta/model/attributes/frozen_error.rb +16 -0
  49. data/lib/jsapi/meta/model/attributes/type_caster.rb +56 -0
  50. data/lib/jsapi/meta/model/attributes.rb +24 -118
  51. data/lib/jsapi/meta/model/base.rb +2 -5
  52. data/lib/jsapi/meta/model/reference.rb +46 -10
  53. data/lib/jsapi/meta/model/wrappable.rb +23 -0
  54. data/lib/jsapi/meta/model/wrapper.rb +26 -0
  55. data/lib/jsapi/meta/model.rb +2 -1
  56. data/lib/jsapi/meta/oauth_flow.rb +1 -1
  57. data/lib/jsapi/meta/openapi/extensions.rb +5 -6
  58. data/lib/jsapi/meta/openapi/version.rb +16 -4
  59. data/lib/jsapi/meta/operation.rb +177 -71
  60. data/lib/jsapi/meta/parameter/base.rb +10 -6
  61. data/lib/jsapi/meta/parameter/wrapper.rb +13 -0
  62. data/lib/jsapi/meta/parameter.rb +3 -0
  63. data/lib/jsapi/meta/path.rb +59 -13
  64. data/lib/jsapi/meta/pathname.rb +6 -3
  65. data/lib/jsapi/meta/property.rb +10 -0
  66. data/lib/jsapi/meta/request_body/base.rb +69 -32
  67. data/lib/jsapi/meta/request_body/wrapper.rb +13 -0
  68. data/lib/jsapi/meta/request_body.rb +3 -0
  69. data/lib/jsapi/meta/rescue_handler.rb +18 -17
  70. data/lib/jsapi/meta/response/base.rb +82 -58
  71. data/lib/jsapi/meta/response/reference.rb +11 -1
  72. data/lib/jsapi/meta/response/wrapper.rb +26 -0
  73. data/lib/jsapi/meta/response.rb +3 -0
  74. data/lib/jsapi/meta/schema/additional_properties.rb +8 -0
  75. data/lib/jsapi/meta/schema/array.rb +20 -8
  76. data/lib/jsapi/meta/schema/base.rb +10 -9
  77. data/lib/jsapi/meta/schema/boundary.rb +1 -0
  78. data/lib/jsapi/meta/schema/numeric.rb +26 -20
  79. data/lib/jsapi/meta/schema/object.rb +60 -44
  80. data/lib/jsapi/meta/schema/reference.rb +1 -8
  81. data/lib/jsapi/meta/schema/string.rb +12 -6
  82. data/lib/jsapi/meta/schema/wrapper.rb +31 -0
  83. data/lib/jsapi/meta/schema.rb +22 -9
  84. data/lib/jsapi/meta/security_requirement.rb +2 -2
  85. data/lib/jsapi/meta/security_scheme/api_key.rb +5 -2
  86. data/lib/jsapi/meta/security_scheme/base.rb +7 -5
  87. data/lib/jsapi/meta/security_scheme/http/basic.rb +5 -7
  88. data/lib/jsapi/meta/security_scheme/http/bearer.rb +5 -5
  89. data/lib/jsapi/meta/security_scheme/http/other.rb +1 -3
  90. data/lib/jsapi/meta/security_scheme/mutual_tls.rb +1 -3
  91. data/lib/jsapi/meta/security_scheme/oauth2.rb +18 -13
  92. data/lib/jsapi/meta/security_scheme/open_id_connect.rb +4 -4
  93. data/lib/jsapi/meta/security_scheme.rb +4 -4
  94. data/lib/jsapi/meta/server.rb +4 -2
  95. data/lib/jsapi/meta/tag.rb +9 -3
  96. data/lib/jsapi/meta.rb +2 -1
  97. data/lib/jsapi/model/base.rb +1 -1
  98. data/lib/jsapi/status/base.rb +35 -0
  99. data/lib/jsapi/status/code.rb +113 -0
  100. data/lib/jsapi/status/default.rb +16 -0
  101. data/lib/jsapi/status/range.rb +35 -0
  102. data/lib/jsapi/status.rb +37 -0
  103. data/lib/jsapi/version.rb +1 -1
  104. data/lib/jsapi.rb +3 -3
  105. metadata +36 -10
  106. data/lib/jsapi/controller/parameters_invalid.rb +0 -27
  107. data/lib/jsapi/dsl/callback.rb +0 -21
  108. data/lib/jsapi/dsl/error.rb +0 -36
  109. data/lib/jsapi/invalid_argument_error.rb +0 -12
  110. data/lib/jsapi/invalid_value_error.rb +0 -12
  111. data/lib/jsapi/invalid_value_helper.rb +0 -17
  112. data/lib/jsapi/meta/model/type_caster.rb +0 -50
  113. data/lib/jsapi/meta/schema/delegator.rb +0 -26
@@ -1,8 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'methods/callbacks'
4
+
3
5
  module Jsapi
4
6
  module Controller
7
+ # Raised when no operation with the specified name could be found.
8
+ class OperationNotFound < StandardError
9
+ def initialize(operation_name)
10
+ super("operation not found: #{operation_name}")
11
+ end
12
+ end
13
+
14
+ # Raised when no suitable response could be found.
15
+ class ResponseNotFound < StandardError
16
+ def initialize(operation, status)
17
+ super(
18
+ if operation.responses.none?
19
+ "#{operation.name.inspect} has no responses"
20
+ else
21
+ "#{operation.name.inspect} has no response for status #{status}"
22
+ end
23
+ )
24
+ end
25
+ end
26
+
27
+ # Raised when the current request could not be authenticated.
28
+ class Unauthorized < StandardError
29
+ def initialize # :nodoc:
30
+ super('request could not be authenticated')
31
+ end
32
+ end
33
+
34
+ # Raised when the request parameters are invalid.
35
+ class ParametersInvalid < StandardError
36
+
37
+ # The parameters.
38
+ attr_reader :params
39
+
40
+ def initialize(params)
41
+ @params = params
42
+ super('')
43
+ end
44
+
45
+ # Returns the errors encountered.
46
+ def errors
47
+ @params.errors.errors
48
+ end
49
+
50
+ # Overrides <code>Exception#message</code> to lazily generate the error message.
51
+ def message
52
+ "#{@params.errors.full_messages.map { |m| m.delete_suffix('.') }.join('. ')}."
53
+ end
54
+ end
55
+
5
56
  module Methods
57
+ def self.included(base) # :nodoc:
58
+ base.include(Callbacks)
59
+ end
60
+
6
61
  # Returns the Meta::Definitions instance associated with the controller class. In
7
62
  # particular, this method can be used to create an OpenAPI document, for example:
8
63
  #
@@ -12,18 +67,36 @@ module Jsapi
12
67
  self.class.api_definitions
13
68
  end
14
69
 
15
- # Performs an API operation by calling the given block. The request parameters are
16
- # passed as an instance of the operation's model class to the block. The object
17
- # returned by the block is implicitly rendered according to the appropriate +response+
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.
70
+ ##
71
+ # :method: api_operation
72
+ # :args: operation_name = nil, omit: nil, status: nil, strong: false, &block
73
+ #
74
+ # Performs an API operation by calling the given block.
75
+ #
76
+ # The request parameters are passed as an instance of the operation's model class to the
77
+ # block. Parameter names are converted to snake case.
78
+ #
79
+ # The object returned by the block is implicitly rendered or streamed according to the
80
+ # most appropriate +response+ specification if the media type of that response is one of:
81
+ #
82
+ # - <code>"application/json"</code>, <code>"text/json"</code>, <code>"\*/\*+json"</code> -
83
+ # The \JSON representation of the object is rendered.
84
+ # - <code>"application/json-seq"</code> - The object is streamed in \JSON sequence
85
+ # text format.
86
+ # - <code>"text/plain"</code> - The +to_s+ representation of the object is rendered.
87
+ #
88
+ # Example:
21
89
  #
22
90
  # api_operation('foo') do |api_params|
23
91
  # # ...
24
92
  # end
25
93
  #
26
- # +operation_name+ can be +nil+ if the controller handles one operation only.
94
+ # +operation_name+ can be omitted if the controller handles one operation only.
95
+ #
96
+ # If no operation could be found for +operation_name+, an OperationNotFound exception
97
+ # is raised.
98
+ #
99
+ # +:status+ specifies the HTTP status code of the response to be produced.
27
100
  #
28
101
  # If +:strong+ is +true+, parameters that can be mapped are accepted only. That means
29
102
  # that the model passed to the block is invalid if there are any request parameters
@@ -35,42 +108,106 @@ module Jsapi
35
108
  # - +:empty+ - All of the properties whose value is empty are omitted.
36
109
  # - +:nil+ - All of the properties whose value is +nil+ are omitted.
37
110
  #
38
- # Raises an +ArgumentError+ when +:omit+ is other than +:empty+, +:nil+ or +nil+.
39
- def api_operation(operation_name = nil,
40
- omit: nil,
41
- status: nil,
42
- strong: false,
43
- &block)
44
- _api_operation(
45
- operation_name,
46
- bang: false,
47
- omit: omit,
48
- status: status,
49
- strong: strong,
50
- &block
51
- )
52
- end
111
+ # Raises an Unauthorized exception if the Authentication module is included and the
112
+ # current request could not be authenticated.
113
+ #
114
+ # See Callbacks::ClassMethods for possible callbacks.
53
115
 
54
- # Like +api_operation+, except that a ParametersInvalid exception is raised on
55
- # invalid request parameters.
116
+ ##
117
+ # :method: api_operation!
118
+ # :args: operation_name = nil, omit: nil, status: nil, strong: false, &block
119
+ #
120
+ # Like +api_operation+, except that a ParametersInvalid exception is raised
121
+ # when request parameters are invalid.
56
122
  #
57
123
  # api_operation!('foo') do |api_params|
58
124
  # # ...
59
125
  # end
60
- #
61
- def api_operation!(operation_name = nil,
62
- omit: nil,
63
- status: nil,
64
- strong: false,
65
- &block)
66
- _api_operation(
67
- operation_name,
68
- bang: true,
69
- omit: omit,
70
- status: status,
71
- strong: strong,
72
- &block
73
- )
126
+
127
+ [true, false].each do |bang|
128
+ define_method(bang ? :api_operation! : :api_operation) \
129
+ do |operation_name = nil, omit: nil, status: nil, strong: false, &block|
130
+ operation = _api_operation(operation_name)
131
+ response_model = nil
132
+
133
+ result = begin
134
+ # Authenticate request first if Authentication is included
135
+ if respond_to?(:_api_authenticated?, true)
136
+ raise Unauthorized unless _api_authenticated?(operation)
137
+
138
+ _api_callback(:after_authentication, operation_name)
139
+ end
140
+
141
+ status = Status::Code.from(status)
142
+ response_model = _api_response_model(operation, status)
143
+
144
+ api_params = _api_params(operation, strong: strong)
145
+
146
+ if bang
147
+ raise ParametersInvalid.new(api_params) if api_params.invalid?
148
+
149
+ _api_callback(:after_validation, operation_name, api_params)
150
+ end
151
+
152
+ block&.call(api_params)
153
+ rescue StandardError => e
154
+ definitions = api_definitions
155
+
156
+ # Lookup a rescue handler
157
+ rescue_handler = definitions.rescue_handler_for(e)
158
+ raise e if rescue_handler.nil?
159
+
160
+ # Replace the status code and response model
161
+ status = rescue_handler.status_code
162
+ response_model = operation.find_response(status)
163
+ raise e if response_model.nil?
164
+
165
+ # Call on_rescue callbacks
166
+ definitions.on_rescue_callbacks.each do |callback|
167
+ callback.respond_to?(:call) ? callback.call(e) : send(callback, e)
168
+ end
169
+
170
+ Error.new(e, status: status)
171
+ end
172
+ # Return if response body has already been rendered
173
+ return if response_body
174
+
175
+ # Produce response
176
+ media_type, content_model = _api_media_type_and_content_model(response_model)
177
+ status = status&.to_i
178
+ return head(status) unless content_model
179
+
180
+ result = _api_before_rendering(operation_name, result, api_params)
181
+
182
+ api_response = Response.new(
183
+ result,
184
+ content_model,
185
+ omit: omit,
186
+ locale: response_model.locale
187
+ )
188
+ if media_type.json?
189
+ render(
190
+ json: api_response,
191
+ status: status,
192
+ content_type: media_type.to_s
193
+ )
194
+ elsif media_type == Media::Type::TEXT_PLAIN
195
+ render(
196
+ plain: result,
197
+ status: status,
198
+ content_type: media_type.to_s
199
+ )
200
+ elsif media_type == Media::Type::APPLICATION_JSON_SEQ
201
+ self.content_type = media_type.to_s
202
+ response.status = status
203
+
204
+ response.stream.tap do |stream|
205
+ api_response.write_json_seq_to(stream)
206
+ ensure
207
+ stream.close
208
+ end
209
+ end
210
+ end
74
211
  end
75
212
 
76
213
  # Returns the request parameters as an instance of the operation's model class.
@@ -78,7 +215,10 @@ module Jsapi
78
215
  #
79
216
  # params = api_params('foo')
80
217
  #
81
- # +operation_name+ can be +nil+ if the controller handles one operation only.
218
+ # +operation_name+ can be omitted if the controller handles one operation only.
219
+ #
220
+ # If no operation could be found for +operation_name+, an OperationNotFound exception
221
+ # is raised.
82
222
  #
83
223
  # If +strong+ is +true+, parameters that can be mapped are accepted only. That means
84
224
  # that the model returned is invalid if there are any request parameters that can't be
@@ -86,12 +226,7 @@ module Jsapi
86
226
  #
87
227
  # Note that each call of +api_params+ returns a newly created instance.
88
228
  def api_params(operation_name = nil, strong: false)
89
- definitions = api_definitions
90
- _api_params(
91
- _find_api_operation(operation_name, definitions),
92
- definitions,
93
- strong: strong
94
- )
229
+ _api_params(_api_operation(operation_name), strong: strong)
95
230
  end
96
231
 
97
232
  # Returns a Response to serialize the JSON representation of +result+ according to the
@@ -99,104 +234,62 @@ module Jsapi
99
234
  #
100
235
  # render(json: api_response(bar, 'foo', status: 200))
101
236
  #
102
- # +operation_name+ can be +nil+ if the controller handles one operation only.
237
+ # +operation_name+ can be omitted if the controller handles one operation only.
238
+ #
239
+ # If no operation could be found for +operation_name+, an OperationNotFound exception
240
+ # is raised.
241
+ #
242
+ # +:status+ specifies the HTTP status code of the response to be produced.
103
243
  #
104
244
  # The +:omit+ option specifies on which conditions properties are omitted.
105
245
  # Possible values are:
106
246
  #
107
247
  # - +:empty+ - All of the properties whose value is empty are omitted.
108
248
  # - +:nil+ - All of the properties whose value is +nil+ are omitted.
109
- #
110
- # Raises an +ArgumentError+ when +:omit+ is other than +:empty+, +:nil+ or +nil+.
111
249
  def api_response(result, operation_name = nil, omit: nil, status: nil)
112
- definitions = api_definitions
113
- operation = _find_api_operation(operation_name, definitions)
114
- response_model = _api_response(operation, status, definitions)
250
+ status = Status::Code.from(status)
251
+ operation = _api_operation(operation_name)
252
+ response_model = _api_response_model(operation, status)
115
253
 
116
- Response.new(result, response_model, api_definitions, omit: omit)
254
+ Response.new(
255
+ result,
256
+ _api_media_type_and_content_model(response_model).second,
257
+ omit: omit,
258
+ locale: response_model.locale
259
+ )
117
260
  end
118
261
 
119
262
  private
120
263
 
121
- def _api_operation(operation_name, bang:, omit:, status:, strong:, &block)
122
- definitions = api_definitions
123
- operation = _find_api_operation(operation_name, definitions)
124
-
125
- # Perform operation
126
- response_model = _api_response(operation, status, definitions)
127
- head(status) && return unless block
128
-
129
- params = _api_params(operation, definitions, strong: strong)
130
-
131
- result = begin
132
- raise ParametersInvalid.new(params) if bang && params.invalid?
133
-
134
- block.call(params)
135
- rescue StandardError => e
136
- # Lookup a rescue handler
137
- rescue_handler = definitions.rescue_handler_for(e)
138
- raise e if rescue_handler.nil?
139
-
140
- # Change the HTTP status code and response model
141
- status = rescue_handler.status
142
- response_model = operation.response(status)&.resolve(definitions)
143
- raise e if response_model.nil?
144
-
145
- # Call on_rescue callbacks
146
- definitions.on_rescue_callbacks.each do |callback|
147
- if callback.respond_to?(:call)
148
- callback.call(e)
149
- else
150
- send(callback, e)
151
- end
152
- end
153
-
154
- Error.new(e, status: status)
155
- end
156
-
157
- # Write response
158
- return unless response_model.json_type? || response_model.json_seq_type?
159
-
160
- response = Response.new(result, response_model, definitions, omit: omit)
161
- self.content_type = response_model.content_type
264
+ def _api_media_type_and_content_model(response_model)
265
+ response_model.media_type_and_content_for(
266
+ *(request.headers['Accept']&.split(',').presence || [Media::Range::ALL])
267
+ )
268
+ end
162
269
 
163
- if response_model.json_seq_type?
164
- self.response.status = status
270
+ def _api_operation(operation_name)
271
+ operation = api_definitions.find_operation(operation_name)
272
+ return operation if operation
165
273
 
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
274
+ raise OperationNotFound.new(operation_name)
174
275
  end
175
276
 
176
- def _api_params(operation, definitions, strong:)
277
+ def _api_params(operation, strong:)
177
278
  (operation.model || Model::Base).new(
178
279
  Parameters.new(
179
280
  params.except(:action, :controller, :format).permit!,
180
281
  request,
181
282
  operation,
182
- definitions,
183
283
  strong: strong
184
284
  )
185
285
  )
186
286
  end
187
287
 
188
- def _api_response(operation, status, definitions)
189
- response = operation.response(status)
190
- return response.resolve(definitions) if response
191
-
192
- raise "status code not defined: #{status}"
193
- end
194
-
195
- def _find_api_operation(operation_name, definitions)
196
- operation = definitions.find_operation(operation_name)
197
- return operation if operation
288
+ def _api_response_model(operation, status)
289
+ response_model = operation.find_response(status)
290
+ return response_model if response_model
198
291
 
199
- raise "operation not defined: #{operation_name}"
292
+ raise ResponseNotFound.new(operation, status)
200
293
  end
201
294
  end
202
295
  end
@@ -8,25 +8,23 @@ module Jsapi
8
8
 
9
9
  attr_reader :raw_additional_attributes, :raw_attributes
10
10
 
11
- # Creates a new instance that wraps +params+ according to +operation+. References are
12
- # resolved to API components in +definitions+.
11
+ # Creates a new instance that wraps +params+ according to +operation+.
13
12
  #
14
13
  # If +strong+ is true+ parameters that can be mapped are accepted only. That means that
15
14
  # the instance created is invalid if +params+ contains any parameters that can't be
16
15
  # mapped to a parameter or a request body property of +operation+.
17
- def initialize(params, request, operation, definitions, strong: false)
16
+ def initialize(params, request, operation, strong: false)
18
17
  params = params.to_h
19
18
  unassigned_params = params.dup
20
19
 
21
20
  @params_to_be_validated = strong == true ? params.dup : {}
22
- @raw_attributes = {}
23
21
 
24
22
  # Parameters
25
- operation.resolved_parameters(definitions).each do |name, parameter_model|
26
- @raw_attributes[name] = JSON.wrap(
23
+ @raw_attributes = operation.parameters.transform_values do |parameter_model|
24
+ JSON.wrap(
27
25
  case parameter_model.in
28
26
  when 'header'
29
- request.headers[name]
27
+ request.headers[parameter_model.name]
30
28
  when 'querystring'
31
29
  query_params = request.query_parameters
32
30
  keys = query_params.keys
@@ -36,27 +34,23 @@ module Jsapi
36
34
 
37
35
  parameter_model.object? ? params.slice(*keys) : query_params.to_query
38
36
  else
39
- unassigned_params.delete(name)
37
+ unassigned_params.delete(parameter_model.name)
40
38
  end,
41
- parameter_model.schema.resolve(definitions),
42
- definitions,
39
+ parameter_model.schema,
43
40
  context: :request
44
41
  )
45
42
  end
46
43
 
47
44
  # Request body
48
- request_body_schema = operation.request_body&.resolve(definitions)
49
- &.schema&.resolve(definitions)
45
+ request_body_schema = operation.request_body&.content_for(request.media_type)&.schema
50
46
  if request_body_schema&.object?
51
47
  request_body = JSON.wrap(
52
48
  unassigned_params,
53
49
  request_body_schema,
54
- definitions,
55
50
  context: :request
56
51
  )
57
52
  @raw_attributes.merge!(request_body.raw_attributes)
58
53
  @raw_additional_attributes = request_body.raw_additional_attributes
59
- @params_to_be_validated.except!(*@raw_additional_attributes.keys)
60
54
  else
61
55
  @raw_additional_attributes = {}
62
56
  end
@@ -66,18 +60,24 @@ module Jsapi
66
60
  # otherwise. Detected errors are added to +errors+.
67
61
  def validate(errors)
68
62
  validate_attributes(errors) &&
69
- validate_parameters(@params_to_be_validated, attributes, errors)
63
+ validate_parameters(@params_to_be_validated, self, errors)
70
64
  end
71
65
 
72
66
  private
73
67
 
74
- def validate_parameters(params, attributes, errors, path = [])
68
+ def validate_parameters(params, model, errors, path = [])
75
69
  params.each.map do |key, value|
76
- if attributes.key?(key)
77
- # Validate nested parameters
78
- !value.respond_to?(:keys) || validate_parameters(
70
+ if model.raw_attributes.key?(key)
71
+ validate_parameter(
79
72
  value,
80
- attributes[key].try(:attributes) || {},
73
+ model.raw_attributes[key],
74
+ errors,
75
+ path + [key]
76
+ )
77
+ elsif model.raw_additional_attributes.key?(key)
78
+ validate_parameter(
79
+ value,
80
+ model.raw_additional_attributes[key],
81
81
  errors,
82
82
  path + [key]
83
83
  )
@@ -94,6 +94,10 @@ module Jsapi
94
94
  end
95
95
  end.all?
96
96
  end
97
+
98
+ def validate_parameter(value, model, errors, path)
99
+ !model.schema.object? || validate_parameters(value, model, errors, path)
100
+ end
97
101
  end
98
102
  end
99
103
  end
@@ -15,8 +15,34 @@ module Jsapi
15
15
  end
16
16
  end
17
17
 
18
- # Creates a new instance to jsonify +object+ according to +response+. References
19
- # are resolved to API components in +definitions+.
18
+ class HashReader # :nodoc:
19
+ delegate_missing_to :@hash
20
+
21
+ def initialize(hash)
22
+ @hash = hash
23
+ end
24
+
25
+ def [](key)
26
+ return unless @hash.key?(key)
27
+
28
+ (@read_keys ||= []) << key
29
+ @hash[key]
30
+ end
31
+
32
+ def additional_properties
33
+ if @hash.key?('additional_properties')
34
+ @hash['additional_properties']
35
+ elsif @hash.key?(:additional_properties)
36
+ @hash[:additional_properties]
37
+ elsif @read_keys
38
+ @hash.except(*@read_keys)
39
+ else
40
+ @hash
41
+ end
42
+ end
43
+ end
44
+
45
+ # Creates a new instance to jsonify +object+ according to +content_model+.
20
46
  #
21
47
  # The +:omit+ option specifies on which conditions properties are omitted.
22
48
  # Possible values are:
@@ -25,10 +51,9 @@ module Jsapi
25
51
  # - +:nil+ - All of the properties whose value is +nil+ are omitted.
26
52
  #
27
53
  # Raises an +ArgumentError+ when +:omit+ is other than +:empty+, +:nil+ or +nil+.
28
- def initialize(object, response, definitions, omit: nil)
54
+ def initialize(object, content_model, omit: nil, locale: nil)
29
55
  @object = object
30
- @response = response
31
- @definitions = definitions
56
+ @content_model = content_model
32
57
 
33
58
  @omittable_check =
34
59
  case omit
@@ -39,8 +64,14 @@ module Jsapi
39
64
  when :empty
40
65
  ->(value, schema) { schema.omittable? && value.try(:empty?) }
41
66
  else
42
- raise InvalidArgumentError.new('omit', omit, valid_values: %i[empty nil])
67
+ raise ArgumentError, Messages.invalid_value(
68
+ name: 'omit',
69
+ value: omit,
70
+ valid_values: %i[empty nil]
71
+ )
43
72
  end
73
+
74
+ @locale = locale
44
75
  end
45
76
 
46
77
  def inspect # :nodoc:
@@ -49,17 +80,16 @@ module Jsapi
49
80
 
50
81
  # Returns the \JSON representation of the response as a string.
51
82
  def to_json(*)
52
- schema = @response.schema.resolve(@definitions)
53
- with_locale { jsonify(@object, schema) }.to_json
83
+ with_locale { jsonify(@object, @content_model.schema) }.to_json
54
84
  end
55
85
 
56
86
  # Writes the response in \JSON sequence text format to +stream+.
57
87
  def write_json_seq_to(stream)
58
- schema = @response.schema.resolve(@definitions)
88
+ schema = @content_model.schema
59
89
  with_locale do
60
90
  items, item_schema =
61
91
  if schema.array? && @object.respond_to?(:each)
62
- [@object, schema.items.resolve(@definitions)]
92
+ [@object, schema.items]
63
93
  else
64
94
  [[@object], schema]
65
95
  end
@@ -76,7 +106,7 @@ module Jsapi
76
106
  private
77
107
 
78
108
  def jsonify(object, schema)
79
- object = schema.default_value(@definitions, context: :response) if object.nil?
109
+ object = schema.default_value(context: :response) if object.nil?
80
110
 
81
111
  if object.nil?
82
112
  raise JsonifyError, "can't be nil" unless schema.nullable?
@@ -112,7 +142,7 @@ module Jsapi
112
142
  end
113
143
 
114
144
  def jsonify_array(array, schema)
115
- item_schema = schema.items.resolve(@definitions)
145
+ item_schema = schema.items
116
146
  index = 0
117
147
 
118
148
  Array(array).map do |item|
@@ -125,39 +155,41 @@ module Jsapi
125
155
  end
126
156
 
127
157
  def jsonify_object(object, schema)
128
- schema = schema.resolve_schema(object, @definitions, context: :response)
129
- properties = {}
130
-
131
- # Add properties
132
- schema.resolve_properties(@definitions, context: :response).each_value do |property|
133
- property_schema = property.schema.resolve(@definitions)
134
- property_value = property.reader.call(object)
135
- property_value = property_schema.default if property_value.nil?
136
- next if @omittable_check&.call(property_value, property_schema)
137
-
138
- properties[property.name] = jsonify(property_value, property_schema)
139
- rescue JsonifyError => e
140
- raise e.prepend(".#{property.name}")
141
- end
142
- # Add additional properties
143
- if (additional_properties = schema.additional_properties)
144
- additional_properties_schema = additional_properties.schema.resolve(@definitions)
145
-
146
- additional_properties.source.call(object)&.each do |key, value|
147
- next if properties.key?(key = key.to_s)
148
-
149
- properties[key] = jsonify(value, additional_properties_schema)
158
+ schema = schema.resolve_schema(object, context: :response)
159
+ additional_properties = schema.additional_properties
160
+ object = HashReader.new(object) if additional_properties && object.is_a?(Hash)
161
+
162
+ {}.tap do |properties|
163
+ # Add properties
164
+ schema.resolve_properties(context: :response).each_value do |property|
165
+ property_schema = property.schema
166
+ property_value = property.reader.call(object)
167
+ property_value = property_schema.default if property_value.nil?
168
+ next if @omittable_check&.call(property_value, property_schema)
169
+
170
+ properties[property.name] = jsonify(property_value, property_schema)
150
171
  rescue JsonifyError => e
151
- raise e.prepend(".#{key}")
172
+ raise e.prepend(".#{property.name}")
152
173
  end
153
- end
174
+ # Add additional properties
175
+ if additional_properties
176
+ additional_properties_schema = additional_properties.schema
177
+
178
+ additional_properties.source.call(object)&.each do |key, value|
179
+ key = key.to_s
180
+ next if properties.key?(key)
154
181
 
155
- properties.presence
182
+ properties[key] = jsonify(value, additional_properties_schema)
183
+ rescue JsonifyError => e
184
+ raise e.prepend(".#{key}")
185
+ end
186
+ end
187
+ end.presence
156
188
  end
157
189
 
158
190
  def with_locale(&block)
159
- if @response.locale
160
- I18n.with_locale(@response.locale, &block)
191
+ if @locale
192
+ I18n.with_locale(@locale, &block)
161
193
  else
162
194
  yield
163
195
  end