lib/relaxo/model/document.rb
# 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