Skip to content

Commit

Permalink
Merge pull request #22 from Sage/feat_add_custom_types
Browse files Browse the repository at this point in the history
Add CustomType module, allowing users to implement serdes for types
  • Loading branch information
guille-sage authored Oct 7, 2021
2 parents 838d548 + 102591b commit 86db592
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 9 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions lib/class_kit.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
22 changes: 22 additions & 0 deletions lib/class_kit/custom_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module ClassKit
module CustomType
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
end
end
12 changes: 12 additions & 0 deletions lib/class_kit/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -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
Expand Down Expand Up @@ -67,13 +75,17 @@ 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?
array_element
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
Expand Down
6 changes: 2 additions & 4 deletions lib/class_kit/value_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,6 +58,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
Expand Down
2 changes: 1 addition & 1 deletion lib/class_kit/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Namespace
module ClassKit
# :nodoc:
VERSION = '0.7.1'
VERSION = '0.8.0'
end
5 changes: 4 additions & 1 deletion spec/class_kit/attribute_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,4 +47,4 @@
end
end
end
end
end
10 changes: 10 additions & 0 deletions spec/class_kit/custom_type_spec.rb
Original file line number Diff line number Diff line change
@@ -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
91 changes: 89 additions & 2 deletions spec/class_kit/helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,6 +97,33 @@
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 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|
Expand Down Expand Up @@ -189,6 +229,48 @@
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
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
Expand All @@ -213,6 +295,7 @@
e.address = address
e.address_collection << address
e.address_collection << address
e.integer_collection << 22
end
end
let(:hash) do
Expand All @@ -239,7 +322,8 @@
line2: 'street 2',
postcode: 'ne3 5rt'
}
]
],
integer_collection: [22]
}
end
it 'should convert the class to json' do
Expand All @@ -249,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
Expand Down Expand Up @@ -298,7 +383,8 @@
line2: 'b street 2',
postcode: 'b ne3 5rt'
}
]
],
integer_collection: [22]
}
end
let(:json) { JSON.dump(hash) }
Expand All @@ -325,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
Expand Down
32 changes: 32 additions & 0 deletions spec/class_kit/test_objects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,37 @@ 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 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

Expand All @@ -34,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
Expand Down
18 changes: 17 additions & 1 deletion spec/class_kit/value_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -334,6 +341,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
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down

0 comments on commit 86db592

Please sign in to comment.