lib/ork/model/associations.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module Ork::Model
  module Associations
    # A macro for defining an attribute, an index, and an accessor
    # for a given model.
    #
    # Example:
    #
    #   class Post
    #     include Ork::Document
    #
    #     reference :user, :User
    #   end
    #
    #   # It's the same as:
    #
    #   class Post
    #     include Ork::Document
    #
    #     attribute :user_id
    #     index :user_id
    #
    #     def user
    #       @_memo[:user] ||= User[user_id]
    #     end
    #
    #     def user=(user)
    #       self.user_id = user.id
    #       @_memo[:user] = user
    #     end
    #
    #     def user_id=(user_id)
    #       @_memo.delete(:user_id)
    #       self.user_id = user_id
    #     end
    #   end
    #
    def reference(name, model)
      reader = :"#{name}_id"
      writer = :"#{name}_id="

      attribute reader, accessors: :reader
      index reader

      define_method(writer) do |value|
        @_memo.delete(name)
        @attributes[reader] = value
      end

      define_method(:"#{name}=") do |object|
        assert_valid_class object, model

        send(writer, object ? object.id : nil)
        @_memo[name] = object
      end

      define_method(name) do
        @_memo[name] ||= begin
                           model = Ork::Utils.const(self.class, model)
                           model[send(reader)]
                         end
      end
    end

    # A macro for defining a method which basically does a find.
    #
    # Example:
    #   class Post
    #     include Ork::Document
    #
    #     reference :user, :User
    #   end
    #
    #   class User
    #     include Ork::Document
    #
    #     referenced :post, :Post
    #   end
    #
    #   # is the same as
    #
    #   class User
    #     include Ork::Document
    #
    #     def post
    #       Post.find(:user_id => self.id)
    #     end
    #   end
    #
    def referenced(name, model, reference = to_reference)
      define_method name do
        return nil if self.id.nil?
        @_memo[name] ||= begin
                           model = Ork::Utils.const(self.class, model)
                           model.find(:"#{reference}_id", self.id).first
                         end
      end
    end

    # A macro for defining a method which basically does a find.
    #
    # Example:
    #   class Post
    #     include Ork::Document
    #
    #     reference :user, :User
    #   end
    #
    #   class User
    #     include Ork::Document
    #
    #     collection :posts, :Post
    #   end
    #
    #   # is the same as
    #
    #   class User
    #     include Ork::Document
    #
    #     attribute posts_ids
    #
    #     def posts
    #       Post.all(self.posts_ids)
    #     end
    #
    #     def posts_add(post)
    #       self.posts_ids << post.id
    #     end
    #   end
    #
    def collection(name, model, reference = to_reference)
      reader = :"#{name}_ids"

      attribute reader, accessors: :reader

      define_method(:"#{name}_ids=") do |value|
        @_memo.delete(name)
        @attributes[reader] = value
      end

      define_method(name) do
        return [] if self.id.nil?
        @_memo[name] ||= begin
                           model = Ork::Utils.const(self.class, model)
                           model.all(@attributes[reader].to_a)
                         end
      end

      define_method(:"#{name}_add") do |object|
        assert_valid_class object, model

        @attributes[reader] = Array(@attributes[reader]) << object.id
        @_memo[name] << object if @_memo[name]
      end

      define_method(:"#{name}_remove") do |object|
        assert_valid_class object, model

        @_memo[name].delete(object) if @_memo[name]
        @attributes[reader].delete(object.id) and object if @attributes[reader]
      end
    end

    # A macro for defining an attribute, and the accessors
    # for a given model.
    #
    # Example:
    #
    #   class Post
    #     include Ork::Document
    #
    #     embed :author, :Author
    #   end
    #
    #   # It's the same as:
    #
    #   class Post
    #     include Ork::Document
    #
    #     def author
    #       @embedding[:author]
    #     end
    #
    #     def author=(author)
    #       @embedding[:author] = author
    #       author.__parent = self
    #     end
    #   end
    #
    def embed(name, model)
      embedding << name unless embedding.include?(name)

      define_method(name) do
        return nil unless @embedding.has_key? name
        @_memo[name] ||= begin
                           model = Ork::Utils.const(self.class, model)
                           new_embedded model, @embedding[name]
                         end
      end

      define_method(:"#{name}=") do |object|
        assert_embeddable object

        @embedding[name] = object.attributes
        object.__parent = self

        @_memo[name] = object
      end
    end

    # A macro for find embedded objects of the same type, massive assign and
    # syntactic sugar for add an object to the collection.
    #
    # Example:
    #
    #   class Post
    #     include Ork::Document
    #
    #     embed_collection :authors, :Author
    #   end
    #
    #   # It's the same as:
    #
    #   class Post
    #     include Ork::Document
    #
    #     def authors
    #       # An array of authors
    #     end
    #
    #     def authors_add(author)
    #       # Add an author to the embed collection
    #     end
    #   end
    #
    def embed_collection(name, model)
      embedding << name unless embedding.include?(name)

      define_method(name) do
        return [] unless @embedding.has_key? name

        @_memo[name] ||= begin
                           model = Ork::Utils.const(self.class, model)
                           @embedding[name].map do |atts|
                             new_embedded model, atts
                           end
                         end
      end

      define_method(:"#{name}_add") do |object|
        assert_embeddable object

        object.__parent = self
        @_memo[name] << object if @_memo[name]
        @embedding[name] = Array(@embedding[name]) << object.attributes
      end

      define_method(:"#{name}_remove") do |object|
        assert_embeddable object

        object.__parent = nil
        @_memo[name].delete(object) if @_memo[name]
        @embedding[name].delete(object.attributes) and object if @embedding[name]
      end
    end

    private

    def to_reference
      name.to_s.
        match(/^(?:.*::)*(.*)$/)[1].
        gsub(/([a-z\d])([A-Z])/, '\1_\2').
        downcase.to_sym
    end
  end
end