module CouchPillow class Document class ValidationError < StandardError; end attr_reader :id @type = "default" PRESENCE_LAMBDA = lambda do |value| !value.nil? end RESERVED_KEYS = %i[_id _type created_at updated_at] def initialize hash = {}, id = SecureRandom.hex @data = self.class.symbolize(hash) @id = id @data[:created_at] and @data[:created_at].is_a? String and @data[:created_at] = Time.parse(@data[:created_at]) or @data[:created_at] = Time.now.utc @data[:updated_at] and @data[:updated_at].is_a? String and @data[:updated_at] = Time.parse(@data[:updated_at]) raise TypeError if @data[:_type] && @data[:_type] != self.class._type @data[:_type] = self.class._type rename! ensure_types! end def [] key @data[key.to_s.to_sym] end def []= key, value @data[key.to_s.to_sym] = value end # Map hash keys to methods # def method_missing m, *args, &block ms = m.to_s if ms.end_with?("=") ms.gsub!('=', '') @data[ms.to_sym] = args[0] else @data[m] if @data.has_key?(m) or super end end def respond_to? m ms = m.to_s return true if ms.end_with?("=") @data.has_key?(m) or super end def timestamp! @data[:updated_at] = Time.now.utc end # Save this document to the server # def save! validate sort! timestamp! CouchPillow.db.set @id, @data end # Delete this document from the server. # def delete! CouchPillow.db.delete @id end # Sort keys on this document. # def sort! @data = @data.sort.to_h end # Attempt to update this Document. Fails if this Document does not yet # exist in the database. # def update! validate sort! timestamp! CouchPillow.db.replace @id, @data end # Updates the attributes in the document. # Existing attributes will be overwritten and new ones will be added. # Existing attributes that are not present in the hash will be ignored. # def update hash hash.each do |k,v| @data[k.to_sym] = v end end def to_json *a to_hash.to_json(*a) end def to_hash { :_id => @id }.merge!(@data) end def validate self.class.validate_keys.each do |k, msg, method| raise ValidationError, "#{k} #{msg}" unless @data.has_key?(k) && method.call(@data[k]) end end def rename! self.class.rename_keys.each do |from, to| @data.has_key?(from) and @data[to] = @data[from] and @data.delete(from) end end def ensure_types! self.class.type_keys.each do |k, t| if value = @data[k] and !value.is_a?(t) if t == Integer @data[k] = Integer(value) elsif t == Float @data[k] = Float(value) elsif t == String @data[k] = String(value) elsif t == Array @data[k] = Array(value) elsif t == Time @data[k] = Time.parse(value) end end end end # Get a Document given an id. # # @returns nil if Document is of a different type. # def self.get id result = CouchPillow.db.get(id) and type = result['_type'] and type == self._type and new(result, id) or nil end def self.type value @type = value.to_s end def self._type @type end # Rename an existing key to a new key. This is invoked right after initialize. # def self.rename from, to raise ArgumentError, "Cannot rename reserved keys" if RESERVED_KEYS.include?(from) || RESERVED_KEYS.include?(to) rename_keys << [from.to_s.to_sym, to.to_s.to_sym] end # Validate the presence of a particular key, and the value of that key # cannot be nil. # def self.validate_presence key validate key, "is missing", PRESENCE_LAMBDA end # Validate the type of a particular key. # def self.validate_type key, type validate key, "is not the correct type. Expected a #{type}", lambda { |v| v.is_a? type } type_keys << [key, type] end # Validate the presence of a particular key using a custom validation method. # Implies the Document must contain the key. # def self.validate key, message, block raise ValidationError, "Provide validation method for key #{key}" unless block validate_keys << [key, message, block] end private def self.rename_keys @rename_keys ||= [] end def self.type_keys @type_keys ||= [] end def self.validate_keys @validate_keys ||= [] end def self.symbolize hash hash.inject({}) do |memo,(k,v)| memo[k.to_sym] = v memo end end end end