lib/neo4j/shared/property.rb in neo4j-4.1.5 vs lib/neo4j/shared/property.rb in neo4j-5.0.0.rc.1

- old
+ new

@@ -9,26 +9,25 @@ include ActiveAttr::QueryAttributes include ActiveModel::Dirty class UndefinedPropertyError < RuntimeError; end class MultiparameterAssignmentError < StandardError; end - class IllegalPropertyError < StandardError; end + # @_declared_property_manager = DeclaredPropertyManager.new - ILLEGAL_PROPS = %w(from_node to_node start_node end_node) - attr_reader :_persisted_obj - def initialize(attributes = {}, options = {}) - attributes = process_attributes(attributes) + # TODO: Remove the commented :super entirely once this code is part of a release. + # It calls an init method in active_attr that has a very negative impact on performance. + def initialize(attributes = {}, _options = nil) + attributes = process_attributes(attributes) unless attributes.empty? @relationship_props = self.class.extract_association_attributes!(attributes) writer_method_props = extract_writer_methods!(attributes) validate_attributes!(attributes) - send_props(writer_method_props) unless writer_method_props.nil? + send_props(writer_method_props) unless writer_method_props.empty? @_persisted_obj = nil - - super(attributes, options) + # super(attributes, options) end # Returning nil when we get ActiveAttr::UnknownAttributeError from ActiveAttr def read_attribute(name) super(name) @@ -36,12 +35,12 @@ nil end alias_method :[], :read_attribute def default_properties=(properties) - keys = self.class.default_properties.keys - @default_properties = properties.select { |key| keys.include?(key) } + default_property_keys = self.class.default_properties_keys + @default_properties = properties.select { |key| default_property_keys.include?(key) } end def default_property(key) default_properties[key.to_sym] end @@ -51,67 +50,83 @@ # keys = self.class.default_properties.keys # _persisted_obj.props.reject{|key| !keys.include?(key)} end def send_props(hash) - hash.each do |key, value| - self.send("#{key}=", value) - end + hash.each { |key, value| self.send("#{key}=", value) } end + protected + + # This method is defined in ActiveModel. + # When each node is loaded, it is called once in pursuit of 'sanitize_for_mass_assignment', which this gem does not implement. + # In the course of doing that, it calls :attributes, which is quite expensive, so we return immediately. + def attribute_method?(attr_name) #:nodoc: + return false if attr_name == 'sanitize_for_mass_assignment' + super(attr_name) + end + private # Changes attributes hash to remove relationship keys # Raises an error if there are any keys left which haven't been defined as properties on the model def validate_attributes!(attributes) + return attributes if attributes.empty? invalid_properties = attributes.keys.map(&:to_s) - self.attributes.keys fail UndefinedPropertyError, "Undefined properties: #{invalid_properties.join(',')}" if invalid_properties.size > 0 end def extract_writer_methods!(attributes) - attributes.keys.each_with_object({}) do |key, writer_method_props| - writer_method_props[key] = attributes.delete(key) if self.respond_to?("#{key}=") + return attributes if attributes.empty? + {}.tap do |writer_method_props| + attributes.each_key do |key| + writer_method_props[key] = attributes.delete(key) if self.respond_to?("#{key}=") + end end end # Gives support for Rails date_select, datetime_select, time_select helpers. def process_attributes(attributes = nil) multi_parameter_attributes = {} new_attributes = {} attributes.each_pair do |key, value| - if key =~ /\A([^\(]+)\((\d+)([if])\)$/ - found_key, index = $1, $2.to_i + if match = key.match(/\A([^\(]+)\((\d+)([if])\)$/) + found_key = match[1] + index = match[2].to_i (multi_parameter_attributes[found_key] ||= {})[index] = value.empty? ? nil : value.send("to_#{$3}") else new_attributes[key] = value end end multi_parameter_attributes.empty? ? new_attributes : process_multiparameter_attributes(multi_parameter_attributes, new_attributes) end def process_multiparameter_attributes(multi_parameter_attributes, new_attributes) - multi_parameter_attributes.each_pair do |key, values| - begin - values = (values.keys.min..values.keys.max).map { |i| values[i] } - field = self.class.attributes[key.to_sym] - new_attributes[key] = instantiate_object(field, values) - rescue - raise MultiparameterAssignmentError, "error on assignment #{values.inspect} to #{key}" + multi_parameter_attributes.each_with_object(new_attributes) do |(key, values), attributes| + values = (values.keys.min..values.keys.max).map { |i| values[i] } + + if (field = self.class.attributes[key.to_sym]).nil? + fail MultiparameterAssignmentError, "error on assignment #{values.inspect} to #{key}" end + + attributes[key] = instantiate_object(field, values) end - new_attributes end def instantiate_object(field, values_with_empty_parameters) return nil if values_with_empty_parameters.all?(&:nil?) values = values_with_empty_parameters.collect { |v| v.nil? ? 1 : v } klass = field[:type] klass ? klass.new(*values) : values end module ClassMethods + extend Forwardable + + def_delegators :declared_property_manager, :serialized_properties, :serialized_properties=, :serialize, :declared_property_defaults + # Defines a property on the class # # See active_attr gem for allowed options, e.g which type # Notice, in Neo4j you don't have to declare properties before using them, see the neo4j-core api. # @@ -137,40 +152,49 @@ # class Person # # declare a property which can have any value # property :name, constraint: :unique # end def property(name, options = {}) - check_illegal_prop(name) - magic_properties(name, options) - attribute(name, options) + prop = DeclaredProperty.new(name, options) + prop.register + declared_property_manager.register(prop) + + attribute(name, prop.options) constraint_or_index(name, options) end def undef_property(name) - fail ArgumentError, "Argument `#{name}` not an attribute" if not attribute_names.include?(name.to_s) - - attribute_methods(name).each do |method| - undef_method(method) - end - + declared_property_manager.unregister(name) + attribute_methods(name).each { |method| undef_method(method) } undef_constraint_or_index(name) end + def declared_property_manager + @_declared_property_manager ||= DeclaredPropertyManager.new(self) + end + + # TODO: Move this to the DeclaredPropertyManager def default_property(name, &block) reset_default_properties(name) if default_properties.respond_to?(:size) default_properties[name] = block end # @return [Hash<Symbol,Proc>] def default_properties @default_property ||= {} end + def default_properties_keys + @default_properties_keys ||= default_properties.keys + end + def reset_default_properties(name_to_keep) default_properties.each_key do |property| + @default_properties_keys = nil undef_method(property) unless property == name_to_keep end + @default_properties_keys = nil @default_property = {} end def default_property_values(instance) default_properties.each_with_object({}) do |(key, block), result| @@ -185,10 +209,16 @@ send("#{name}_will_change!") unless typecast_value == read_attribute(name) super(value) end end + # @return [Hash] A frozen hash of all model properties with nil values. It is used during node loading and prevents + # an extra call to a slow dependency method. + def attributes_nil_hash + declared_property_manager.attributes_nil_hash + end + private def constraint_or_index(name, options) # either constraint or index, do not set both if options[:constraint] @@ -196,24 +226,9 @@ constraint(name, type: :unique) elsif options[:index] fail "unknown index type #{options[:index]}, only :exact supported" if options[:index] != :exact index(name, options) if options[:index] == :exact end - end - - def check_illegal_prop(name) - if ILLEGAL_PROPS.include?(name.to_s) - fail IllegalPropertyError, "#{name} is an illegal property" - end - end - - # Tweaks properties - def magic_properties(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 end end end