module ActiveModelSerializerPlus::Assignment

@author Todd Knarr <tknarr@silverglass.org>

Default implementation of the #attributes= method for ActiveModel classes.

This is the base of the JSON and Xml modules. It supplies a standard #attributes= method to classes that follows the basic pattern of the custom one you’d normally write and adds the ability to pick up object type information from the #attributes_types hash to convert the hash values of contained objects into actual objects. This mostly eliminates the need to write custom #attributes= methods for each class with code to check attribute names and initialize objects from child hashes.

The standard ActiveModel/ActiveRecord serialization/deserialization has some flaws, for instance it will serialize classes as contained hashes whether or not they have an #attributes= method available to deserialize them. We work around that by having building Procs which can do the correct thing in many cases.

Public Instance Methods

attributes=(hash) click to toggle source

The default #attributes= method which assigns a hash to the object’s attributes. @param [Hash] hash the hash of attribute names and values @return [self] self @raise [ArgumentError] if any error occurs

# File lib/active_model_serializer_plus/assignment.rb, line 148
def attributes=(hash)
    hash.each do |key, value|
        # Check #attribute_types for what type this item should be, and if we have an entry
        # convert it to an actual Class object we can use.
        attr_klass = nil
        attr_typename = nil

        attr_typename = self.attribute_types[key] if self.respond_to?(:attribute_types) && self.attribute_types.is_a?(Hash)
        if attr_typename.is_a?(Array)
            # Container

            container_klass = nil
            object_klass = nil

            if attr_typename.length != 2
                raise ArgumentError, "Container type specification for attribute #{key.to_s} is invalid."
            end

            # Sort out the type of container. First make sure the container type name translates to 'Container'.
            # If the type name actually is 'Container', take the type of the container from the type of the
            # original value. If it's an actual type name, convert it to a Class object. Raise exceptions if
            # any problems are encountered.
            container_typename = attr_typename[0]
            if container_typename == 'Container'
                container_klass = value.class
            else
                xlated_container_typename = ActiveModelSerializerPlus.type_name_xlate(container_typename)
                if xlated_container_typename != 'Container'
                    raise ArgumentError, "Container type #{container_typename} for attribute #{key.to_s} is not a container."
                end
                begin
                    container_klass = ActiveModelSerializerPlus.to_class(container_typename)
                rescue ArgumentError
                    raise ArgumentError, "Container type #{container_typename} for attribute #{key.to_s} is not a valid type."
                end
            end

            # Sort out the type of objects in the container. Convert the object type name to a Class object
            # and raise an exception if it can't be converted.
            object_typename = attr_typename[1]
            begin
                object_klass = ActiveModelSerializerPlus.to_class(object_typename)
            rescue ArgumentError
                raise ArgumentError, "Object type #{object_typename} for attribute #{key.to_s} is not a valid type."
            end

            container = container_klass.new
            if container.nil?
                raise ArgumentError, "Cannot create container of type #{container_klass.name}."
            end
            adder_proc = ActiveModelSerializerPlus.get_container_adder(container_klass.name)
            iterator_proc = ActiveModelSerializerPlus.get_container_iterator(value.class.name)
            if adder_proc.nil? || iterator_proc.nil?
                msg = ''
                if iterator_proc.nil?
                    msg << "iterate through #{value.class.name}"
                end
                if adder_proc.nil?
                    if msg.length == 0
                        msg << ', '
                    end
                    msg << "add to #{container_klass.name}"
                end
                raise ArgumentError, "Cannot #{msg}."
            end
            # Build the new container by iterating through the value container and adding each item to
            # the new container, using the procs we looked up.
            iterator_proc.call(container, adder_proc, object_klass, value)

            self.send("#{key}=", container)

        else
            # Object

            unless attr_typename.nil?
                begin
                    attr_klass = ActiveModelSerializerPlus.to_class(attr_typename)
                rescue ArgumentError
                    raise ArgumentError, "Type #{attr_typename.to_s} for attribute #{key.to_s} is not a valid type."
                end

                v = ActiveModelSerializerPlus.build_object(attr_klass, value)
                value = v unless v.nil?
            end

            self.send("#{key}=", value)

        end

        self
    end

end