From 3c2514c5bb2d97e28c4950c190c1a4b6359df771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Thu, 30 Sep 2021 09:11:26 +0200 Subject: [PATCH 1/3] Add CustomType module, allowing users to implement serdes for types Bumps version to 0.8.0 --- README.md | 4 ++++ lib/class_kit.rb | 1 + lib/class_kit/custom_type.rb | 26 ++++++++++++++++++++ lib/class_kit/helper.rb | 12 ++++++++++ lib/class_kit/value_helper.rb | 2 ++ lib/class_kit/version.rb | 2 +- spec/class_kit/helper_spec.rb | 37 +++++++++++++++++++++++++++++ spec/class_kit/test_objects.rb | 21 ++++++++++++++++ spec/class_kit/value_helper_spec.rb | 9 +++++++ 9 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 lib/class_kit/custom_type.rb diff --git a/README.md b/README.md index f5111ea..e6eb7f0 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,10 @@ Attributes don't require the `type:` argument to be specified for attributes whe The `attr_accessor_type` method allows attributes of type `Array` to be specified, these attributes can also specify the `collection_type:` argument to specify what the type the elements of the Array will be. +#### Custom types + +ClassKit offers the CustomType module, which can be included to define attributes as custom types, while controlling how the serialisation and deserialisation of its values are handled via the implementation of the included methods. + #### Additional Arguments The below are additional arguments that can be specified when using the `attr_accessor_type` method. diff --git a/lib/class_kit.rb b/lib/class_kit.rb index e225176..df42c15 100644 --- a/lib/class_kit.rb +++ b/lib/class_kit.rb @@ -1,6 +1,7 @@ require_relative 'class_kit/class_methods' require_relative 'class_kit/exceptions' require_relative 'class_kit/attribute_helper' +require_relative 'class_kit/custom_type' require_relative 'class_kit/value_helper' require_relative 'class_kit/helper' require_relative 'class_kit/version' diff --git a/lib/class_kit/custom_type.rb b/lib/class_kit/custom_type.rb new file mode 100644 index 0000000..c9c1e49 --- /dev/null +++ b/lib/class_kit/custom_type.rb @@ -0,0 +1,26 @@ +module ClassKit + module CustomType + # :nocov: + + def self.included(base) + base.extend(CustomType) + end + + # This method must return the parsed value when +from_hash+ is called + def self.parse_from_hash(_value) + raise NotImplementedError + end + + # This method must return the parsed value when the class' attribute is assigned a value + def self.parse_assign(_value) + raise NotImplementedError + end + + # This method must return the value that will be serialised for the attribute + def to_hash_value + raise NotImplementedError + end + + # :nocov: + end +end diff --git a/lib/class_kit/helper.rb b/lib/class_kit/helper.rb index 5b1f561..fae40bf 100644 --- a/lib/class_kit/helper.rb +++ b/lib/class_kit/helper.rb @@ -11,6 +11,10 @@ def is_class_kit?(klass) klass.is_a?(ClassKit) end + def is_class_kit_custom_type?(klass) + !klass.nil? && klass != :bool && klass.include?(ClassKit::CustomType) + end + def validate_class_kit(klass) is_class_kit?(klass) || raise(ClassKit::Exceptions::InvalidClassError, "Class: #{klass} does not implement ClassKit.") @@ -32,10 +36,14 @@ def to_hash(object, use_alias = false) if value != nil hash[key] = if is_class_kit?(type) to_hash(value, use_alias) + elsif is_class_kit_custom_type?(type) + value.to_hash_value elsif type == Array value.map do |i| if is_class_kit?(i.class) to_hash(i, use_alias) + elsif is_class_kit_custom_type?(i.class) + i.to_hash_value else i end @@ -67,6 +75,8 @@ def from_hash(hash:, klass:, use_alias: false) value = if is_class_kit?(type) from_hash(hash: hash[key], klass: type, use_alias: use_alias) + elsif is_class_kit_custom_type?(type) + type.parse_from_hash(hash[key]) elsif type == Array hash[key].map do |array_element| if attribute[:collection_type].nil? @@ -74,6 +84,8 @@ def from_hash(hash:, klass:, use_alias: false) else if is_class_kit?(attribute[:collection_type]) from_hash(hash: array_element, klass: attribute[:collection_type], use_alias: use_alias) + elsif is_class_kit_custom_type?(attribute[:collection_type]) + attribute[:collection_type].parse_from_hash(array_element) else @value_helper.parse(type: attribute[:collection_type], value: array_element) end diff --git a/lib/class_kit/value_helper.rb b/lib/class_kit/value_helper.rb index 9e10f7a..64eb381 100644 --- a/lib/class_kit/value_helper.rb +++ b/lib/class_kit/value_helper.rb @@ -62,6 +62,8 @@ def parse(type:, value:) elsif type == Array raise 'Unable to parse Array' unless value.is_a?(Array) value + elsif type.include?(ClassKit::CustomType) + type.parse_assign(value) else raise 'Unable to parse' end diff --git a/lib/class_kit/version.rb b/lib/class_kit/version.rb index 33a007d..7fa210e 100644 --- a/lib/class_kit/version.rb +++ b/lib/class_kit/version.rb @@ -1,5 +1,5 @@ # Namespace module ClassKit # :nodoc: - VERSION = '0.7.1' + VERSION = '0.8.0' end diff --git a/spec/class_kit/helper_spec.rb b/spec/class_kit/helper_spec.rb index 56cf85a..5611da1 100644 --- a/spec/class_kit/helper_spec.rb +++ b/spec/class_kit/helper_spec.rb @@ -13,6 +13,19 @@ end end + describe '#is_class_kit_custom_type?' do + context 'when the argument includes ClassKit::CustomType' do + it 'should return true' do + expect(subject.is_class_kit_custom_type?(TestCustomType)).to be true + end + end + context 'when the argument does NOT include ClassKit::CustomType' do + it 'should return false' do + expect(subject.is_class_kit_custom_type?(String)).to be false + end + end + end + describe '#to_hash' do context 'when a valid class is specified' do let(:entity) do @@ -84,6 +97,18 @@ expect(hash[:address_collection][1][:postcode]).to eq(address2.postcode) end end + context 'when a valid class is specified with custom types' do + let(:entity) do + TestEntityWithCustomType.new.tap do |e| + e.text = 'line1' + end + end + it 'should return a hash' do + hash = subject.to_hash(entity) + expect(hash).to be_a(Hash) + expect(hash[:text]).to eq(entity.text) + end + end context 'when an invalid class is specified' do let(:entity) do InvalidClass.new.tap do |e| @@ -189,6 +214,18 @@ expect(entity.country).to eq hash[:c] end end + context 'when a valid hash is specified with custom types' do + let(:hash) do + { + text: 'line1' + } + end + it 'should convert the hash' do + entity = subject.from_hash(hash: hash, klass: TestEntityWithCustomType) + expect(entity).not_to be_nil + expect(entity.text).to eq TestCustomType.parse_from_hash(hash[:text]) + end + end end describe '#to_json' do diff --git a/spec/class_kit/test_objects.rb b/spec/class_kit/test_objects.rb index 56eb053..23f0971 100644 --- a/spec/class_kit/test_objects.rb +++ b/spec/class_kit/test_objects.rb @@ -14,6 +14,27 @@ class TestAddressWithAlias attr_accessor_type :country, type: String, default: 'United Kingdom', alias_name: :c end +class TestCustomType < String + include ClassKit::CustomType + + def self.parse_from_hash(value) + self.new("#{value}_from_hash") + end + + def self.parse_assign(value) + self.new("#{value}_from_assign") + end + + def to_hash_value + self + end +end + +class TestEntityWithCustomType + extend ClassKit + attr_accessor_type :text, type: TestCustomType +end + class TestEntity extend ClassKit diff --git a/spec/class_kit/value_helper_spec.rb b/spec/class_kit/value_helper_spec.rb index 0bd009b..f8328ff 100644 --- a/spec/class_kit/value_helper_spec.rb +++ b/spec/class_kit/value_helper_spec.rb @@ -334,6 +334,15 @@ end end end + context 'when type: is CustomType' do + context 'and value is a valid CustomType' do + it 'should parse the value' do + value = subject.parse(type: TestCustomType, value: TestCustomType.new('foo')) + expect(value).to be_a(TestCustomType) + expect(value).to eq(TestCustomType.parse_assign('foo')) + end + end + end end describe '#instance' do From 80fca2768ba71d53777fdb9f6f09f786953e5b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Thu, 7 Oct 2021 13:39:41 +0200 Subject: [PATCH 2/3] Remove broken code The methods DateTime.at and Date.at do not exist. --- lib/class_kit/value_helper.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/class_kit/value_helper.rb b/lib/class_kit/value_helper.rb index 64eb381..56f0f6d 100644 --- a/lib/class_kit/value_helper.rb +++ b/lib/class_kit/value_helper.rb @@ -17,16 +17,12 @@ def parse(type:, value:) elsif type == Date if value.is_a?(Date) value - elsif value.is_a?(Integer) - Date.at(value) else Date.parse(value) end elsif type == DateTime if value.is_a?(DateTime) value - elsif value.is_a?(Integer) - DateTime.at(value) else DateTime.parse(value) end From 102591b150419a4b3fccdb60919964b875a7914a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Thu, 7 Oct 2021 13:19:49 +0200 Subject: [PATCH 3/3] Add specs to increase coverage --- lib/class_kit/custom_type.rb | 4 -- spec/class_kit/attribute_helper_spec.rb | 5 ++- spec/class_kit/custom_type_spec.rb | 10 +++++ spec/class_kit/helper_spec.rb | 54 ++++++++++++++++++++++++- spec/class_kit/test_objects.rb | 11 +++++ spec/class_kit/value_helper_spec.rb | 9 ++++- spec/spec_helper.rb | 1 + 7 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 spec/class_kit/custom_type_spec.rb diff --git a/lib/class_kit/custom_type.rb b/lib/class_kit/custom_type.rb index c9c1e49..9c01b3c 100644 --- a/lib/class_kit/custom_type.rb +++ b/lib/class_kit/custom_type.rb @@ -1,7 +1,5 @@ module ClassKit module CustomType - # :nocov: - def self.included(base) base.extend(CustomType) end @@ -20,7 +18,5 @@ def self.parse_assign(_value) def to_hash_value raise NotImplementedError end - - # :nocov: end end diff --git a/spec/class_kit/attribute_helper_spec.rb b/spec/class_kit/attribute_helper_spec.rb index 7028c9e..eed8c1b 100644 --- a/spec/class_kit/attribute_helper_spec.rb +++ b/spec/class_kit/attribute_helper_spec.rb @@ -29,6 +29,9 @@ expect(attribute[:name]).to eq(:child1) end end + it 'will raise an Error if the attribute cannot be found' do + expect { subject.get_attribute(klass: TestChild, name: :doesnotexist) }.to raise_error(ClassKit::Exceptions::AttributeNotFoundError) + end end describe '#get_attribute_type' do context 'when a class has NO polymorphism chain' do @@ -44,4 +47,4 @@ end end end -end \ No newline at end of file +end diff --git a/spec/class_kit/custom_type_spec.rb b/spec/class_kit/custom_type_spec.rb new file mode 100644 index 0000000..90bc811 --- /dev/null +++ b/spec/class_kit/custom_type_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' +RSpec.describe ClassKit::CustomType do + describe 'the module methods' do + it 'should raise a NotImplementedError when called directly' do + expect { subject.parse_assign(nil) }.to raise_error(NotImplementedError) + expect { subject.parse_from_hash(nil) }.to raise_error(NotImplementedError) + expect { Class.new.extend(subject).to_hash_value }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/class_kit/helper_spec.rb b/spec/class_kit/helper_spec.rb index 5611da1..eb9a5d5 100644 --- a/spec/class_kit/helper_spec.rb +++ b/spec/class_kit/helper_spec.rb @@ -109,6 +109,21 @@ expect(hash[:text]).to eq(entity.text) end end + context 'when a valid class is specified with custom types in its array attributes' do + let(:custom_type_attr) { TestCustomType.new('test') } + let(:entity) do + TestEntityWithArrayOfCustomTypes.new.tap do |e| + e.custom_type_collection << custom_type_attr + end + end + it 'should return a hash' do + hash = subject.to_hash(entity) + expect(hash).to be_a(Hash) + expect(hash[:custom_type_collection]).to be_a(Array) + expect(hash[:custom_type_collection].length).to eq 1 + expect(hash[:custom_type_collection][0]).to eq(custom_type_attr) + end + end context 'when an invalid class is specified' do let(:entity) do InvalidClass.new.tap do |e| @@ -226,6 +241,36 @@ expect(entity.text).to eq TestCustomType.parse_from_hash(hash[:text]) end end + context 'when a valid hash is specified with an array of custom types' do + let(:hash) do + { + custom_type_collection: ['test', 'test2'] + } + end + it 'should convert the hash' do + entity = subject.from_hash(hash: hash, klass: TestEntityWithArrayOfCustomTypes) + expect(entity).not_to be_nil + expect(entity.custom_type_collection).to be_a(Array) + expect(entity.custom_type_collection.length).to eq(2) + expect(entity.custom_type_collection[0]).to eq(TestCustomType.parse_from_hash(hash[:custom_type_collection][0])) + expect(entity.custom_type_collection[1]).to eq(TestCustomType.parse_from_hash(hash[:custom_type_collection][1])) + end + end + context 'when a valid hash is specified with an array with no collection_type' do + let(:hash) do + { + undefined_type_collection: ['test', 100] + } + end + it 'should convert the hash' do + entity = subject.from_hash(hash: hash, klass: TestEntityWithArrayWithoutType) + expect(entity).not_to be_nil + expect(entity.undefined_type_collection).to be_a(Array) + expect(entity.undefined_type_collection.length).to eq(2) + expect(entity.undefined_type_collection[0]).to eq(hash[:undefined_type_collection][0]) + expect(entity.undefined_type_collection[1]).to eq(hash[:undefined_type_collection][1]) + end + end end describe '#to_json' do @@ -250,6 +295,7 @@ e.address = address e.address_collection << address e.address_collection << address + e.integer_collection << 22 end end let(:hash) do @@ -276,7 +322,8 @@ line2: 'street 2', postcode: 'ne3 5rt' } - ] + ], + integer_collection: [22] } end it 'should convert the class to json' do @@ -286,6 +333,7 @@ expect(result_hash['address']['post_code']).to eq(hash[:address][:post_code]) expect(result_hash['address_collection'].length).to eq(2) expect(result_hash['address_collection'][0]['post_code']).to eq hash[:address_collection][0][:post_code] + expect(result_hash['integer_collection'][0]).to eq hash[:integer_collection][0] end context 'when entity specifies aliases' do let(:address) do @@ -335,7 +383,8 @@ line2: 'b street 2', postcode: 'b ne3 5rt' } - ] + ], + integer_collection: [22] } end let(:json) { JSON.dump(hash) } @@ -362,6 +411,7 @@ expect(entity.address_collection[1].line1).to eq(hash[:address_collection][1][:line1]) expect(entity.address_collection[1].line2).to eq(hash[:address_collection][1][:line2]) expect(entity.address_collection[1].postcode).to eq(hash[:address_collection][1][:postcode]) + expect(entity.integer_collection[0]).to eq(hash[:integer_collection][0]) end context 'when entity has json aliases' do let(:hash) do diff --git a/spec/class_kit/test_objects.rb b/spec/class_kit/test_objects.rb index 23f0971..58fd5e2 100644 --- a/spec/class_kit/test_objects.rb +++ b/spec/class_kit/test_objects.rb @@ -35,6 +35,16 @@ class TestEntityWithCustomType attr_accessor_type :text, type: TestCustomType end +class TestEntityWithArrayOfCustomTypes + extend ClassKit + attr_accessor_type :custom_type_collection, type: Array, collection_type: TestCustomType, allow_nil: false, auto_init: true +end + +class TestEntityWithArrayWithoutType + extend ClassKit + attr_accessor_type :undefined_type_collection, type: Array, collection_type: nil +end + class TestEntity extend ClassKit @@ -55,6 +65,7 @@ class TestEntity attr_accessor_type :address_auto, type: TestAddress, allow_nil: false, auto_init: true attr_accessor_type :address_collection, type: Array, collection_type: TestAddress, allow_nil: false, auto_init: true + attr_accessor_type :integer_collection, type: Array, collection_type: Integer, allow_nil: false, auto_init: true end class InvalidClass diff --git a/spec/class_kit/value_helper_spec.rb b/spec/class_kit/value_helper_spec.rb index f8328ff..82c5048 100644 --- a/spec/class_kit/value_helper_spec.rb +++ b/spec/class_kit/value_helper_spec.rb @@ -247,13 +247,20 @@ expect(value).to eq BigDecimal('-0.005') end end - context 'BigDecimal' do + context 'Float' do it 'should parse the value' do value = subject.parse(type: BigDecimal, value: 0.005) expect(value).to be_a(BigDecimal) expect(value).to eq BigDecimal.new('0.005') end end + context 'BigDecimal' do + it 'should parse the value' do + value = subject.parse(type: BigDecimal, value: 0.005.to_d) + expect(value).to be_a(BigDecimal) + expect(value).to eq BigDecimal.new('0.005') + end + end end context 'and value is NOT valid' do it 'should raise a invalid parse value error' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1786c43..2d26408 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,7 @@ require 'pry' require 'hash_kit' require 'class_kit' +require 'bigdecimal/util' require_relative 'class_kit/test_objects' RSpec.configure do |config|