we4tech/flexi-model

View on GitHub
lib/flexi_model/ar_persistence.rb

Summary

Maintainability
A
0 mins
Test Coverage
module FlexiModel
  module ArPersistence
    extend ActiveSupport::Concern

    RECORD     = FlexiModel::ArModels::Record
    COLLECTION = FlexiModel::ArModels::Collection
    FIELD      = FlexiModel::ArModels::Field
    VALUE      = FlexiModel::ArModels::Value

    included do
      class_eval <<-RUBY
        @@_flexi_collection = nil
        cattr_accessor :_flexi_collection

        @@_flexi_metadata = { }
        cattr_accessor :_flexi_metadata

        @@_flexi_fields_map = nil
        cattr_accessor :_flexi_fields_map

      RUBY
    end

    module ClassMethods
      # Set collection label
      #
      # singular - Set singular name for the collection
      # plural - Set plural name for the collection
      def set_flexi_label(singular, plural)
        _flexi_metadata[:label_singular] = singular
        _flexi_metadata[:label_plural]   = plural
      end

      # Return singular and plural collection label
      # If not defined it will take class name as collection name
      #
      # Returns array of singular and plural labels
      def get_flexi_label
        labels = [_flexi_metadata[:label_singular],
                  _flexi_metadata[:label_plural]].compact

        if labels.empty?
          _f_name = _friendly_name(self.name)
          [_f_name.singularize, _f_name.pluralize]
        else
          labels
        end
      end

      # Return collection name based on parametrized class name
      def flexi_collection_name
        self.name.parameterize
      end

      def get_flexi_namespace
        self.flexi_collection_name.parameterize
      end

      # Initialize new instance and set data from record
      def initialize_with_record(record)
        inst = self.new
        inst.send(:_record=, record)
        inst.send(:_id=, record.id)
        inst
      end

      # Return by default plural label
      def flexi_label(singular = false)
        if singular
          self._flexi_collection.singular_label
        else
          self._flexi_collection.plural_label
        end
      end

      def flexi_collection;
        _flexi_collection
      end

      delegate :id, :to => :_flexi_collection

      def destroy_all;
        RECORD.by_namespace(self.get_flexi_namespace).each do |record|
          inst = initialize_with_record(record)
          inst.destroy
        end
      end

      def delete_all;
        RECORD.by_namespace(self.get_flexi_namespace).delete_all
      end

      # Create does exactly as `save`, but it initiates `:create` callbacks
      def create(attributes = { })
        inst = self.new(attributes)
        inst.save

        inst
      end

      private
      def _friendly_name(long_name)
        long_name.to_s.split("::").last
      end
    end

    def initialize(*)
      super
      _find_or_update_or_build_collection!
    end

    # Ensure object with same _id returns true on equality check
    def ==(another_instance)
      self._id && self._id == another_instance._id
    end

    # Return true if record is not saved
    def new_record?
      !_id.present?
    end

    # Store record in persistent storage
    def save
      create_or_update
      _id.present?
    end

    # Update stored attributes by give hash
    def update_attributes(_params)
      assign_attributes _params
      save
    end

    # Update single attribute by key and value
    def update_attribute(key, value)
      self.update_attributes(key => value)
    end

    def destroy
      if _id.present?
        RECORD.delete(self._id)
      else
        false
      end
    end

    # Reload object instance
    def reload
      self.class.find(self._id)
    end

    # Forcefully load all attributes
    def load_attributes!
      self.flexi_fields.map { |f| self.send(f.name.to_sym) }
    end

    # Return existing or create new collection set
    def get_flexi_collection
      _find_or_update_or_build_collection!

      self._flexi_collection
    end

    # Return flexi fields in name and field object map
    def get_flexi_fields_map
      self._flexi_fields_map ||=
          Hash[get_flexi_collection.fields.
                   map { |_field| [_field.name.to_sym, _field] }]
    end

    delegate :created_at, :updated_at, :to => :_get_record, :prefix => :flexi

    private
    def create_or_update
      _id.nil? ? create : update
    end

    def create(*)
      # Initialize AR record and store
      record = _get_record
      record.save

      # Set Id and errors to the parent host object
      self._id = record.id
      @errors  = record.errors

      record
    end

    def update(*)
      record = _get_record
      record.values.destroy_all
      record.update_attributes(values: _get_values)
      record
    end

    def _get_record
      _load_record_instance!
      self._record
    end

    def _load_record_instance!
      self._record ||= _record_load_or_initialize
    end

    def _record_load_or_initialize
      collection = _find_or_update_or_build_collection!

      if self._id.nil?
        RECORD.new(
            namespace:  self.class.get_flexi_namespace,
            collection: collection,
            values:     self.send(:_get_values)
        )
      else
        RECORD.find(self._id)
      end
    end

    # Return `Value` object based on flexi attributes
    def _get_values
      _fields_map = get_flexi_fields_map

      @attributes.map do |k, v|
        field = _fields_map[k]
        raise "Field - #{k} not defined" if field.nil?
        VALUE.new(:field => field, value: self.send(:"#{k}"))
      end
    end

    # Find existing collection object
    #   If not found create new collection
    #   If found but schema is back dated
    #     update schema
    def _find_or_update_or_build_collection!
      return _flexi_collection if _flexi_collection.present?

      # Find existing collection
      self._flexi_collection = COLLECTION.where(
          namespace:    self.class.get_flexi_namespace,
          name:         self.class.flexi_collection_name,
          partition_id: self.class.flexi_partition_id
      ).first

      # Update if schema changed
      if self._flexi_collection
        _update_schema
      else
        _build_collection
      end

      self._flexi_collection
    end

    # Check whether update is back dated
    # This update is verified through comparing stored collection
    # and new definition
    def _update_schema
      singular_label, plural_label = self.class.get_flexi_label
      existing                     = self._flexi_collection

      # Check labels
      if existing.singular_label != singular_label
        existing.update_attribute :singular_label, singular_label
      end

      if existing.plural_label != plural_label
        existing.update_attribute :plural_label, plural_label
      end


      # Check fields
      fields = _build_fields
      if _fields_changed? fields, existing
        # TODO: Dangerous need to fix it up
        existing.fields.destroy_all
        existing.update_attribute :fields, fields
      end
    end

    def _fields_changed?(fields, existing)
      added_or_removed = existing.fields.length != fields.length
      name_changed     = existing.fields.map(&:name).sort != fields.map(&:name).sort
      type_changed     = existing.fields.map(&:field_type).sort != fields.map(&:field_type).sort

      added_or_removed || name_changed || type_changed
    end

    def _build_collection
      singular_label, plural_label = self.class.get_flexi_label

      self._flexi_collection = COLLECTION.create(
          namespace:      self.class.get_flexi_namespace,
          name:           self.class.flexi_collection_name,
          partition_id:   self.class.flexi_partition_id,
          singular_label: singular_label,
          plural_label:   plural_label,

          fields:         _build_fields
      )
    end

    def _build_fields
      self.flexi_fields.map do |field|
        params = {
            namespace:      self.class.get_flexi_namespace,
            name:           field.name.to_s,
            partition_id:   self.class.flexi_partition_id,
            field_type:     field.type,
            singular_label: field.singular,
            plural_label:   field.plural
        }

        FIELD.send(:"find_or_create_by_#{params.keys.map(&:to_s).join('_and_')}", params)
      end
    end
  end
end