Skip to content

Commit

Permalink
feat(ruby): add support of additional properties (#48)
Browse files Browse the repository at this point in the history
* feat(ruby): add support of additional properties

closes #47

Co-authored-by: MaryamAdnan3 <maryamadnan544@gmail.com>
  • Loading branch information
MaryamAdnan3 and MaryamAdnan33 authored Nov 28, 2024
1 parent f22e319 commit 606255f
Show file tree
Hide file tree
Showing 15 changed files with 532 additions and 29 deletions.
2 changes: 1 addition & 1 deletion apimatic_core.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Gem::Specification.new do |s|
s.name = 'apimatic_core'
s.version = '0.3.10'
s.version = '0.3.11'
s.summary = 'A library that contains apimatic-apimatic-core logic and utilities for consuming REST APIs using Python SDKs generated '\
'by APIMatic.'
s.description = 'The APIMatic Core libraries provide a stable runtime that powers all the functionality of SDKs.'\
Expand Down
67 changes: 67 additions & 0 deletions lib/apimatic-core/utilities/api_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,73 @@ def self.map_response(obj, keys)
val
end

# Apply unboxing_function to additional properties from hash.
# @param [Hash] hash The hash to extract additional properties from.
# @param [Proc] unboxing_function The deserializer to apply to each item in the hash.
# @return [Hash] A hash containing the additional properties and their values.
def self.get_additional_properties(hash, unboxing_function, is_array: false, is_dict: false, is_array_of_map: false,
is_map_of_array: false, dimension_count: 1)
additional_properties = {}

# Iterate over each key-value pair in the input hash
hash.each do |key, value|
# Prepare arguments for apply_unboxing_function
args = {
is_array: is_array,
is_dict: is_dict,
is_array_of_map: is_array_of_map,
is_map_of_array: is_map_of_array,
dimension_count: dimension_count
}

# If the value is a complex structure (Hash or Array), apply apply_unboxing_function
additional_properties[key] = if is_array || is_dict
apply_unboxing_function(value, unboxing_function, **args)
else
# Apply the unboxing function directly for simple values
unboxing_function.call(value)
end
rescue StandardError
# Ignore the exception and continue processing
end

additional_properties
end

def self.apply_unboxing_function(obj, unboxing_function, is_array: false, is_dict: false, is_array_of_map: false,
is_map_of_array: false, dimension_count: 1)
if is_dict
if is_map_of_array
# Handle case where the object is a map of arrays (Hash with array values)
obj.transform_values do |v|
apply_unboxing_function(v, unboxing_function, is_array: true, dimension_count: dimension_count)
end
else
# Handle regular Hash (map) case
obj.transform_values { |v| unboxing_function.call(v) }
end
elsif is_array
if is_array_of_map
# Handle case where the object is an array of maps (Array of Hashes)
obj.map do |element|
apply_unboxing_function(element, unboxing_function, is_dict: true, dimension_count: dimension_count)
end
elsif dimension_count > 1
# Handle multi-dimensional array
obj.map do |element|
apply_unboxing_function(element, unboxing_function, is_array: true,
dimension_count: dimension_count - 1)
end
else
# Handle regular Array case
obj.map { |element| unboxing_function.call(element) }
end
else
# Handle base case where the object is neither Array nor Hash
unboxing_function.call(obj)
end
end

# Get content-type depending on the value
# @param [Object] value The value for which the content-type is resolved.
def self.get_content_type(value)
Expand Down
2 changes: 2 additions & 0 deletions test/test-apimatic-core/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
# test constants
TEST_TOKEN = 'MyDuMmYtOkEn'.freeze
JSON_CONTENT_TYPE = 'application/json'.freeze
FORM_PARAM_KEY = 'form_param'.freeze
TEST_EMAIL = 'test@gmail.com'
186 changes: 183 additions & 3 deletions test/test-apimatic-core/utilities/api_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_relative '../../test-helper/models/person'
require_relative '../../test-helper/models/morning'
require_relative '../../../lib/apimatic-core/utilities/file_helper'
require_relative '../test_helper'
require 'faraday'

class ApiHelperTest < Minitest::Test
Expand Down Expand Up @@ -313,6 +314,37 @@ def test_form_encode

end

def test_form_encode_with_additional_properties
key = FORM_PARAM_KEY
test_cases = [
[TestComponent::MockHelper.get_model_with_additional_properties_of_primitive_type_success,
{ "#{FORM_PARAM_KEY}[email]" => "#{TEST_EMAIL}", "#{FORM_PARAM_KEY}[prop]" => 20 }],
[TestComponent::MockHelper.get_model_with_additional_properties_of_primitive_array_type,
{ "#{FORM_PARAM_KEY}[email]" => "#{TEST_EMAIL}", "#{FORM_PARAM_KEY}[prop][0]" => 20, "#{FORM_PARAM_KEY}[prop][1]" => 30 }],
[TestComponent::MockHelper.get_model_with_additional_properties_of_primitive_dict_type,
{ "#{FORM_PARAM_KEY}[email]" => "#{TEST_EMAIL}", "#{FORM_PARAM_KEY}[prop][inner prop 1]" => 20, "#{FORM_PARAM_KEY}[prop][inner prop 2]" => 30 }],
[TestComponent::MockHelper.get_model_with_additional_properties_of_model_type,
{ "#{FORM_PARAM_KEY}[email]" => "#{TEST_EMAIL}", "#{FORM_PARAM_KEY}[prop1][starts_at]" => "8:00", "#{FORM_PARAM_KEY}[prop1][ends_at]" => "10:00",
"#{FORM_PARAM_KEY}[prop1][offer_dinner]" => true, "#{FORM_PARAM_KEY}[prop1][session_type]" => "Evening" }],
[TestComponent::MockHelper.get_model_with_additional_properties_of_model_array_type,
{ "#{FORM_PARAM_KEY}[email]" => "#{TEST_EMAIL}",
"#{FORM_PARAM_KEY}[prop1][0][starts_at]" => "8:00", "#{FORM_PARAM_KEY}[prop1][0][ends_at]" => "10:00", "#{FORM_PARAM_KEY}[prop1][0][offer_dinner]" => true, "#{FORM_PARAM_KEY}[prop1][0][session_type]" => "Evening",
"#{FORM_PARAM_KEY}[prop1][1][starts_at]" => "8:00", "#{FORM_PARAM_KEY}[prop1][1][ends_at]" => "10:00", "#{FORM_PARAM_KEY}[prop1][1][offer_dinner]" => true, "#{FORM_PARAM_KEY}[prop1][1][session_type]" => "Evening" }],
[TestComponent::MockHelper.get_model_with_additional_properties_of_model_dict_type,
{ "#{FORM_PARAM_KEY}[email]" => "#{TEST_EMAIL}",
"#{FORM_PARAM_KEY}[prop1][inner_prop1][starts_at]" => "8:00", "#{FORM_PARAM_KEY}[prop1][inner_prop1][ends_at]" => "10:00", "#{FORM_PARAM_KEY}[prop1][inner_prop1][offer_dinner]" => true, "#{FORM_PARAM_KEY}[prop1][inner_prop1][session_type]" => "Evening",
"#{FORM_PARAM_KEY}[prop1][inner_prop2][starts_at]" => "8:00", "#{FORM_PARAM_KEY}[prop1][inner_prop2][ends_at]" => "10:00", "#{FORM_PARAM_KEY}[prop1][inner_prop2][offer_dinner]" => true, "#{FORM_PARAM_KEY}[prop1][inner_prop2][session_type]" => "Evening" }],
[TestComponent::MockHelper.get_model_with_additional_properties_of_type_combinator_primitive_type,
{ "#{FORM_PARAM_KEY}[email]" => "#{TEST_EMAIL}", "#{FORM_PARAM_KEY}[prop]" => 10.55 }]
]

# Iterate through each test case
test_cases.each do |input_value, expected_form_params|
assert_equal(ApiHelper.form_encode(input_value, key, formatting: ArraySerializationFormat::INDEXED),
expected_form_params)
end
end

def test_custom_merge
assert_equal(ApiHelper.custom_merge({ "number1" => 1, "string1" => ["a", "b", "d"], "same" => "c" },
{ "number2" => 1, "string2" => ["d", "e"], "same" => "c" }),
Expand Down Expand Up @@ -359,7 +391,27 @@ def test_json_serialize
"\"birthtime\":\"2016-03-13T12:52:32+00:00\",\"name\":\"Jone\",\"uid\":\"1234\",\"personType\":\"Per\"}"
)
assert_equal(ApiHelper.json_serialize(123), "123")
end

def test_json_serialize_with_exception
test_cases = [
[
TestComponent::MockHelper.get_model_with_additional_properties_of_primitive_type,
"An additional property key, 'email' conflicts with one of the model's properties"
]
]

test_cases.each do |input_value, expected_validation_message|
assert_raises(StandardError) do
ApiHelper.json_serialize(input_value)
end

begin
ApiHelper.json_serialize(input_value)
rescue StandardError => e
assert_equal expected_validation_message, e.message
end
end
end

def test_update_user_agent_value_with_parameters
Expand Down Expand Up @@ -523,6 +575,59 @@ def test_valid_type_model_array_of_map
is_model_hash: true, is_inner_model_hash: true)
end

def test_json_deserialize
test_cases = [
['{"email":"test","prop1":1,"prop2":2,"prop3":"invalid type"}',
TestComponent::ModelWithAdditionalPropertiesOfPrimitiveType, false,
'{"email":"test","prop1":1,"prop2":2}'],

['{"email":"test","prop1":[1,2,3],"prop2":[1,2,3],"prop3":"invalid type"}',
TestComponent::ModelWithAdditionalPropertiesOfPrimitiveArrayType, false,
'{"email":"test","prop1":[1,2,3],"prop2":[1,2,3]}'],

['{"email":"test","prop1":{"inner_prop1":1,"inner_prop2":2},"prop2":{"inner_prop1":1,"inner_prop2":2},"prop3":"invalid type"}',
TestComponent::ModelWithAdditionalPropertiesOfPrimitiveDictType, false,
'{"email":"test","prop1":{"inner_prop1":1,"inner_prop2":2},"prop2":{"inner_prop1":1,"inner_prop2":2}}'],

['{"email":"test","prop1":{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"},"prop3":"invalid type"}',
TestComponent::ModelWithAdditionalPropertiesOfModelType, false,
'{"email":"test","prop1":{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"}}'],

['{"email":"test","prop":[{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"},{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"}]}',
TestComponent::ModelWithAdditionalPropertiesOfModelArrayType, false,
'{"email":"test","prop":[{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"},{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"}]}'],

['{"email":"test","prop":{"inner prop 1":{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"},"inner prop 2":{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"}}}',
TestComponent::ModelWithAdditionalPropertiesOfModelDictType, false,
'{"email":"test","prop":{"inner prop 1":{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"},"inner prop 2":{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"}}}'],

['{"email":"test","prop":{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"}}',
TestComponent::ModelWithAdditionalPropertiesOfTypeCombinatorPrimitiveType, false,
'{"email":"test","prop":{"startsAt":"15:30","endsAt":"20:30","offerDinner":false,"sessionType":"Evening"}}'],
['{"email":"test","prop":"100.65"}',
TestComponent::ModelWithAdditionalPropertiesOfTypeCombinatorPrimitiveType, false,
'{"email":"test","prop":"100.65"}'],
['{"email":"test","prop":"some string"}',
TestComponent::ModelWithAdditionalPropertiesOfTypeCombinatorPrimitiveType, false,
'{"email":"test","prop":"some string"}'],
['{"email":"test","prop":100.65}',
TestComponent::ModelWithAdditionalPropertiesOfTypeCombinatorPrimitiveType, false,
'{"email":"test"}'],
]

# Iterate through each test case
test_cases.each do |input_json_value, model_class, as_dict, expected_value|
deserialized_value = model_class.from_hash(
ApiHelper.json_deserialize(input_json_value, as_dict)
)

serialized_value = ApiHelper.json_serialize(deserialized_value)

# Assert that the serialized value matches the expected value
assert_equal expected_value, serialized_value
end
end

def test_valid_type_hash
assert ApiHelper.valid_type?(
{
Expand Down Expand Up @@ -602,6 +707,81 @@ def test_deserialize_union_type
]
assert_equal(expected, actual, 'Actual did not match the expected.')
end
end



def test_get_additional_properties_success
test_cases = [
{ dictionary: {}, expected_result: {}, unboxing_func: Proc.new { |x| Integer(x) }},
{ dictionary: { "a" => 1, "b" => 2 }, expected_result: { "a" => 1, "b" => 2 }, unboxing_func: Proc.new { |x| Integer(x) }},
{ dictionary: { "a" => "1", "b" => "2" }, expected_result: { "a" => "1", "b" => "2" }, unboxing_func: Proc.new { |x| x.to_s }},
{ dictionary: { "a" => "Test 1", "b" => "Test 2" }, expected_result: {}, unboxing_func: Proc.new { |x| Integer(x) }},
{ dictionary: { "a" => [1, 2], "b" => [3, 4] }, expected_result: { "a" => [1, 2], "b" => [3, 4] }, unboxing_func: Proc.new { |x| Integer(x) }, is_array: true},
{ dictionary: { "a" => { "x" => 1, "y" => 2 }, "b" => { "x" => 3, "y" => 4 } }, expected_result: { "a" => { "x" => 1, "y" => 2 }, "b" => { "x" => 3, "y" => 4 } }, unboxing_func: Proc.new { |x| Integer(x) }, is_array: false, is_dict: true}
]

test_cases.each do |case_data|
actual_result = ApiHelper.get_additional_properties(case_data[:dictionary], case_data[:unboxing_func], is_array: case_data[:is_array], is_dict: case_data[:is_dict])
assert_equal(case_data[:expected_result], actual_result)
end
end

def test_get_additional_properties_exception
test_cases = [
{ dictionary: { "a" => nil }, unboxing_func: Proc.new { |x| Integer(x) } },
{ dictionary: { "a" => Proc.new { |x| x } }, unboxing_func: Proc.new { |x| Integer(x)}}
]

test_cases.each do |case_data|
actual_result = ApiHelper.get_additional_properties(case_data[:dictionary], case_data[:unboxing_func])
expected_result = {}
assert_equal(expected_result, actual_result)
end
end

def test_apply_unboxing_function
test_cases = [
# Test case 1: Simple object
{ value: 5, unboxing_func: Proc.new { |x| x * 2 }, is_array: false, is_dict: false,
is_array_of_map: false, is_map_of_array: false, dimension_count: 0, expected: 10 },

# Test case 2: Array
{ value: [1, 2, 3], unboxing_func: Proc.new { |x| x * 2 }, is_array: true, is_dict: false,
is_array_of_map: false, is_map_of_array: false, dimension_count: 0, expected: [2, 4, 6] },

# Test case 3: Dictionary
{ value: { "a" => 1, "b" => 2 }, unboxing_func: Proc.new { |x| x * 2 }, is_array: false,
is_dict: true, is_array_of_map: false, is_map_of_array: false, dimension_count: 0, expected: { "a" => 2, "b" => 4 } },

# Test case 4: Array of maps
{ value: [{ "a" => 1 }, { "b" => 2 }], unboxing_func: Proc.new { |x| x * 2 }, is_array: true,
is_dict: false, is_array_of_map: true, is_map_of_array: false, dimension_count: 0, expected: [{ "a" => 2 }, { "b" => 4 }] },

# Test case 5: Map of arrays
{ value: { "a" => [1, 2], "b" => [3, 4] }, unboxing_func: Proc.new { |x| x * 2 }, is_array: false,
is_dict: true, is_array_of_map: false, is_map_of_array: true, dimension_count: 0, expected: { "a" => [2, 4], "b" => [6, 8] } },

# Test case 6: Multi-dimensional array
{ value: [[1], [2, 3], [4]], unboxing_func: Proc.new { |x| x * 2 }, is_array: true,
is_dict: false, is_array_of_map: false, is_map_of_array: false, dimension_count: 2, expected: [[2], [4, 6], [8]] },

# Test case 7: Array of arrays
{ value: [[1, 2], [3, 4]], unboxing_func: Proc.new { |x| x * 2 }, is_array: true,
is_dict: false, is_array_of_map: false, is_map_of_array: false, dimension_count: 2, expected: [[2, 4], [6, 8]] },

# Test case 8: Array of arrays of arrays
{ value: [[[1, 2], [3, 4]], [[5, 6], [7, 8]]], unboxing_func: Proc.new { |x| x * 2 }, is_array: true,
is_dict: false, is_array_of_map: false, is_map_of_array: false, dimension_count: 3, expected: [[[2, 4], [6, 8]], [[10, 12], [14, 16]]] }
]

test_cases.each do |test_case|
result = ApiHelper.apply_unboxing_function(test_case[:value],
test_case[:unboxing_func],
is_array: test_case[:is_array],
is_dict: test_case[:is_dict],
is_array_of_map: test_case[:is_array_of_map],
is_map_of_array: test_case[:is_map_of_array],
dimension_count: test_case[:dimension_count])

assert_equal test_case[:expected], result
end
end
end
Loading

0 comments on commit 606255f

Please sign in to comment.