rom-rb/rom-repository

View on GitHub
lib/rom/repository/changeset/associated.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'rom/initializer'

module ROM
  class Changeset
    # Associated changesets automatically set up FKs
    #
    # @api public
    class Associated
      extend Initializer

      # @!attribute [r] left
      #   @return [Changeset::Create] Child changeset
      param :left

      # @!attribute [r] associations
      #   @return [Array] List of association identifiers from relation schema
      option :associations

      # Infer association name from an object with a schema
      #
      # This expects other to be an object with a schema that includes a primary key
      # attribute with :source meta information. This makes it work with both structs
      # and relations
      #
      # @see Stateful#associate
      #
      # @api private
      def self.infer_assoc_name(other)
        schema = other.class.schema
        attrs = schema.is_a?(Hash) ? schema.values : schema
        pk = attrs.detect { |attr| attr.meta[:primary_key] }

        if pk
          pk.meta[:source].relation
        else
          raise ArgumentError, "can't infer association name for #{other}"
        end
      end

      # Commit changeset's composite command
      #
      # @example
      #   task_changeset = task_repo.
      #     changeset(title: 'Task One').
      #     associate(user, :user).
      #     commit
      #   # {:id => 1, :user_id => 1, title: 'Task One'}
      #
      # @return [Array<Hash>, Hash]
      #
      # @api public
      def commit
        command.call
      end

      # @api public
      def associate(other, name = Associated.infer_assoc_name(other))
        self.class.new(left, associations: associations.merge(name => other))
      end

      # Create a composed command
      #
      # @example using existing parent data
      #   user_changeset = user_repo.changeset(name: 'Jane')
      #   task_changeset = task_repo.changeset(title: 'Task One')
      #
      #   user = user_repo.create(user_changeset)
      #   task = task_repo.create(task_changeset.associate(user, :user))
      #
      # @example saving both parent and child in one go
      #   user_changeset = user_repo.changeset(name: 'Jane')
      #   task_changeset = task_repo.changeset(title: 'Task One')
      #
      #   task = task_repo.create(task_changeset.associate(user, :user))
      #
      # This works *only* with parent => child(ren) changeset hierarchy
      #
      # @return [ROM::Command::Composite]
      #
      # @api public
      def command
        associations.reduce(left.command.curry(left)) do |a, (assoc, other)|
          case other
          when Changeset
            a >> other.command.with_association(assoc).curry(other)
          when Associated
            a >> other.command.with_association(assoc)
          else
            a.with_association(assoc, parent: other)
          end
        end
      end

      # @api private
      def relation
        left.relation
      end
    end
  end
end