apimatic_core 0.3.8 → 0.3.17
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.
- checksums.yaml +4 -4
- data/README.md +38 -23
- data/lib/apimatic-core/api_call.rb +55 -0
- data/lib/apimatic-core/configurations/global_configuration.rb +16 -0
- data/lib/apimatic-core/http/configurations/http_client_configuration.rb +18 -0
- data/lib/apimatic-core/http/http_call_context.rb +50 -0
- data/lib/apimatic-core/http/response/http_response.rb +29 -0
- data/lib/apimatic-core/pagination/paginated_data.rb +137 -0
- data/lib/apimatic-core/pagination/pagination_strategy.rb +46 -0
- data/lib/apimatic-core/pagination/strategies/cursor_pagination.rb +72 -0
- data/lib/apimatic-core/pagination/strategies/link_pagination.rb +67 -0
- data/lib/apimatic-core/pagination/strategies/offset_pagination.rb +63 -0
- data/lib/apimatic-core/pagination/strategies/page_pagination.rb +60 -0
- data/lib/apimatic-core/request_builder.rb +139 -14
- data/lib/apimatic-core/response_handler.rb +14 -3
- data/lib/apimatic-core/types/parameter.rb +2 -2
- data/lib/apimatic-core/types/sdk/base_model.rb +1 -1
- data/lib/apimatic-core/utilities/api_helper.rb +110 -4
- data/lib/apimatic-core/utilities/deep_clone_utils.rb +114 -0
- data/lib/apimatic-core/utilities/file_helper.rb +15 -9
- data/lib/apimatic-core/utilities/json_pointer.rb +257 -0
- data/lib/apimatic-core/utilities/json_pointer_helper.rb +48 -124
- data/lib/apimatic_core.rb +10 -0
- metadata +28 -19
@@ -0,0 +1,63 @@
|
|
1
|
+
module CoreLibrary
|
2
|
+
# Implements offset-based pagination strategy for API responses.
|
3
|
+
#
|
4
|
+
# This class manages pagination by updating an offset parameter in the request builder,
|
5
|
+
# allowing sequential retrieval of paginated data. It extracts and updates the offset
|
6
|
+
# based on a configurable JSON pointer and applies a metadata wrapper to each page response.
|
7
|
+
class OffsetPagination < PaginationStrategy
|
8
|
+
# Initializes an OffsetPagination instance with the given input pointer and metadata wrapper.
|
9
|
+
#
|
10
|
+
# @param input [String] JSON pointer indicating the pagination parameter to update.
|
11
|
+
# @param metadata_wrapper [Proc] Callable for handling pagination metadata.
|
12
|
+
# @raise [ArgumentError] If input is nil.
|
13
|
+
def initialize(input, metadata_wrapper)
|
14
|
+
super(metadata_wrapper)
|
15
|
+
|
16
|
+
raise ArgumentError, 'Input pointer for offset based pagination cannot be nil' if input.nil?
|
17
|
+
|
18
|
+
@input = input
|
19
|
+
@offset = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
# Determines whether the offset pagination strategy is applicable
|
23
|
+
# based on the given HTTP response.
|
24
|
+
#
|
25
|
+
# @param [HttpResponse, nil] _response The response from the previous API call.
|
26
|
+
# @return [Boolean] Always returns true, as this strategy does not depend on the response content.
|
27
|
+
def applicable?(_response)
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Updates the request builder to fetch the next page of results using offset-based pagination.
|
32
|
+
#
|
33
|
+
# If this is the first page, initializes the offset from the request builder.
|
34
|
+
# Otherwise, increments the offset by the previous page size and updates the pagination parameter.
|
35
|
+
#
|
36
|
+
# @param paginated_data [PaginatedData] Contains the last response, request builder, and page size.
|
37
|
+
# @return [Object] An updated request builder configured for the next page request.
|
38
|
+
def apply(paginated_data)
|
39
|
+
last_response = paginated_data.last_response
|
40
|
+
request_builder = paginated_data.request_builder
|
41
|
+
|
42
|
+
# If there is no response yet, this is the first page
|
43
|
+
if last_response.nil?
|
44
|
+
param_value = request_builder.get_parameter_value_by_json_pointer(@input)
|
45
|
+
@offset = param_value.nil? ? 0 : Integer(param_value)
|
46
|
+
|
47
|
+
return request_builder
|
48
|
+
end
|
49
|
+
|
50
|
+
@offset += paginated_data.page_size
|
51
|
+
|
52
|
+
request_builder.get_updated_request_by_json_pointer(@input, @offset)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Applies the metadata wrapper to the given page response, passing the current offset.
|
56
|
+
#
|
57
|
+
# @param page_response [Object] The response object for the current page.
|
58
|
+
# @return [Object] The result of the metadata wrapper with the page response and offset.
|
59
|
+
def apply_metadata_wrapper(page_response)
|
60
|
+
@metadata_wrapper.call(page_response, @offset)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module CoreLibrary
|
2
|
+
# Implements a page-based pagination strategy for API requests.
|
3
|
+
#
|
4
|
+
# This class manages pagination by updating the request builder with the appropriate page number,
|
5
|
+
# using a JSON pointer to identify the pagination parameter. It also applies a metadata wrapper
|
6
|
+
# to each paged response, including the current page number.
|
7
|
+
class PagePagination < PaginationStrategy
|
8
|
+
# Initializes a PagePagination instance with the given input pointer and metadata wrapper.
|
9
|
+
#
|
10
|
+
# @param input [String] JSON pointer indicating the pagination parameter in the request.
|
11
|
+
# @param metadata_wrapper [Proc] A callable for wrapping pagination metadata.
|
12
|
+
# @raise [ArgumentError] If input is nil.
|
13
|
+
def initialize(input, metadata_wrapper)
|
14
|
+
super(metadata_wrapper)
|
15
|
+
|
16
|
+
raise ArgumentError, 'Input pointer for page based pagination cannot be nil' if input.nil?
|
17
|
+
|
18
|
+
@input = input
|
19
|
+
@page_number = 1
|
20
|
+
end
|
21
|
+
|
22
|
+
# Determines whether the page pagination strategy is applicable
|
23
|
+
# based on the given HTTP response.
|
24
|
+
#
|
25
|
+
# @param [HttpResponse, nil] _response The response from the previous API call.
|
26
|
+
# @return [Boolean] Always returns true, as this strategy does not depend on the response content.
|
27
|
+
def applicable?(_response)
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Updates the request builder to fetch the next page of results based on the current paginated data.
|
32
|
+
#
|
33
|
+
# @param paginated_data [PaginatedData] An object containing the last response, request builder, and page size.
|
34
|
+
# @return [Object] The updated request builder configured for the next page request.
|
35
|
+
def apply(paginated_data)
|
36
|
+
last_response = paginated_data.last_response
|
37
|
+
request_builder = paginated_data.request_builder
|
38
|
+
|
39
|
+
# If there is no response yet, this is the first page
|
40
|
+
if last_response.nil?
|
41
|
+
param_value = request_builder.get_parameter_value_by_json_pointer(@input)
|
42
|
+
@page_number = param_value.nil? ? 1 : Integer(param_value)
|
43
|
+
|
44
|
+
return request_builder
|
45
|
+
end
|
46
|
+
|
47
|
+
@page_number += 1 if paginated_data.page_size.positive?
|
48
|
+
|
49
|
+
request_builder.get_updated_request_by_json_pointer(@input, @page_number)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Applies the metadata wrapper to the paged response, including the current page number.
|
53
|
+
#
|
54
|
+
# @param paged_response [Object] The response object for the current page.
|
55
|
+
# @return [Object] The result of the metadata wrapper with the paged response and current page number.
|
56
|
+
def apply_metadata_wrapper(paged_response)
|
57
|
+
@metadata_wrapper.call(paged_response, @page_number)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -1,6 +1,13 @@
|
|
1
1
|
module CoreLibrary
|
2
2
|
# This class is the builder of the http request for an API call.
|
3
3
|
class RequestBuilder
|
4
|
+
PATH_PARAM_POINTER = '$request.path'.freeze
|
5
|
+
QUERY_PARAM_POINTER = '$request.query'.freeze
|
6
|
+
HEADER_PARAM_POINTER = '$request.headers'.freeze
|
7
|
+
BODY_PARAM_POINTER = '$request.body'.freeze
|
8
|
+
|
9
|
+
attr_reader :template_params, :query_params, :header_params, :body_params, :form_params
|
10
|
+
|
4
11
|
# Creates an instance of RequestBuilder.
|
5
12
|
def initialize
|
6
13
|
@server = nil
|
@@ -13,7 +20,7 @@ module CoreLibrary
|
|
13
20
|
@additional_form_params = {}
|
14
21
|
@additional_query_params = {}
|
15
22
|
@multipart_params = {}
|
16
|
-
@
|
23
|
+
@body_params = nil
|
17
24
|
@body_serializer = nil
|
18
25
|
@auth = nil
|
19
26
|
@array_serialization_format = ArraySerializationFormat::INDEXED
|
@@ -112,10 +119,10 @@ module CoreLibrary
|
|
112
119
|
def body_param(body_param)
|
113
120
|
body_param.validate
|
114
121
|
if !body_param.get_key.nil?
|
115
|
-
@
|
116
|
-
@
|
122
|
+
@body_params = {} if @body_params.nil?
|
123
|
+
@body_params[body_param.get_key] = body_param.get_value
|
117
124
|
else
|
118
|
-
@
|
125
|
+
@body_params = body_param.get_value
|
119
126
|
end
|
120
127
|
self
|
121
128
|
end
|
@@ -222,7 +229,11 @@ module CoreLibrary
|
|
222
229
|
_request_headers.merge!(@header_params)
|
223
230
|
end
|
224
231
|
|
225
|
-
_request_headers
|
232
|
+
_serialized_headers = _request_headers.transform_values do |value|
|
233
|
+
value.nil? ? value : ApiHelper.json_serialize(value)
|
234
|
+
end
|
235
|
+
|
236
|
+
_serialized_headers
|
226
237
|
end
|
227
238
|
|
228
239
|
# Processes the body parameter of the request (including form param, json body or xml body).
|
@@ -231,7 +242,7 @@ module CoreLibrary
|
|
231
242
|
_has_form_params = !@form_params.nil? && @form_params.any?
|
232
243
|
_has_additional_form_params = !@additional_form_params.nil? && @additional_form_params.any?
|
233
244
|
_has_multipart_param = !@multipart_params.nil? && @multipart_params.any?
|
234
|
-
_has_body_param = !@
|
245
|
+
_has_body_param = !@body_params.nil?
|
235
246
|
_has_body_serializer = !@body_serializer.nil?
|
236
247
|
_has_xml_attributes = !@xml_attributes.nil?
|
237
248
|
|
@@ -283,15 +294,15 @@ module CoreLibrary
|
|
283
294
|
# Resolves the body parameter to appropriate type.
|
284
295
|
# @return [Hash] The resolved body parameter as per the type.
|
285
296
|
def resolve_body_param
|
286
|
-
if !@
|
287
|
-
@header_params['content-type'] = @
|
288
|
-
|
289
|
-
@header_params['content-length'] = @
|
290
|
-
return @
|
291
|
-
elsif !@
|
292
|
-
@header_params['content-length'] = @
|
297
|
+
if !@body_params.nil? && @body_params.is_a?(FileWrapper)
|
298
|
+
@header_params['content-type'] = @body_params.content_type if
|
299
|
+
!@body_params.file.nil? && !@body_params.content_type.nil?
|
300
|
+
@header_params['content-length'] = @body_params.file.size.to_s
|
301
|
+
return @body_params.file
|
302
|
+
elsif !@body_params.nil? && @body_params.is_a?(File)
|
303
|
+
@header_params['content-length'] = @body_params.size.to_s
|
293
304
|
end
|
294
|
-
@
|
305
|
+
@body_params
|
295
306
|
end
|
296
307
|
|
297
308
|
# Applies the configured auth onto the http request.
|
@@ -302,5 +313,119 @@ module CoreLibrary
|
|
302
313
|
@auth.apply(http_request) if is_valid_auth
|
303
314
|
raise AuthValidationException, @auth.error_message if !@auth.nil? && !is_valid_auth
|
304
315
|
end
|
316
|
+
|
317
|
+
# Updates the given request builder by modifying its path, query,
|
318
|
+
# or header parameters based on the specified JSON pointer and value.
|
319
|
+
#
|
320
|
+
# @param json_pointer [String] JSON pointer indicating which parameter to update.
|
321
|
+
# @param value [Object] The value to set at the specified parameter location.
|
322
|
+
# @return [Object] The updated request builder with the modified parameter.
|
323
|
+
def get_updated_request_by_json_pointer(json_pointer, value)
|
324
|
+
param_pointer, field_pointer = JsonPointerHelper.split_into_parts(json_pointer)
|
325
|
+
|
326
|
+
template_params = nil
|
327
|
+
query_params = nil
|
328
|
+
header_params = nil
|
329
|
+
body_params = nil
|
330
|
+
form_params = nil
|
331
|
+
|
332
|
+
case param_pointer
|
333
|
+
when PATH_PARAM_POINTER
|
334
|
+
template_params = JsonPointerHelper.update_entry_by_json_pointer(
|
335
|
+
DeepCloneUtils.deep_copy(@template_params), "#{field_pointer}/value", value
|
336
|
+
)
|
337
|
+
when QUERY_PARAM_POINTER
|
338
|
+
query_params = JsonPointerHelper.update_entry_by_json_pointer(
|
339
|
+
DeepCloneUtils.deep_copy(@query_params), field_pointer, value
|
340
|
+
)
|
341
|
+
when HEADER_PARAM_POINTER
|
342
|
+
header_params = JsonPointerHelper.update_entry_by_json_pointer(
|
343
|
+
DeepCloneUtils.deep_copy(@header_params), field_pointer, value
|
344
|
+
)
|
345
|
+
when BODY_PARAM_POINTER
|
346
|
+
if @body_params
|
347
|
+
body_params = JsonPointerHelper.update_entry_by_json_pointer(
|
348
|
+
DeepCloneUtils.deep_copy(@body_params), field_pointer, value
|
349
|
+
)
|
350
|
+
else
|
351
|
+
form_params = JsonPointerHelper.update_entry_by_json_pointer(
|
352
|
+
DeepCloneUtils.deep_copy(@form_params), field_pointer, value
|
353
|
+
)
|
354
|
+
end
|
355
|
+
else
|
356
|
+
clone_with
|
357
|
+
end
|
358
|
+
|
359
|
+
clone_with(
|
360
|
+
template_params: template_params,
|
361
|
+
query_params: query_params,
|
362
|
+
header_params: header_params,
|
363
|
+
body_params: body_params,
|
364
|
+
form_params: form_params
|
365
|
+
)
|
366
|
+
end
|
367
|
+
|
368
|
+
# Extracts the initial pagination offset value from the request builder using the specified JSON pointer.
|
369
|
+
#
|
370
|
+
# @param json_pointer [String] JSON pointer indicating which parameter to extract.
|
371
|
+
# @return [Object, nil] The initial offset value from the specified parameter, or default if not found.
|
372
|
+
def get_parameter_value_by_json_pointer(json_pointer)
|
373
|
+
param_pointer, field_pointer = JsonPointerHelper.split_into_parts(json_pointer)
|
374
|
+
|
375
|
+
case param_pointer
|
376
|
+
when PATH_PARAM_POINTER
|
377
|
+
JsonPointerHelper.get_value_by_json_pointer(
|
378
|
+
@template_params, "#{field_pointer}/value"
|
379
|
+
)
|
380
|
+
when QUERY_PARAM_POINTER
|
381
|
+
JsonPointerHelper.get_value_by_json_pointer(@query_params, field_pointer)
|
382
|
+
when HEADER_PARAM_POINTER
|
383
|
+
JsonPointerHelper.get_value_by_json_pointer(@header_params, field_pointer)
|
384
|
+
when BODY_PARAM_POINTER
|
385
|
+
JsonPointerHelper.get_value_by_json_pointer(
|
386
|
+
@body_params || @form_params, field_pointer
|
387
|
+
)
|
388
|
+
else
|
389
|
+
nil
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# Creates a deep copy of this RequestBuilder instance with optional overrides.
|
394
|
+
#
|
395
|
+
# @param template_params [Hash, nil] Optional replacement for template_params
|
396
|
+
# @param query_params [Hash, nil] Optional replacement for query_params
|
397
|
+
# @param header_params [Hash, nil] Optional replacement for header_params
|
398
|
+
# @param body_params [Object, nil] Optional replacement for body_params
|
399
|
+
# @param form_params [Hash, nil] Optional replacement for form_params
|
400
|
+
#
|
401
|
+
# @return [RequestBuilder] A new instance with copied state and applied overrides
|
402
|
+
def clone_with(
|
403
|
+
template_params: nil,
|
404
|
+
query_params: nil,
|
405
|
+
header_params: nil,
|
406
|
+
body_params: nil,
|
407
|
+
form_params: nil
|
408
|
+
)
|
409
|
+
clone = RequestBuilder.new
|
410
|
+
|
411
|
+
# Manually copy internal state
|
412
|
+
clone.instance_variable_set(:@server, DeepCloneUtils.deep_copy(@server))
|
413
|
+
clone.instance_variable_set(:@path, DeepCloneUtils.deep_copy(@path))
|
414
|
+
clone.instance_variable_set(:@http_method, DeepCloneUtils.deep_copy(@http_method))
|
415
|
+
clone.instance_variable_set(:@template_params, template_params || DeepCloneUtils.deep_copy(@template_params))
|
416
|
+
clone.instance_variable_set(:@header_params, header_params || DeepCloneUtils.deep_copy(@header_params))
|
417
|
+
clone.instance_variable_set(:@query_params, query_params || DeepCloneUtils.deep_copy(@query_params))
|
418
|
+
clone.instance_variable_set(:@form_params, form_params || DeepCloneUtils.deep_copy(@form_params))
|
419
|
+
clone.instance_variable_set(:@additional_form_params, DeepCloneUtils.deep_copy(@additional_form_params))
|
420
|
+
clone.instance_variable_set(:@additional_query_params, DeepCloneUtils.deep_copy(@additional_query_params))
|
421
|
+
clone.instance_variable_set(:@multipart_params, DeepCloneUtils.deep_copy(@multipart_params))
|
422
|
+
clone.instance_variable_set(:@body_params, body_params || DeepCloneUtils.deep_copy(@body_params))
|
423
|
+
clone.instance_variable_set(:@body_serializer, DeepCloneUtils.deep_copy(@body_serializer))
|
424
|
+
clone.instance_variable_set(:@auth, DeepCloneUtils.deep_copy(@auth))
|
425
|
+
clone.instance_variable_set(:@array_serialization_format, DeepCloneUtils.deep_copy(@array_serialization_format))
|
426
|
+
clone.instance_variable_set(:@xml_attributes, DeepCloneUtils.deep_copy(@xml_attributes))
|
427
|
+
|
428
|
+
clone
|
429
|
+
end
|
305
430
|
end
|
306
431
|
end
|
@@ -16,6 +16,7 @@ module CoreLibrary
|
|
16
16
|
@is_date_response = false
|
17
17
|
@is_response_array = false
|
18
18
|
@is_response_void = false
|
19
|
+
@is_nullable_response = false
|
19
20
|
end
|
20
21
|
|
21
22
|
# Sets deserializer for the response.
|
@@ -85,7 +86,7 @@ module CoreLibrary
|
|
85
86
|
# Sets the is_primitive_response property.
|
86
87
|
# @param [Boolean] is_primitive_response Flag if the response is of primitive type.
|
87
88
|
# @return [ResponseHandler] An updated instance of ResponseHandler.
|
88
|
-
# rubocop:disable Naming/PredicateName
|
89
|
+
# rubocop:disable Naming/PredicateName, Naming/PredicatePrefix
|
89
90
|
def is_primitive_response(is_primitive_response)
|
90
91
|
@is_primitive_response = is_primitive_response
|
91
92
|
self
|
@@ -138,7 +139,15 @@ module CoreLibrary
|
|
138
139
|
@is_response_void = is_response_void
|
139
140
|
self
|
140
141
|
end
|
141
|
-
|
142
|
+
|
143
|
+
# Sets the is_nullable_response property.
|
144
|
+
# @param [Boolean] is_nullable_response Flag to return early in case of empty response payload.
|
145
|
+
# @return [ResponseHandler] An updated instance of ResponseHandler.
|
146
|
+
def is_nullable_response(is_nullable_response)
|
147
|
+
@is_nullable_response = is_nullable_response
|
148
|
+
self
|
149
|
+
end
|
150
|
+
# rubocop:enable Naming/PredicateName, Naming/PredicatePrefix
|
142
151
|
|
143
152
|
# Main method to handle the response with all the set properties.
|
144
153
|
# @param [HttpResponse] response The response received.
|
@@ -152,7 +161,7 @@ module CoreLibrary
|
|
152
161
|
# validating response if configured
|
153
162
|
validate(response, global_errors)
|
154
163
|
|
155
|
-
return if @is_response_void
|
164
|
+
return if @is_response_void && !@is_api_response
|
156
165
|
|
157
166
|
# applying deserializer if configured
|
158
167
|
deserialized_value = apply_deserializer(response, should_symbolize_hash)
|
@@ -191,6 +200,8 @@ module CoreLibrary
|
|
191
200
|
# Applies deserializer to the response.
|
192
201
|
# @param [Boolean] should_symbolize_hash Flag to symbolize the hash during response deserialization.
|
193
202
|
def apply_deserializer(response, should_symbolize_hash)
|
203
|
+
return if @is_nullable_response && (response.raw_body.nil? || response.raw_body.to_s.strip.empty?)
|
204
|
+
|
194
205
|
return apply_xml_deserializer(response) if @is_xml_response
|
195
206
|
return response.raw_body if @deserializer.nil?
|
196
207
|
|
@@ -45,12 +45,12 @@ module CoreLibrary
|
|
45
45
|
# The setter for the flag if the parameter is required.
|
46
46
|
# @param [Boolean] is_required true if the parameter is required otherwise false, by default the value is false.
|
47
47
|
# @return [Parameter] An updated instance of Parameter.
|
48
|
-
# rubocop:disable Naming/PredicateName
|
48
|
+
# rubocop:disable Naming/PredicateName, Naming/PredicatePrefix
|
49
49
|
def is_required(is_required)
|
50
50
|
@is_required = is_required
|
51
51
|
self
|
52
52
|
end
|
53
|
-
# rubocop:enable Naming/PredicateName
|
53
|
+
# rubocop:enable Naming/PredicateName, Naming/PredicatePrefix
|
54
54
|
|
55
55
|
# The setter for the flag if the parameter value is to be encoded.
|
56
56
|
# @param [Boolean] should_encode true if the parameter value is to be encoded otherwise false, default is false.
|
@@ -16,7 +16,7 @@ module CoreLibrary
|
|
16
16
|
|
17
17
|
# Override for additional model properties.
|
18
18
|
def respond_to_missing?(method_sym, include_private = false)
|
19
|
-
instance_variable_defined?("@#{method_sym}")
|
19
|
+
instance_variable_defined?("@#{method_sym}") || super
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'erb'
|
2
|
+
require 'uri'
|
3
|
+
require 'cgi'
|
2
4
|
|
3
5
|
module CoreLibrary
|
4
6
|
# API utility class involved in executing an API
|
@@ -290,7 +292,7 @@ module CoreLibrary
|
|
290
292
|
# @return [Hash, Array, nil] The parsed JSON object, or nil if the input string is nil.
|
291
293
|
# @raise [TypeError] if the server responds with invalid JSON and primitive type parsing is not allowed.
|
292
294
|
def self.json_deserialize(json, should_symbolize = false, allow_primitive_type_parsing = false)
|
293
|
-
return if json.nil?
|
295
|
+
return if json.nil? || json.to_s.strip.empty?
|
294
296
|
|
295
297
|
begin
|
296
298
|
JSON.parse(json, symbolize_names: should_symbolize)
|
@@ -375,12 +377,12 @@ module CoreLibrary
|
|
375
377
|
def self.form_encode(obj, instance_name, formatting: ArraySerializationFormat::INDEXED)
|
376
378
|
retval = {}
|
377
379
|
|
378
|
-
# If this is a structure, resolve
|
380
|
+
# If this is a structure, resolve its field names.
|
379
381
|
obj = obj.to_hash if obj.is_a? BaseModel
|
380
382
|
|
381
383
|
# Create a form encoded hash for this object.
|
382
384
|
if obj.nil?
|
383
|
-
|
385
|
+
return retval
|
384
386
|
elsif obj.instance_of? Array
|
385
387
|
if formatting == ArraySerializationFormat::INDEXED
|
386
388
|
obj.each_with_index do |value, index|
|
@@ -415,6 +417,7 @@ module CoreLibrary
|
|
415
417
|
else
|
416
418
|
retval[instance_name] = obj
|
417
419
|
end
|
420
|
+
|
418
421
|
retval
|
419
422
|
end
|
420
423
|
|
@@ -442,6 +445,73 @@ module CoreLibrary
|
|
442
445
|
val
|
443
446
|
end
|
444
447
|
|
448
|
+
# Apply unboxing_function to additional properties from hash.
|
449
|
+
# @param [Hash] hash The hash to extract additional properties from.
|
450
|
+
# @param [Proc] unboxing_function The deserializer to apply to each item in the hash.
|
451
|
+
# @return [Hash] A hash containing the additional properties and their values.
|
452
|
+
def self.get_additional_properties(hash, unboxing_function, is_array: false, is_dict: false, is_array_of_map: false,
|
453
|
+
is_map_of_array: false, dimension_count: 1)
|
454
|
+
additional_properties = {}
|
455
|
+
|
456
|
+
# Iterate over each key-value pair in the input hash
|
457
|
+
hash.each do |key, value|
|
458
|
+
# Prepare arguments for apply_unboxing_function
|
459
|
+
args = {
|
460
|
+
is_array: is_array,
|
461
|
+
is_dict: is_dict,
|
462
|
+
is_array_of_map: is_array_of_map,
|
463
|
+
is_map_of_array: is_map_of_array,
|
464
|
+
dimension_count: dimension_count
|
465
|
+
}
|
466
|
+
|
467
|
+
# If the value is a complex structure (Hash or Array), apply apply_unboxing_function
|
468
|
+
additional_properties[key] = if is_array || is_dict
|
469
|
+
apply_unboxing_function(value, unboxing_function, **args)
|
470
|
+
else
|
471
|
+
# Apply the unboxing function directly for simple values
|
472
|
+
unboxing_function.call(value)
|
473
|
+
end
|
474
|
+
rescue StandardError
|
475
|
+
# Ignore the exception and continue processing
|
476
|
+
end
|
477
|
+
|
478
|
+
additional_properties
|
479
|
+
end
|
480
|
+
|
481
|
+
def self.apply_unboxing_function(obj, unboxing_function, is_array: false, is_dict: false, is_array_of_map: false,
|
482
|
+
is_map_of_array: false, dimension_count: 1)
|
483
|
+
if is_dict
|
484
|
+
if is_map_of_array
|
485
|
+
# Handle case where the object is a map of arrays (Hash with array values)
|
486
|
+
obj.transform_values do |v|
|
487
|
+
apply_unboxing_function(v, unboxing_function, is_array: true, dimension_count: dimension_count)
|
488
|
+
end
|
489
|
+
else
|
490
|
+
# Handle regular Hash (map) case
|
491
|
+
obj.transform_values { |v| unboxing_function.call(v) }
|
492
|
+
end
|
493
|
+
elsif is_array
|
494
|
+
if is_array_of_map
|
495
|
+
# Handle case where the object is an array of maps (Array of Hashes)
|
496
|
+
obj.map do |element|
|
497
|
+
apply_unboxing_function(element, unboxing_function, is_dict: true, dimension_count: dimension_count)
|
498
|
+
end
|
499
|
+
elsif dimension_count > 1
|
500
|
+
# Handle multi-dimensional array
|
501
|
+
obj.map do |element|
|
502
|
+
apply_unboxing_function(element, unboxing_function, is_array: true,
|
503
|
+
dimension_count: dimension_count - 1)
|
504
|
+
end
|
505
|
+
else
|
506
|
+
# Handle regular Array case
|
507
|
+
obj.map { |element| unboxing_function.call(element) }
|
508
|
+
end
|
509
|
+
else
|
510
|
+
# Handle base case where the object is neither Array nor Hash
|
511
|
+
unboxing_function.call(obj)
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
445
515
|
# Get content-type depending on the value
|
446
516
|
# @param [Object] value The value for which the content-type is resolved.
|
447
517
|
def self.get_content_type(value)
|
@@ -476,7 +546,7 @@ module CoreLibrary
|
|
476
546
|
if placeholder.include? '#'
|
477
547
|
# pick the 2nd chunk then remove the last character (i.e. `}`) of the string value
|
478
548
|
node_pointer = placeholder.split('#')[1].delete_suffix('}')
|
479
|
-
value_pointer =
|
549
|
+
value_pointer = JsonPointer.new(value, node_pointer, symbolize_keys: true)
|
480
550
|
extracted_value = json_serialize(value_pointer.value) if value_pointer.exists?
|
481
551
|
elsif !value.nil?
|
482
552
|
extracted_value = json_serialize(value)
|
@@ -513,5 +583,41 @@ module CoreLibrary
|
|
513
583
|
|
514
584
|
template
|
515
585
|
end
|
586
|
+
|
587
|
+
# Parses query parameters from a given URL.
|
588
|
+
# Returns a Hash with decoded keys and values.
|
589
|
+
# If a key has multiple values, they are returned as an array.
|
590
|
+
#
|
591
|
+
# Example:
|
592
|
+
# ApiHelper.get_query_parameters("https://example.com?a=1&b=2&b=3")
|
593
|
+
# => {"a"=>"1", "b"=>["2", "3"]}
|
594
|
+
def self.get_query_parameters(url)
|
595
|
+
return {} if url.nil? || url.strip.empty?
|
596
|
+
|
597
|
+
begin
|
598
|
+
uri = URI.parse(url)
|
599
|
+
query = uri.query
|
600
|
+
return {} if query.nil? || query.strip.empty?
|
601
|
+
|
602
|
+
parsed = CGI.parse(query)
|
603
|
+
|
604
|
+
# Convert arrays to string or handle empty ones as ""
|
605
|
+
parsed.transform_values! do |v|
|
606
|
+
if v.empty?
|
607
|
+
''
|
608
|
+
elsif v.size == 1
|
609
|
+
v.first
|
610
|
+
else
|
611
|
+
v
|
612
|
+
end
|
613
|
+
end
|
614
|
+
rescue URI::InvalidURIError => e
|
615
|
+
warn "Invalid URL provided: #{e.message}"
|
616
|
+
{}
|
617
|
+
rescue StandardError => e
|
618
|
+
warn "Unexpected error while parsing URL: #{e.message}"
|
619
|
+
{}
|
620
|
+
end
|
621
|
+
end
|
516
622
|
end
|
517
623
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module CoreLibrary
|
2
|
+
# DeepCloneUtils provides utility methods for performing deep copies of various Ruby objects.
|
3
|
+
#
|
4
|
+
# It supports deep copying of arrays (including nested arrays), hashes (with nested keys/values),
|
5
|
+
# and arbitrary objects. For custom objects, it attempts to use the `deep_copy` method if defined;
|
6
|
+
# otherwise, it falls back to using `dup` with error handling.
|
7
|
+
#
|
8
|
+
# The implementation also keeps track of already visited objects using identity-based hashing
|
9
|
+
# to safely handle circular references and avoid redundant cloning.
|
10
|
+
#
|
11
|
+
# Example usage:
|
12
|
+
# original = { foo: [1, 2, { bar: 3 }] }
|
13
|
+
# copy = CoreLibrary::DeepCloneUtils.deep_copy(original)
|
14
|
+
#
|
15
|
+
# copy[:foo][2][:bar] = 99
|
16
|
+
# puts original[:foo][2][:bar] # => 3 (remains unchanged)
|
17
|
+
class DeepCloneUtils
|
18
|
+
class << self
|
19
|
+
# Deep copy any value with support for arrays, hashes, and custom deep_copy methods.
|
20
|
+
#
|
21
|
+
# @param value [Object] The value to deeply clone.
|
22
|
+
# @param visited [Hash] A hash that tracks already visited objects to handle cycles.
|
23
|
+
# @return [Object] A deep copy of the input value.
|
24
|
+
def deep_copy(value, visited = {}.compare_by_identity)
|
25
|
+
return value if primitive?(value)
|
26
|
+
return visited[value] if visited.key?(value)
|
27
|
+
|
28
|
+
result = case value
|
29
|
+
when Array
|
30
|
+
deep_copy_array(value, visited)
|
31
|
+
when Hash
|
32
|
+
deep_copy_hash(value, visited)
|
33
|
+
else
|
34
|
+
deep_copy_object(value)
|
35
|
+
end
|
36
|
+
|
37
|
+
visited[value] = result
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
# Deep copy a plain array (supports n-dimensional arrays).
|
42
|
+
#
|
43
|
+
# @param array [Array] The array to deeply clone.
|
44
|
+
# @param visited [Hash] Identity hash to track visited objects.
|
45
|
+
# @return [Array] A deep copy of the array.
|
46
|
+
def deep_copy_array(array, visited = {}.compare_by_identity)
|
47
|
+
return nil if array.nil?
|
48
|
+
return array if primitive?(array)
|
49
|
+
|
50
|
+
visited[array] = array_clone = []
|
51
|
+
|
52
|
+
array.each do |item|
|
53
|
+
array_clone << deep_copy(item, visited)
|
54
|
+
end
|
55
|
+
|
56
|
+
array_clone
|
57
|
+
end
|
58
|
+
|
59
|
+
# Deep copy a hash (map), including nested maps.
|
60
|
+
#
|
61
|
+
# @param hash [Hash] The hash to deeply clone.
|
62
|
+
# @param visited [Hash] Identity hash to track visited objects.
|
63
|
+
# @return [Hash] A deep copy of the hash.
|
64
|
+
def deep_copy_hash(hash, visited = {}.compare_by_identity)
|
65
|
+
return nil if hash.nil?
|
66
|
+
return hash if primitive?(hash)
|
67
|
+
|
68
|
+
visited[hash] = hash_clone = {}
|
69
|
+
|
70
|
+
hash.each do |key, value|
|
71
|
+
# Keys are usually immutable, but still cloned for safety
|
72
|
+
key_copy = primitive?(key) ? key : deep_copy(key, visited)
|
73
|
+
hash_clone[key_copy] = deep_copy(value, visited)
|
74
|
+
end
|
75
|
+
|
76
|
+
hash_clone
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Deep copy any non-collection object.
|
82
|
+
#
|
83
|
+
# @param obj [Object] The object to deeply clone.
|
84
|
+
# @return [Object] A deep copy of the object.
|
85
|
+
def deep_copy_object(obj)
|
86
|
+
return obj if obj.nil?
|
87
|
+
|
88
|
+
if obj.respond_to?(:deep_copy)
|
89
|
+
obj.deep_copy
|
90
|
+
elsif obj.respond_to?(:dup)
|
91
|
+
begin
|
92
|
+
obj.dup
|
93
|
+
rescue TypeError
|
94
|
+
obj
|
95
|
+
end
|
96
|
+
else
|
97
|
+
obj
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Identify values that do not need deep copy.
|
102
|
+
#
|
103
|
+
# @param value [Object] The value to check.
|
104
|
+
# @return [Boolean] Whether the value is primitive.
|
105
|
+
def primitive?(value)
|
106
|
+
value.is_a?(Numeric) ||
|
107
|
+
value.is_a?(Symbol) ||
|
108
|
+
value.is_a?(TrueClass) ||
|
109
|
+
value.is_a?(FalseClass) ||
|
110
|
+
value.nil?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|