jsapi 1.4.1 → 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 (111) 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 +188 -71
  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 +39 -6
  35. data/lib/jsapi/media/type.rb +8 -4
  36. data/lib/jsapi/media.rb +5 -0
  37. data/lib/jsapi/messages.rb +19 -0
  38. data/lib/jsapi/meta/callback/base.rb +63 -8
  39. data/lib/jsapi/meta/content.rb +59 -0
  40. data/lib/jsapi/meta/definitions.rb +291 -141
  41. data/lib/jsapi/meta/example/base.rb +41 -8
  42. data/lib/jsapi/meta/existence.rb +1 -1
  43. data/lib/jsapi/meta/header/base.rb +4 -2
  44. data/lib/jsapi/meta/info.rb +3 -1
  45. data/lib/jsapi/meta/license.rb +11 -5
  46. data/lib/jsapi/meta/model/attributes/class_methods.rb +150 -0
  47. data/lib/jsapi/meta/model/attributes/frozen_error.rb +16 -0
  48. data/lib/jsapi/meta/model/attributes/type_caster.rb +56 -0
  49. data/lib/jsapi/meta/model/attributes.rb +24 -118
  50. data/lib/jsapi/meta/model/base.rb +2 -5
  51. data/lib/jsapi/meta/model/reference.rb +46 -10
  52. data/lib/jsapi/meta/model/wrappable.rb +23 -0
  53. data/lib/jsapi/meta/model/wrapper.rb +26 -0
  54. data/lib/jsapi/meta/model.rb +2 -1
  55. data/lib/jsapi/meta/oauth_flow.rb +1 -1
  56. data/lib/jsapi/meta/openapi/extensions.rb +5 -6
  57. data/lib/jsapi/meta/openapi/version.rb +2 -2
  58. data/lib/jsapi/meta/operation.rb +177 -71
  59. data/lib/jsapi/meta/parameter/base.rb +9 -5
  60. data/lib/jsapi/meta/parameter/wrapper.rb +13 -0
  61. data/lib/jsapi/meta/parameter.rb +3 -0
  62. data/lib/jsapi/meta/path.rb +59 -13
  63. data/lib/jsapi/meta/pathname.rb +3 -0
  64. data/lib/jsapi/meta/property.rb +10 -0
  65. data/lib/jsapi/meta/request_body/base.rb +69 -32
  66. data/lib/jsapi/meta/request_body/wrapper.rb +13 -0
  67. data/lib/jsapi/meta/request_body.rb +3 -0
  68. data/lib/jsapi/meta/rescue_handler.rb +18 -17
  69. data/lib/jsapi/meta/response/base.rb +86 -49
  70. data/lib/jsapi/meta/response/reference.rb +11 -1
  71. data/lib/jsapi/meta/response/wrapper.rb +26 -0
  72. data/lib/jsapi/meta/response.rb +3 -0
  73. data/lib/jsapi/meta/schema/additional_properties.rb +8 -0
  74. data/lib/jsapi/meta/schema/array.rb +20 -8
  75. data/lib/jsapi/meta/schema/base.rb +10 -9
  76. data/lib/jsapi/meta/schema/numeric.rb +26 -20
  77. data/lib/jsapi/meta/schema/object.rb +60 -44
  78. data/lib/jsapi/meta/schema/reference.rb +1 -8
  79. data/lib/jsapi/meta/schema/string.rb +12 -6
  80. data/lib/jsapi/meta/schema/wrapper.rb +31 -0
  81. data/lib/jsapi/meta/schema.rb +22 -9
  82. data/lib/jsapi/meta/security_requirement.rb +2 -2
  83. data/lib/jsapi/meta/security_scheme/api_key.rb +5 -2
  84. data/lib/jsapi/meta/security_scheme/base.rb +7 -5
  85. data/lib/jsapi/meta/security_scheme/http/basic.rb +5 -7
  86. data/lib/jsapi/meta/security_scheme/http/bearer.rb +5 -5
  87. data/lib/jsapi/meta/security_scheme/http/other.rb +1 -3
  88. data/lib/jsapi/meta/security_scheme/mutual_tls.rb +1 -3
  89. data/lib/jsapi/meta/security_scheme/oauth2.rb +18 -13
  90. data/lib/jsapi/meta/security_scheme/open_id_connect.rb +4 -4
  91. data/lib/jsapi/meta/security_scheme.rb +4 -4
  92. data/lib/jsapi/meta/server.rb +4 -2
  93. data/lib/jsapi/meta/tag.rb +9 -3
  94. data/lib/jsapi/meta.rb +2 -1
  95. data/lib/jsapi/model/base.rb +1 -1
  96. data/lib/jsapi/status/base.rb +35 -0
  97. data/lib/jsapi/status/code.rb +113 -0
  98. data/lib/jsapi/status/default.rb +16 -0
  99. data/lib/jsapi/status/range.rb +35 -0
  100. data/lib/jsapi/status.rb +37 -0
  101. data/lib/jsapi/version.rb +1 -1
  102. data/lib/jsapi.rb +2 -3
  103. metadata +32 -10
  104. data/lib/jsapi/controller/parameters_invalid.rb +0 -27
  105. data/lib/jsapi/dsl/callback.rb +0 -21
  106. data/lib/jsapi/dsl/error.rb +0 -36
  107. data/lib/jsapi/invalid_argument_error.rb +0 -12
  108. data/lib/jsapi/invalid_value_error.rb +0 -12
  109. data/lib/jsapi/invalid_value_helper.rb +0 -17
  110. data/lib/jsapi/meta/model/type_caster.rb +0 -50
  111. 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
  #
@@ -16,18 +71,32 @@ module Jsapi
16
71
  # :method: api_operation
17
72
  # :args: operation_name = nil, omit: nil, status: nil, strong: false, &block
18
73
  #
19
- # Performs an API operation by calling the given block. The request parameters are
20
- # passed as an instance of the operation's model class to the block. The object
21
- # returned by the block is implicitly rendered according to the appropriate +response+
22
- # specification when the content type is a \JSON MIME type. When content type is
23
- # <code>application/json-seq</code>, the object returned by the block is streamed in
24
- # \JSON sequence text format.
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:
25
89
  #
26
90
  # api_operation('foo') do |api_params|
27
91
  # # ...
28
92
  # end
29
93
  #
30
- # +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.
31
100
  #
32
101
  # If +:strong+ is +true+, parameters that can be mapped are accepted only. That means
33
102
  # that the model passed to the block is invalid if there are any request parameters
@@ -39,7 +108,10 @@ module Jsapi
39
108
  # - +:empty+ - All of the properties whose value is empty are omitted.
40
109
  # - +:nil+ - All of the properties whose value is +nil+ are omitted.
41
110
  #
42
- # Raises an +ArgumentError+ when +:omit+ is other than +:empty+, +:nil+ or +nil+.
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.
43
115
 
44
116
  ##
45
117
  # :method: api_operation!
@@ -55,41 +127,77 @@ module Jsapi
55
127
  [true, false].each do |bang|
56
128
  define_method(bang ? :api_operation! : :api_operation) \
57
129
  do |operation_name = nil, omit: nil, status: nil, strong: false, &block|
58
- definitions = api_definitions
59
- operation_model = _find_api_operation_model(operation_name, definitions)
60
- response_model = _find_api_response_model(operation_model, status, definitions)
61
- head(status) && return unless 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)
62
181
 
63
- # Perform operation
64
- api_params = _api_params(operation_model, definitions, strong: strong)
65
182
  api_response = Response.new(
66
- begin
67
- raise ParametersInvalid.new(api_params) if bang && api_params.invalid?
68
-
69
- block.call(api_params)
70
- rescue StandardError => e
71
- # Lookup a rescue handler
72
- rescue_handler = definitions.rescue_handler_for(e)
73
- raise e if rescue_handler.nil?
74
-
75
- # Change the HTTP status code and response model
76
- status = rescue_handler.status
77
- response_model = operation_model.response(status)&.resolve(definitions)
78
- raise e if response_model.nil?
79
-
80
- # Call on_rescue callbacks
81
- definitions.on_rescue_callbacks.each do |callback|
82
- callback.respond_to?(:call) ? callback.call(e) : send(callback, e)
83
- end
84
-
85
- Error.new(e, status: status)
86
- end,
87
- response_model, definitions, omit: omit
183
+ result,
184
+ content_model,
185
+ omit: omit,
186
+ locale: response_model.locale
88
187
  )
89
- # Write response
90
- media_type = response_model.content_type
91
-
92
- if media_type == Media::Type::APPLICATION_JSON_SEQ
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
93
201
  self.content_type = media_type.to_s
94
202
  response.status = status
95
203
 
@@ -98,8 +206,6 @@ module Jsapi
98
206
  ensure
99
207
  stream.close
100
208
  end
101
- elsif media_type.json?
102
- render(json: api_response, status: status, content_type: media_type.to_s)
103
209
  end
104
210
  end
105
211
  end
@@ -109,7 +215,10 @@ module Jsapi
109
215
  #
110
216
  # params = api_params('foo')
111
217
  #
112
- # +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.
113
222
  #
114
223
  # If +strong+ is +true+, parameters that can be mapped are accepted only. That means
115
224
  # that the model returned is invalid if there are any request parameters that can't be
@@ -117,12 +226,7 @@ module Jsapi
117
226
  #
118
227
  # Note that each call of +api_params+ returns a newly created instance.
119
228
  def api_params(operation_name = nil, strong: false)
120
- definitions = api_definitions
121
- _api_params(
122
- _find_api_operation_model(operation_name, definitions),
123
- definitions,
124
- strong: strong
125
- )
229
+ _api_params(_api_operation(operation_name), strong: strong)
126
230
  end
127
231
 
128
232
  # Returns a Response to serialize the JSON representation of +result+ according to the
@@ -130,49 +234,62 @@ module Jsapi
130
234
  #
131
235
  # render(json: api_response(bar, 'foo', status: 200))
132
236
  #
133
- # +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.
134
243
  #
135
244
  # The +:omit+ option specifies on which conditions properties are omitted.
136
245
  # Possible values are:
137
246
  #
138
247
  # - +:empty+ - All of the properties whose value is empty are omitted.
139
248
  # - +:nil+ - All of the properties whose value is +nil+ are omitted.
140
- #
141
- # Raises an +ArgumentError+ when +:omit+ is other than +:empty+, +:nil+ or +nil+.
142
249
  def api_response(result, operation_name = nil, omit: nil, status: nil)
143
- definitions = api_definitions
144
- operation_model = _find_api_operation_model(operation_name, definitions)
145
- response_model = _find_api_response_model(operation_model, status, definitions)
250
+ status = Status::Code.from(status)
251
+ operation = _api_operation(operation_name)
252
+ response_model = _api_response_model(operation, status)
146
253
 
147
- Response.new(result, response_model, 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
+ )
148
260
  end
149
261
 
150
262
  private
151
263
 
152
- def _api_params(operation_model, definitions, strong:)
153
- (operation_model.model || Model::Base).new(
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
269
+
270
+ def _api_operation(operation_name)
271
+ operation = api_definitions.find_operation(operation_name)
272
+ return operation if operation
273
+
274
+ raise OperationNotFound.new(operation_name)
275
+ end
276
+
277
+ def _api_params(operation, strong:)
278
+ (operation.model || Model::Base).new(
154
279
  Parameters.new(
155
280
  params.except(:action, :controller, :format).permit!,
156
281
  request,
157
- operation_model,
158
- definitions,
282
+ operation,
159
283
  strong: strong
160
284
  )
161
285
  )
162
286
  end
163
287
 
164
- def _find_api_response_model(operation, status, definitions)
165
- response = operation.response(status)
166
- return response.resolve(definitions) if response
167
-
168
- raise "status code not defined: #{status}"
169
- end
170
-
171
- def _find_api_operation_model(operation_name, definitions)
172
- operation = definitions.find_operation(operation_name)
173
- 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
174
291
 
175
- raise "operation not defined: #{operation_name}"
292
+ raise ResponseNotFound.new(operation, status)
176
293
  end
177
294
  end
178
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
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'controller/parameters_invalid'
4
3
  require_relative 'controller/error'
4
+ require_relative 'controller/authentication'
5
5
  require_relative 'controller/parameters'
6
6
  require_relative 'controller/response'
7
7
  require_relative 'controller/methods'
8
+ require_relative 'controller/actions'
8
9
  require_relative 'controller/base'
9
10
 
10
11
  module Jsapi