Created
April 16, 2012 00:47
-
-
Save rf-/2395628 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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