lib/couchpillow/document.rb in couchpillow-0.3.10 vs lib/couchpillow/document.rb in couchpillow-0.4.0

- old
+ new

@@ -1,48 +1,39 @@ module CouchPillow class Document - # Validation Error. - # - class ValidationError < StandardError; end + extend Attributive - - # Faux Boolean class used for type checking and conversion. - # - class Boolean; end - - attr_reader :id + RESERVED_KEYS = %i[_id _type _created_at _updated_at] - @type = "default" + DEFAULT_TYPE = "default".freeze - PRESENCE_LAMBDA = lambda do |value| !value.nil? end + attribute(:_created_at) + .required + .type(Time).auto_convert + .default { Time.now.utc } - RESERVED_KEYS = %i[_id _type created_at updated_at] + attribute(:_updated_at) + .required + .type(Time).auto_convert + .default { Time.now.utc } - def initialize hash = {}, id = SecureRandom.hex + 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 - @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] @@ -53,90 +44,63 @@ @data[key.to_s.to_sym] = value end # @private - # 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 - - - # @private - # - def respond_to? m - ms = m.to_s - return true if ms.end_with?("=") - @data.has_key?(m) or - super - end - - - # @private - # def timestamp! - @data[:updated_at] = Time.now.utc + @data[:_updated_at] = Time.now.utc end # Save this document to the server # def save! whitelist! - validate + validate! sort! timestamp! - CouchPillow.db.set @id, @data + to_save = @data.merge({ + :_type => self.class._type + }) + CouchPillow.db.set @id, to_save 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 + validate! sort! timestamp! - CouchPillow.db.replace @id, @data + 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. - # Existing attributes that are not present in the hash will be ignored. + # 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! + validate! end def has? key @data.has_key?(key) @@ -147,22 +111,14 @@ to_hash.to_json(*a) end def to_hash - { :_id => @id }.merge!(@data) + { :_id => @id, :_type => self.class._type }.merge!(@data) end - def validate - self.class.validate_keys.each do |k, msg, method| - raise ValidationError, "[#{k}, #{@data[k]}] #{msg}" unless - @data.has_key?(k) && method.call(@data[k]) - end - 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 @@ -170,67 +126,65 @@ @data.delete(from) end end - # Ensure types of the values in the Document are converted to the type - # specified in the {validate_type} method. + # Cleanup the @data hash so it only contains relevant fields. # - 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) - elsif t == Boolean - @data[k] = value == 0 || !value ? false : true - end - end + def whitelist! + @data.delete_if do |k, v| + !self.class.attributes.has_key?(k) end end - # Filter this Document through the whitelist as specified by the - # {whitelist} directive. + # Go through each attribute, and validate the values. + # Validation also perform auto-conversion if auto-conversion is enabled + # for that attribute. # - def whitelist! - unless self.class.whitelist_keys.empty? - @data.select! do |k, v| - RESERVED_KEYS.include?(k) || - self.class.whitelist_keys.include?(k) + 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 Document is of a different type. + # @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'] and - type == self._type and + type = result[:_type] || result["_type"] and + type == _type and new(result, id) or nil end - # Sets the type of this Document. - # - def self.type value - @type = value.to_s - 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 @@ -238,110 +192,34 @@ 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. + # Sets the type of this Document. # - def self.validate_presence key - validate key, "is missing", PRESENCE_LAMBDA + def self.type value + @type = value.to_s end - # Validate the type of a particular key. - # - # @example - # validate_type :name, String - # - def self.validate_type key, type - validate key, "#{key} is not the correct type. Expected a #{type}", lambda { |v| v.is_a? type } - type_keys << [key, type] - end - - - # Validate the type is a Boolean, since Ruby lacks a Boolean class. - # - # @example - # validate_type_boolean :available, Boolean - # - def self.validate_type_boolean key - validate key, "#{key} is not the correct type. Expected a Boolean", lambda { |v| !!v == v } - type_keys << [key, Boolean] - end - - - # Validate the presence of a particular key using a custom validation method. - # - # @param key Key to be validated. - # @param message Message that will be displayed when validation fails. - # @yield [v] Value of the key. - # The block must return truthy for it to pass the validation. - # - # @example - # validate :first_name, 'first name is not Joe', lambda { |v| v != "Joe" } - # - 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. - # - # @param list Whitelist of keys. - # - # @example - # whitelist :first_name, :last_name, :address - # - def self.whitelist *list - list.flatten.each do |k| - whitelist_keys << k.to_s.to_sym - end - end - - private - # Read the type of this Document. Internal use only. - # def self._type - @type + @type ||= DEFAULT_TYPE end 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