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! whitelist! 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 if @data.has_key?(m) @data[m] else super end 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! whitelist! 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! whitelist! 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 whitelist! end def has? key @data.has_key?(key) 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 def whitelist! unless self.class.whitelist_keys.empty? @data.select! do |k, v| RESERVED_KEYS.include?(k) || self.class.whitelist_keys.include?(k) 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. # If block is omitted, it only validates the existence of the key. # # @param key Key to be validated. # @param message Message that will be displayed when validation fails. # Will be appended after the key. # @yield [v] Value of the key. # The block must return truthy for it to pass the validation. # # @example # validate :first_name, 'does not exist', lambda { |v| !v.nil? } # def self.validate key, message, block raise ValidationError, "Provide validation method for key #{key}" unless block validate_keys << [key, message, block] end # This Document should only accept keys that are specified here. # The existence of these keys are optional, and won't trigger any validation # unless specified by the validate methods. Hashes passed to update and # initialize will be filtered through this list. # # If you don't specify a whitelist, Document will accept any keys, but # once you specify it, only those keys will be accepted and the rest will # be dropped. # # @example # whitelist :first_name, :last_name, :address # def self.whitelist *list list.each do |k| whitelist_keys << k.to_s.to_sym end 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.whitelist_keys @whitelist_keys ||= [] end def self.symbolize hash hash.inject({}) do |memo,(k,v)| memo[k.to_sym] = v memo end end end end