# This module handles the getting, setting and updating of attributes or properties # in a Railsy way. This typically means not writing anything to the DB until the # object is saved (after validation). # # Externally, when we talk about properties (e.g. #property?, #property_names, #properties), # we mean all of the stored properties for this object include the 'hidden' props # with underscores at the beginning such as _neo_id and _classname. When we talk # about attributes, we mean all the properties apart from those hidden ones. module Neo4j module Rails module Attributes extend ActiveSupport::Concern included do include ActiveModel::Dirty # track changes to attributes include ActiveModel::MassAssignmentSecurity # handle attribute hash assignment class_inheritable_hash :attribute_defaults self.attribute_defaults ||= {} # save the original [] and []= to use as read/write to Neo4j alias_method :read_attribute, :[] alias_method :write_attribute, :[]= # wrap the read/write in type conversion alias_method_chain :read_local_property, :type_conversion alias_method_chain :write_local_property, :type_conversion # whenever we refer to [] or []=. use our local properties store alias_method :[], :read_local_property alias_method :[]=, :write_local_property end # The behaviour of []= changes with a Rails Model, where nothing gets written # to Neo4j until the object is saved, during which time all the validations # and callbacks are run to ensure correctness def write_local_property(key, value) key_s = key.to_s if @properties[key_s] != value attribute_will_change!(key_s) @properties[key_s] = value end value end # Returns the locally stored value for the key or retrieves the value from # the DB if we don't have one def read_local_property(key) key = key.to_s if @properties.has_key?(key) @properties[key] else @properties[key] = (persisted? && _java_node.has_property?(key)) ? read_attribute(key) : attribute_defaults[key] end end # Mass-assign attributes. Stops any protected attributes from being assigned. def attributes=(attributes, guard_protected_attributes = true) attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes attributes.each { |k, v| respond_to?("#{k}=") ? send("#{k}=", v) : self[k] = v } end # Tracks the current changes and clears the changed attributes hash. Called # after saving the object. def clear_changes @previously_changed = changes @changed_attributes.clear end # Return the properties from the Neo4j Node, merged with those that haven't # yet been saved def props ret = {} property_names.each do |property_name| ret[property_name] = respond_to?(property_name) ? send(property_name) : send(:[], property_name) end ret end # Return all the attributes for this model as a hash attr => value. Doesn't # include properties that start with _. def attributes ret = {} attribute_names.each do |attribute_name| ret[attribute_name] = respond_to?(attribute_name) ? send(attribute_name) : send(:[], attribute_name) end ret end # Known properties are either in the @properties, the declared # attributes or the property keys for the persisted node. def property_names keys = @properties.keys + self.class._decl_props.keys.map { |k| k.to_s } keys += _java_node.property_keys.to_a if persisted? keys.flatten.uniq end # Known attributes are either in the @properties, the declared # attributes or the property keys for the persisted node. Any attributes # that start with _ are rejected def attribute_names property_names.reject { |property_name| property_name[0] == ?_ } end # Known properties are either in the @properties, the declared # properties or the property keys for the persisted node def property?(name) @properties.keys.include?(name) || self.class._decl_props.map { |k| k.to_s }.include?(name) || begin super rescue org.neo4j.graphdb.NotFoundException set_deleted_properties nil end end # Return true if method_name is the name of an appropriate attribute # method def attribute?(name) name[0] != ?_ && property?(name) end # To get ActiveModel::Dirty to work, we need to be able to call undeclared # properties as though they have get methods def method_missing(method_id, *args, &block) method_name = method_id.to_s if property?(method_name) self[method_name] else super end end def respond_to?(method_id, include_private = false) method_name = method_id.to_s if property?(method_name) true else super end end # Wrap the getter in a conversion from Java to Ruby def read_local_property_with_type_conversion(property) Neo4j::TypeConverters.to_ruby(self.class, property, read_local_property_without_type_conversion(property)) end # Wrap the setter in a conversion from Ruby to Java def write_local_property_with_type_conversion(property, value) write_local_property_without_type_conversion(property, Neo4j::TypeConverters.to_java(self.class, property, value)) end end end end