sxross/MotionModel

View on GitHub
motion/adapters/array_model_persistence.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module MotionModel
  module ArrayModelAdapter
    class PersistFileError < Exception; end
    class VersionNumberError < ArgumentError; end

    module PublicClassMethods

      def validate_schema_version(version_number)
        raise MotionModel::ArrayModelAdapter::VersionNumberError.new('version number must be a string') unless version_number.is_a?(String)
        if version_number !~ /^[\d.]+$/
          raise MotionModel::ArrayModelAdapter::VersionNumberError.new('version number string must contain only numbers and periods')
        end
      end

      # Declare a version number for this schema. For example:
      #
      # class Task
      #   include MotionModel::Model
      #   include MotionModel::ArrayModelAdapter
      #
      #   version_number 1.0.1
      # end
      #
      # When a version number mismatch occurs as an individual row is loaded
      # from persistent storage, the migrate method is invoked, allowing
      # you to programmatically migrate on a per-row basis.

      def schema_version(*version_number)
        if version_number.empty?
          return @schema_version
        else
          validate_schema_version(version_number[0])
          @schema_version = version_number[0]
        end
      end

      def migrate
      end


      # Returns the unarchived object if successful, otherwise false
      #
      # Note that subsequent calls to serialize/deserialize methods
      # will remember the file name, so they may omit that argument.
      #
      # Raises a +MotionModel::PersistFileFailureError+ on failure.
      def deserialize_from_file(file_name = nil, directory = nil)
        if schema_version != '1.0.0'
          migrate
        end

        @file_name = file_name if file_name
        @file_path =
          if directory.nil?
            documents_file(@file_name)
          else
            File.join(directory, @file_name)
          end
            

        if File.exist? @file_path
          error_ptr = Pointer.new(:object)

          data = NSData.dataWithContentsOfFile(@file_path, options:NSDataReadingMappedIfSafe, error:error_ptr)

          if data.nil?
            error = error_ptr[0]
            raise MotionModel::PersistFileError.new "Error when reading the data: #{error}"
          else
            bulk_update do
              NSKeyedUnarchiver.unarchiveObjectWithData(data)
            end
            
            # ensure _next_id is in sync with deserialized model
            max_id = self.all.map { |o| o.id }.max
            increment_next_id(max_id) unless max_id.nil? || _next_id > max_id
            
            return self
          end
        else
          return false
        end
      end
      # Serializes data to a persistent store (file, in this
      # terminology). Serialization is synchronous, so this
      # will pause your run loop until complete.
      #
      # +file_name+ is the name of the persistent store you
      # want to use. If you omit this, it will use the last
      # remembered file name.
      #
      # Raises a +MotionModel::PersistFileError+ on failure.
      def serialize_to_file(file_name = nil, directory = nil)
        @file_name = file_name if file_name
        @file_path =
          if directory.nil?
            documents_file(@file_name)
          else
            File.join(directory, @file_name)
          end
      
        error_ptr = Pointer.new(:object)

        data = NSKeyedArchiver.archivedDataWithRootObject collection
        unless data.writeToFile(@file_path, options: NSDataWritingAtomic, error: error_ptr)
          # De-reference the pointer.
          error = error_ptr[0]

          # Now we can use the `error' object.
          raise MotionModel::PersistFileError.new "Error when writing data: #{error}"
        end
      end

      def documents_file(file_name)
        file_path = File.join NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true), file_name
        file_path
      end
    end

    def initWithCoder(coder)
      self.init

      new_tag_id = 1
      columns.each do |attr|
        next if has_relation?(attr)
        # If a model revision has taken place, don't try to decode
        # something that's not there.
        if coder.containsValueForKey(attr.to_s)
          value = coder.decodeObjectForKey(attr.to_s)
          self.send("#{attr}=", value)
        else
          self.send("#{attr}=", nil)
        end

        # re-issue tags to make sure they are unique
        @tag = new_tag_id
        new_tag_id += 1
      end
      add_to_store

      self
    end

    # Follow Apple's recommendation not to encode missing
    # values.
    def encodeWithCoder(coder)
      columns.each do |attr|
        # Serialize attributes except the proxy has_many and belongs_to ones.
        unless [:belongs_to, :has_many, :has_one].include? column(attr).type
          value = self.send(attr)
          unless value.nil?
            coder.encodeObject(value, forKey: attr.to_s)
          end
        end
      end
    end

  end
end