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.
@@ -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
@@ -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 = !@body_param.nil?
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 !@body_param.nil? && @body_param.is_a?(FileWrapper)
287
- @header_params['content-type'] = @body_param.content_type if !@body_param.file.nil? &&
288
- !@body_param.content_type.nil?
289
- @header_params['content-length'] = @body_param.file.size.to_s
290
- return @body_param.file
291
- elsif !@body_param.nil? && @body_param.is_a?(File)
292
- @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
293
304
  end
294
- @body_param
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
- # rubocop:enable Naming/PredicateName
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}") ? true : super
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 it's field names.
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
- nil
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 = JsonPointerHelper.new(value, node_pointer, symbolize_keys: true)
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