mccraigmccraig/activerecord-model-spaces

View on GitHub
lib/active_record/model_spaces/registry.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'active_record'
require 'active_record/model_spaces/model_space'

module ActiveRecord
  module ModelSpaces

    class Registry
      include Util

      attr_reader :model_spaces
      attr_reader :model_spaces_by_models
      attr_reader :enforce_context

      def initialize
        reset!
      end

      # drop all model_space and model registrations. will cause any with_model_space_context
      # to most likely bork horribly
      def reset!
        @model_spaces = {}
        @model_spaces_by_models = {}
      end

      def register_model(model, model_space_name, opts={})
        old_ms = unchecked_get_model_space_for_model(model)
        old_ms.deregister_model(model) if old_ms

        new_ms = register_model_space(model_space_name).register_model(model, opts)
        register_model_space_for_model(model, new_ms)
      end

      def set_base_table_name(model, table_name)
        ms = unchecked_get_model_space_for_model(model)
        raise "model #{model} is not (yet) registered to a ModelSpace. do in_model_space before set_table_name or use the :base_table_name option of in_model_space" if !ms
        ms.set_base_table_name(model, table_name)
      end

      def base_table_name(model)
        get_model_space_for_model(model).base_table_name(model)
      end

      def set_enforce_context(v)
        @enforce_context = !!v
      end

      def table_name(model)
        ctx = enforce_context ? get_context_for_model(model) : unchecked_get_context_for_model(model)
        if ctx
          ctx.table_name(model)
        else
          get_model_space_for_model(model).base_table_name(model)
        end
      end

      def current_table_name(model)
        get_context_for_model(model).current_table_name(model)
      end

      def working_table_name(model)
        get_context_for_model(model).working_table_name(model)
      end

      # create a new version of the model
      def new_version(model, &block)
        get_context_for_model(model).new_version(model, &block)
      end

      # create an updated version of the model
      def updated_version(model, &block)
        get_context_for_model(model).updated_version(model, &block)
      end

      def hoover(model)
        get_context_for_model(model).hoover
      end

      # execute a block with a ModelSpace context.
      # only a single context can be active for a given ModelSpace on any Thread at
      # any time, though different ModelSpaces may have active contexts concurrently
      def with_context(model_space_name, model_space_key, &block)

        ms = get_model_space(model_space_name)
        raise "no such model space: #{model_space_name}" if !ms

        current_ctx = merged_context[ms.name]

        if current_ctx && current_ctx.model_space_key==model_space_key.to_sym

          block.call # same context is already active

        elsif current_ctx

          raise "ModelSpace: #{model_space_name}: context with key #{current_ctx.model_space_key} already active"

        else

          old_merged_context = self.send(:merged_context)
          ctx = ms.create_context(model_space_key)
          context_stack << ctx
          begin
            self.merged_context = merge_context_stack

            r = block.call
            ctx.commit
            r
          ensure
            context_stack.pop
            self.merged_context = old_merged_context
          end

        end
      end

      def kill_context(model_space_name, model_space_key)
        get_model_space(model_space_name).kill_context(model_space_key)
      end

      # return the key of the active context for the given model_space, or nil
      # if there is no active context
      def active_key(model_space_name)
        ms = get_model_space(model_space_name)
        raise "no such model space: #{model_space_name}" if !ms

        ctx = merged_context[ms.name]

        ctx.model_space_key if ctx
      end

      private

      def register_model_space(model_space_name)
        model_spaces[model_space_name.to_sym] ||= ModelSpace.new(model_space_name.to_sym)
      end

      def get_model_space(model_space_name)
        model_spaces[model_space_name.to_sym]
      end

      def register_model_space_for_model(model, model_space)
        model_spaces_by_models[name_from_model(model)] = model_space
      end

      def unchecked_get_model_space_for_model(model)
        mc = all_model_superclasses(model).find do |klass|
          model_spaces_by_models[name_from_model(klass)]
        end
        model_spaces_by_models[name_from_model(mc)] if mc
      end

      def get_model_space_for_model(model)
        ms = unchecked_get_model_space_for_model(model)
        raise "model: #{model} is not registered to any ModelSpace" if !ms
        ms
      end

      CONTEXT_STACK_KEY = "ActiveRecord::ModelSpaces.context_stack"

      def context_stack
        Thread.current[CONTEXT_STACK_KEY] ||= []
      end

      MERGED_CONTEXT_KEY = "ActiveRecord::ModelSpaces.merged_context"

      def merged_context
        Thread.current[MERGED_CONTEXT_KEY] || {}
      end

      def merged_context=(mc)
        Thread.current[MERGED_CONTEXT_KEY] = mc
      end

      # merge all entries in the context stack into a map
      def merge_context_stack
        context_stack.reduce({}) do |m, ctx|
          raise "ModelSpace: #{ctx.model_space.name}: already has an active context" if m[ctx.model_space.name]
          m[ctx.model_space.name] = ctx
          m
        end
      end

      def unchecked_get_context_for_model(model)
        ms = unchecked_get_model_space_for_model(model)
        merged_context[ms.name] if ms
      end

      def get_context_for_model(model)
        ms = get_model_space_for_model(model)
        ctx = merged_context[ms.name]
        raise "ModelSpace: '#{ms.name}' has no current context" if !ctx
        ctx
      end
    end
  end
end