ioquatix/relaxo-model

View on GitHub
lib/relaxo/model/document.rb

Summary

Maintainability
A
45 mins
Test Coverage
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

require 'relaxo/model/component'

require 'msgpack'

module Relaxo
    module Model
        class ValidationFailure < StandardError
            def initialize(document, errors)
                @document = document
                @errors = errors
                
                super "Failed to validate document #{@document} because: #{@errors.join(', ')}!"
            end
            
            attr :document
            attr :errors
        end
        
        class TypeError < StandardError
            def initialize(document)
                @document = document
                
                super "Expected type #{@document.class.type} but got #{@document.type}!"
            end
            
            attr :document
        end
        
        module Document
            def self.included(child)
                child.send(:include, Component)
                child.send(:extend, ClassMethods)
            end
            
            module ClassMethods
                # Create a new document with a particular specified type.
                def create(dataset, properties = nil)
                    instance = self.new(dataset, type: @type)

                    if properties
                        properties.each do |key, value|
                            instance[key] = value
                        end
                    end

                    instance.after_create

                    return instance
                end
                
                def insert(dataset, properties)
                    instance = self.create(dataset, properties)
                    
                    instance.save(dataset)
                    
                    return instance
                end
                
                # Fetch a record or create a model object from a hash of attributes.
                def fetch(dataset, path = nil, **attributes)
                    if path and object = dataset.read(path)
                        instance = self.new(dataset, object, **attributes)
                        
                        instance.load_object
                        
                        instance.after_fetch
                        
                        return instance
                    end
                end
            end
            
            include Comparable
            
            def new_record?
                !persisted?
            end
            
            def persisted?
                @object != nil
            end
            
            def type
                @attributes[:type]
            end

            def valid_type?
                self.type == self.class.type
            end

            # Update any calculations:
            def before_save(changeset)
            end

            def after_save
            end

            # The canonical path to the object in the data store, assuming there is some unique way to identify the object.
            def to_s
                if primary_key = self.class.primary_key
                    primary_key.object_path(self)
                else
                    super
                end
            end

            def inspect
                "\#<#{self.class}:#{self.id} #{self.attributes.inspect}>"
            end

            # Make a copy of the record, as if calling create.
            def dup(dataset = @dataset)
                # Splat already calls dup internally I guess.
                clone = self.class.new(dataset, nil, @changed.dup, **@attributes)
                
                clone.after_create
                
                return clone
            end
            
            def paths
                return to_enum(:paths) unless block_given?
                
                self.class.keys.each do |name, key|
                    # @attributes is not modified until we call self.dump (which flattens @attributes into @changes). When we generate paths, we want to ensure these are done based on the non-mutable state of the object.
                    yield key.object_path(self, **@attributes)
                end
            end

            # Save the model object.
            def save(changeset)
                before_save(changeset)
                
                return if persisted? and @changed.empty?
                
                if errors = self.validate(changeset)
                    return errors
                end
                
                existing_paths = persisted? ? paths.to_a : []
                
                # Write data, check if any actual changes made:
                object = changeset.append(self.dump)
                return if object == @object
                
                existing_paths.each do |path|
                    changeset.delete(path)
                end
                
                paths do |path|
                    if changeset.exist?(path)
                        raise KeyError, "Dataset already contains path: #{path}, when inserting #{@attributes.inspect}"
                    end
                        
                    changeset.write(path, object)
                end
                
                @dataset = changeset
                @object = object
                
                after_save
                
                return true
            end

            def save!(changeset)
                result = self.save(changeset)
                
                if result != true
                    raise ValidationFailure.new(self, result)
                end
                
                return self
            end

            def before_delete
            end

            def after_delete
            end

            def delete(changeset)
                before_delete
                
                @changed.clear
                
                paths.each do |path|
                    changeset.delete(path)
                end

                after_delete
            end

            def after_fetch
                raise TypeError.new(self) unless valid_type?
            end

            # Set any default values:
            def after_create
            end
            
            # Equality is done only on id to improve performance.
            def <=> other
                self.id <=> other.id if other
            end
            
            def eql? other
                self.id.eql?(other.id) if other
            end
            
            def == other
                self.attributes == other.attributes if other
            end
            
            def hash
                self.id.hash
            end
            
            def empty?
                @attributes.empty?
            end
        end
    end
end