Skip to content

Instantly share code, notes, and snippets.

@rf-
Created April 16, 2012 00:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rf-/2395628 to your computer and use it in GitHub Desktop.
Save rf-/2395628 to your computer and use it in GitHub Desktop.
# This module defines an alternative to STI where individual types are
# represented by lightweight proxies instead of full models. Each type can have
# its own validations, callbacks, and business logic. In general, client code
# works with the underlying model class directly instead of the type classes,
# but the #as_type method returns a wrapped object that can be used to access
# type-specific logic.
module HasType
INVALID_TYPE_MESSAGE = 'Please choose a valid type'
# `HasType::Model` is to be included in an ActiveRecord::Base subclass with a
# `type` column. It can also have a `TYPE_CONTAINER` constant containing the
# name of a module which must contain all types for the given model.
module Model
extend ActiveSupport::Concern
included do
set_inheritance_column 'none' # to disable STI
validate :confirm_valid_type
validate :run_type_validations
end
# @return [HasType::Type] An instance of `type_class` wrapping the base
# object.
# @raise [TypeError] If `type` isn't set.
def as_type
@_has_type_type_instance ||=
if type
type_class.new(self)
else
raise TypeError, 'Missing type attribute'
end
end
def type=(new_type)
@_has_type_type_class = nil
@_has_type_type_instance = nil
super
end
protected
# Make sure `type` is set and refers to a valid subclass of HasType::Type.
def confirm_valid_type
errors.add(:base, INVALID_TYPE_MESSAGE) unless valid_type?
end
# Run `type_class`'s validations and copy any errors to the base object.
def run_type_validations
if valid_type? && (type_object = self.as_type).invalid?
type_object.errors.each do |attr, message|
self.errors.add(attr, message)
end
end
end
# Is there a type which corresponds to a subclass of HasType::Type?
def valid_type?
type && type_class.is_a?(Class) &&
type_class.ancestors.include?(HasType::Type)
rescue NameError
false
end
# @return [Class] The class referenced in the `type` column.
# @raise [TypeError] If `type` isn't set.
def type_class
@_has_type_type_class ||=
if self[:type]
"#{type_container}::#{self[:type]}".constantize
else
raise TypeError, "Missing type attribute"
end
end
# @return [String] Either an empty string or the fully-qualified name of a
# class or module that contains all types for this class.
def type_container
if self.class.const_defined?(:TYPE_CONTAINER)
self.class::TYPE_CONTAINER
else
''
end
end
# If `type` is set, run the type's callbacks inside the base class's.
# Note that this overrides a method from ActiveRecord::Callbacks.
def run_callbacks(kind, *args, &block)
if valid_type?
super { self.as_type.run_callbacks(kind, *args, &block) }
else
super
end
end
end
# `HasType::Type` is mostly a simple proxy class, but with the addition of
# ActiveModel/ActiveRecord callbacks and validations. Because of
# `method_missing`, any method calls from inside the proxy end up on the
# underlying object, including calls that happen within ActiveModel.
class Type
extend ActiveModel::Callbacks
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
# Define the same callback methods as ActiveRecord. We don't include
# ActiveRecord::Callbacks directly because it also overrides a bunch of
# persistence methods to run the hooks, which we don't want since
# HasType::Model#run_callbacks will handle that.
define_model_callbacks :initialize, :find, :touch, :only => :after
define_model_callbacks :save, :create, :update, :destroy
def initialize(model)
@model = model
end
def respond_to_missing?(meth_name, incl_private)
@model.respond_to?(meth_name, incl_private)
end
def method_missing(*args, &block)
@model.send(*args, &block)
rescue NoMethodError
super
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment