apimatic_core 0.3.14 → 0.3.16
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 +37 -22
- data/lib/apimatic-core/api_call.rb +54 -5
- 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 +134 -13
- data/lib/apimatic-core/response_handler.rb +2 -2
- data/lib/apimatic-core/types/parameter.rb +2 -2
- data/lib/apimatic-core/utilities/api_helper.rb +39 -1
- data/lib/apimatic-core/utilities/deep_clone_utils.rb +114 -0
- data/lib/apimatic-core/utilities/file_helper.rb +6 -6
- 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 +11 -2
@@ -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
|
@@ -235,7 +242,7 @@ module CoreLibrary
|
|
235
242
|
_has_form_params = !@form_params.nil? && @form_params.any?
|
236
243
|
_has_additional_form_params = !@additional_form_params.nil? && @additional_form_params.any?
|
237
244
|
_has_multipart_param = !@multipart_params.nil? && @multipart_params.any?
|
238
|
-
_has_body_param = !@
|
245
|
+
_has_body_param = !@body_params.nil?
|
239
246
|
_has_body_serializer = !@body_serializer.nil?
|
240
247
|
_has_xml_attributes = !@xml_attributes.nil?
|
241
248
|
|
@@ -287,15 +294,15 @@ module CoreLibrary
|
|
287
294
|
# Resolves the body parameter to appropriate type.
|
288
295
|
# @return [Hash] The resolved body parameter as per the type.
|
289
296
|
def resolve_body_param
|
290
|
-
if !@
|
291
|
-
@header_params['content-type'] = @
|
292
|
-
|
293
|
-
@header_params['content-length'] = @
|
294
|
-
return @
|
295
|
-
elsif !@
|
296
|
-
@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
|
297
304
|
end
|
298
|
-
@
|
305
|
+
@body_params
|
299
306
|
end
|
300
307
|
|
301
308
|
# Applies the configured auth onto the http request.
|
@@ -306,5 +313,119 @@ module CoreLibrary
|
|
306
313
|
@auth.apply(http_request) if is_valid_auth
|
307
314
|
raise AuthValidationException, @auth.error_message if !@auth.nil? && !is_valid_auth
|
308
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
|
309
430
|
end
|
310
431
|
end
|
@@ -86,7 +86,7 @@ module CoreLibrary
|
|
86
86
|
# Sets the is_primitive_response property.
|
87
87
|
# @param [Boolean] is_primitive_response Flag if the response is of primitive type.
|
88
88
|
# @return [ResponseHandler] An updated instance of ResponseHandler.
|
89
|
-
# rubocop:disable Naming/PredicateName
|
89
|
+
# rubocop:disable Naming/PredicateName, Naming/PredicatePrefix
|
90
90
|
def is_primitive_response(is_primitive_response)
|
91
91
|
@is_primitive_response = is_primitive_response
|
92
92
|
self
|
@@ -147,7 +147,7 @@ module CoreLibrary
|
|
147
147
|
@is_nullable_response = is_nullable_response
|
148
148
|
self
|
149
149
|
end
|
150
|
-
# rubocop:enable Naming/PredicateName
|
150
|
+
# rubocop:enable Naming/PredicateName, Naming/PredicatePrefix
|
151
151
|
|
152
152
|
# Main method to handle the response with all the set properties.
|
153
153
|
# @param [HttpResponse] response The response received.
|
@@ -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.
|
@@ -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
|
@@ -544,7 +546,7 @@ module CoreLibrary
|
|
544
546
|
if placeholder.include? '#'
|
545
547
|
# pick the 2nd chunk then remove the last character (i.e. `}`) of the string value
|
546
548
|
node_pointer = placeholder.split('#')[1].delete_suffix('}')
|
547
|
-
value_pointer =
|
549
|
+
value_pointer = JsonPointer.new(value, node_pointer, symbolize_keys: true)
|
548
550
|
extracted_value = json_serialize(value_pointer.value) if value_pointer.exists?
|
549
551
|
elsif !value.nil?
|
550
552
|
extracted_value = json_serialize(value)
|
@@ -581,5 +583,41 @@ module CoreLibrary
|
|
581
583
|
|
582
584
|
template
|
583
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
|
584
622
|
end
|
585
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
|
@@ -9,19 +9,19 @@ module CoreLibrary
|
|
9
9
|
# Class method which takes a URL, downloads the file (if not already downloaded
|
10
10
|
# for this test session), and returns the file path.
|
11
11
|
# @param [String] url The URL of the required file.
|
12
|
-
# @return [
|
12
|
+
# @return [Tempfile] The downloaded file.
|
13
13
|
def self.get_file(url)
|
14
|
-
|
14
|
+
if @cache.key?(url)
|
15
|
+
@cache[url].rewind
|
16
|
+
return @cache[url]
|
17
|
+
end
|
15
18
|
|
16
19
|
tempfile = Tempfile.new('APIMatic')
|
17
20
|
tempfile.binmode
|
18
21
|
tempfile.write(URI.parse(url).open(ssl_ca_cert: Certifi.where).read)
|
19
22
|
tempfile.flush
|
20
|
-
tempfile.close
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
@cache[url] = tempfile.path.to_s # Store only the file path
|
24
|
+
@cache[url] = tempfile
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|