module CouchPillow class Document extend Attributive attr_reader :id RESERVED_KEYS = %i[_id _type _created_at _updated_at] DEFAULT_TYPE = "default".freeze attribute(:_created_at) .required .type(Time).auto_convert .default { Time.now.utc } attribute(:_updated_at) .required .type(Time).auto_convert .default { Time.now.utc } def initialize hash = {}, id = "#{self.class._type}::#{SecureRandom.hex}" @data = self.class.symbolize(hash) @id = id time = Time.now.utc @data[:_created_at] ||= time @data[:_updated_at] = time rename! whitelist! assign_defaults! auto_convert! end def [] key @data[key.to_s.to_sym] end def []= key, value @data[key.to_s.to_sym] = value end # @private # def timestamp! @data[:_updated_at] = Time.now.utc end # Save this document to the server # def save! opts = {} whitelist! sort! timestamp! validate! to_save = @data.merge({ :_type => self.class._type }) CouchPillow.db.set(@id, to_save, opts) end # Delete this document from the server. # def delete! CouchPillow.db.delete @id end # Attempt to update this Document. Fails if this Document does not yet # exist in the database. # def update! whitelist! sort! timestamp! validate! to_save = @data.merge({ :_type => self.class._type }) CouchPillow.db.replace @id, to_save end # Updates the attributes in the document. # Existing attributes will be overwritten and new ones will be added. # Any other 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 rename! 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, :_type => self.class._type }.merge!(@data) end # Rename the keys in this Document as specified by the {rename} directive. # 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 # Cleanup the @data hash so it only contains relevant fields. # def whitelist! @data.delete_if do |k, v| !self.class.attributes.has_key?(k) end end # Assign default value # def assign_defaults! self.class.attributes.each do |k, attr| @data[k] = attr.trigger_default_directive if !has?(k) && attr.has_default? end end # Auto convert. # def auto_convert! self.class.attributes.each do |k, attr| @data[k] = attr.trigger_auto_convert_directive(@data[k]) if has?(k) end end # Go through each attribute, and validate the values. # Validation also perform auto-conversion if auto-conversion is enabled # for that attribute. # def validate! self.class.attributes.each do |k, attr| if has?(k) @data[k] = attr.validate(@data[k]) if has?(k) else @data[k] = attr.trigger_default_directive if attr.has_default? raise ValidationError, "Attribute '#{k}' is required" if attr.required? && !has?(k) end end end # Sort keys on this document. # def sort! @data = @data.sort.to_h end def _type self.class._type end def _id @id end # Get a Document given an id. # # @return nil if not found or Document is of a different type. # def self.get id result = CouchPillow.db.get(id) and type = result[:_type] || result["_type"] and type == _type and new(result, id) or nil 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 # Sets the type of this Document. # def self.type value @type = value.to_s end private def self._type @type ||= DEFAULT_TYPE end def self.rename_keys @rename_keys ||= [] end def self.symbolize hash hash.inject({}) do |memo,(k,v)| memo[k.to_sym] = v memo end end end end