Skip to content

Commit

Permalink
easy type conversion for classes
Browse files Browse the repository at this point in the history
  • Loading branch information
subvertallchris committed Jan 26, 2015
1 parent 530d83e commit f8689b5
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 31 deletions.
1 change: 1 addition & 0 deletions lib/neo4j.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
require 'neo4j/shared/validations'
require 'neo4j/shared/identity'
require 'neo4j/shared/serialized_properties'
require 'neo4j/shared/typecaster'
require 'neo4j/shared'

require 'neo4j/active_rel/callbacks'
Expand Down
21 changes: 18 additions & 3 deletions lib/neo4j/shared/property.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def instantiate_object(field, values_with_empty_parameters)
klass ? klass.new(*values) : values
end

def magic_typecast_properties
self.class.magic_typecast_properties
end

module ClassMethods
# Defines a property on the class
#
Expand Down Expand Up @@ -187,6 +191,10 @@ def attribute!(name, options = {})
end
end

def magic_typecast_properties
@magic_typecast_properties ||= {}
end

private

def constraint_or_index(name, options)
Expand All @@ -201,19 +209,26 @@ def constraint_or_index(name, options)
end

def check_illegal_prop(name)
if ILLEGAL_PROPS.include?(name.to_s)
fail IllegalPropertyError, "#{name} is an illegal property"
end
fail IllegalPropertyError, "#{name} is an illegal property" if ILLEGAL_PROPS.include?(name.to_s)
end

# Tweaks properties
def magic_properties(name, options)
magic_typecast(name, options)
options[:type] ||= DateTime if name.to_sym == :created_at || name.to_sym == :updated_at

# ActiveAttr does not handle "Time", Rails and Neo4j.rb 2.3 did
# Convert it to DateTime in the interest of consistency
options[:type] = DateTime if options[:type] == Time
end

def magic_typecast(name, options)
typecaster = Neo4j::Shared::TypeConverters.typecaster_for(options[:type])
return unless typecaster && typecaster.respond_to?(:primitive_type)
magic_typecast_properties[name] = options[:type]
options[:type] = typecaster.primitive_type
options[:typecaster] = typecaster
end
end
end
end
65 changes: 37 additions & 28 deletions lib/neo4j/shared/type_converters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ def convert_type
end

def to_db(value)
return nil if value.nil?
Time.utc(value.year, value.month, value.day).to_i
end

def to_ruby(value)
return nil if value.nil?
Time.at(value).utc.to_date
end
end
Expand All @@ -29,7 +27,6 @@ def convert_type
# Converts the given DateTime (UTC) value to an Integer.
# DateTime values are automatically converted to UTC.
def to_db(value)
return nil if value.nil?
value = value.new_offset(0) if value.respond_to?(:new_offset)
if value.class == Date
Time.utc(value.year, value.month, value.day, 0, 0, 0).to_i
Expand All @@ -38,13 +35,13 @@ def to_db(value)
end
end

DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %z'
def to_ruby(value)
return nil if value.nil?
t = case value
when Integer
Time.at(value).utc
when String
DateTime.strptime(value, '%Y-%m-%d %H:%M:%S %z')
DateTime.strptime(value, DATETIME_FORMAT)
else
fail ArgumentError, "Invalid value type for DateType property: #{value.inspect}"
end
Expand All @@ -63,7 +60,6 @@ def convert_type
# Converts the given DateTime (UTC) value to an Integer.
# Only utc times are supported !
def to_db(value)
return nil if value.nil?
if value.class == Date
Time.utc(value.year, value.month, value.day, 0, 0, 0).to_i
else
Expand All @@ -72,7 +68,6 @@ def to_db(value)
end

def to_ruby(value)
return nil if value.nil?
Time.at(value).utc
end
end
Expand All @@ -86,12 +81,10 @@ def convert_type
end

def to_db(value)
return nil if value.nil?
Psych.dump(value)
end

def to_ruby(value)
return nil if value.nil?
Psych.load(value)
end
end
Expand All @@ -105,39 +98,53 @@ def convert_type
end

def to_db(value)
return nil if value.nil?
value.to_json
end

def to_ruby(value)
return nil if value.nil?
JSON.parse(value, quirks_mode: true)
end
end
end



def convert_properties_to(medium, properties)
# Perform type conversion
serialize = self.respond_to?(:serialized_properties) ? self.serialized_properties : {}
converter = medium == :ruby ? :to_ruby : :to_db

properties.each_with_object({}) do |(attr, value), new_attributes|
next new_attributes if skip_conversion?(attr, value)
primitive = primitive_type(attr.to_sym)
new_attributes[attr] = converted_property(primitive, value, converter)
end
end

properties.each_with_object({}) do |key_value_pair, new_attributes|
attr, value = key_value_pair
private

# skip "secret" undeclared attributes such as uuid
next new_attributes unless self.class.attributes[attr]
def converted_property(type, value, converter)
TypeConverters.converters[type].nil? ? value : TypeConverters.send(converter, value, type)
end

type = serialize.key?(attr.to_sym) ? serialize[attr.to_sym] : self.class._attribute_type(attr)
new_attributes[attr] = if TypeConverters.converters[type].nil?
value
else
TypeConverters.send "to_#{medium}", value, type
end
# If the attribute is to be typecast using a custom converter, which converter should it use? If no, returns the type to find a native serializer.
def primitive_type(attr)
case
when serialized_properties.key?(attr)
serialized_properties[attr]
when magic_typecast_properties.key?(attr)
self.class.magic_typecast_properties[attr]
else
self.class._attribute_type(attr)
end
end

# Moves on if the property is either undeclared (UUID or just not included in the model) or nil and unchanged
def skip_conversion?(attr, value)
!self.class.attributes[attr] || (value.nil? && !changed_attributes.key?(attr))
end

class << self
def typecaster_for(primitive_type)
converters.key?(primitive_type) ? converters[primitive_type] : nil
end

# Converts the value to ruby from a Neo4j database value if there is a converter for given type
def to_ruby(value, type = nil)
found_converter = converters[type]
Expand All @@ -150,13 +157,15 @@ def to_db(value, type = nil)
found_converter ? found_converter.to_db(value) : value
end

def register_converter(converter)
converters[converter.convert_type] = converter
end

def converters
@converters ||= begin
Neo4j::Shared::TypeConverters.constants.each_with_object({}) do |constant_name, result|
constant = Neo4j::Shared::TypeConverters.const_get(constant_name)
if constant.respond_to?(:convert_type)
result[constant.convert_type] = constant
end
result[constant.convert_type] = constant if constant.respond_to?(:convert_type)
end
end
end
Expand Down
9 changes: 9 additions & 0 deletions lib/neo4j/shared/typecaster.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Neo4j
module Shared
module Typecaster
def self.included(other)
Neo4j::Shared::TypeConverters.register_converter(other)
end
end
end
end
44 changes: 44 additions & 0 deletions spec/e2e/typecasting_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'spec_helper'

describe 'custom type conversion' do
class RangeConverter
class << self
def primitive_type
String
end

def convert_type
Range
end

def to_db(value)
value.to_s
end

def to_ruby(value)
ends = value.to_s.split('..').map { |d| Integer(d) }
ends[0]..ends[1]
end
alias_method :call, :to_ruby
end

include Neo4j::Shared::Typecaster
end

class RangeConvertPerson
include Neo4j::ActiveNode
property :my_range, type: Range
end

it 'registers the typecaster' do
expect(Neo4j::Shared::TypeConverters.converters).to have_key(Range)
end

it 'uses the custom typecaster' do
r = RangeConvertPerson.new
r.my_range = 1..30
r.save
r.reload
expect(r.my_range).to be_a(Range)
end
end
3 changes: 3 additions & 0 deletions spec/unit/persistance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

it 'creates a new node if not persisted before' do
o = clazz.new(name: 'kalle', age: '42')
o.stub(:serialized_properties).and_return({})
clazz.stub(:cached_class?).and_return(false)
clazz.should_receive(:neo4j_session).and_return(session)
clazz.should_receive(:mapped_label_names).and_return(:MyClass)
Expand All @@ -54,6 +55,7 @@
it 'updates node if already persisted before if an attribute was changed' do
o = clazz.new
o.name = 'sune'
o.stub(:serialized_properties).and_return({})
o.stub(:_persisted_obj).and_return(node)
expect(node).to receive(:update_props).and_return(name: 'sune')
o.save
Expand All @@ -67,6 +69,7 @@
o = clazz.new

o.stub(:props).and_return(start_props)
o.stub(:serialized_properties).and_return({})
o.class.stub(:name).and_return('MyClass') # set_classname looks for this
clazz.stub(:neo4j_session).and_return(session)

Expand Down
3 changes: 3 additions & 0 deletions spec/unit/validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def self.model_name
it 'creates a new node if not persisted before' do
o = clazz.new(name: 'kalle', age: '42')
o.stub(:_persisted_obj).and_return(nil)
o.stub(:serialized_properties).and_return({})
o.serialized_properties
clazz.stub(:cached_class?).and_return(false)
clazz.should_receive(:neo4j_session).and_return(session)
node.should_receive(:props).and_return(name: 'kalle2', age: '43')
Expand All @@ -49,6 +51,7 @@ def self.model_name
o = clazz.new
o.name = 'sune'
o.stub(:_persisted_obj).and_return(node)
o.stub(:serialized_properties).and_return({})
node.should_receive(:update_props).and_return(name: 'sune')
o.save.should be true
end
Expand Down

0 comments on commit f8689b5

Please sign in to comment.