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.
@@ -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
- @body_param = nil
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
- @body_param = {} if @body_param.nil?
116
- @body_param[body_param.get_key] = body_param.get_value
122
+ @body_params = {} if @body_params.nil?
123
+ @body_params[body_param.get_key] = body_param.get_value
117
124
  else
118
- @body_param = body_param.get_value
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 = !@body_param.nil?
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 !@body_param.nil? && @body_param.is_a?(FileWrapper)
291
- @header_params['content-type'] = @body_param.content_type if !@body_param.file.nil? &&
292
- !@body_param.content_type.nil?
293
- @header_params['content-length'] = @body_param.file.size.to_s
294
- return @body_param.file
295
- elsif !@body_param.nil? && @body_param.is_a?(File)
296
- @header_params['content-length'] = @body_param.size.to_s
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
- @body_param
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 = JsonPointerHelper.new(value, node_pointer, symbolize_keys: true)
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 [String] The path of the downloaded file.
12
+ # @return [Tempfile] The downloaded file.
13
13
  def self.get_file(url)
14
- return @cache[url] if @cache.key?(url)
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
- raise 'Tempfile path is nil!' if tempfile.path.nil?
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