Skip to content

Instantly share code, notes, and snippets.

@rlburkes
Last active April 20, 2022 15:10
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rlburkes/798e186acb2f93e787a5 to your computer and use it in GitHub Desktop.
Save rlburkes/798e186acb2f93e787a5 to your computer and use it in GitHub Desktop.
# Wouldn't it be great if you could have STI like functionality
# without needing to encode strings of class names in the database?
# Well today is your lucky day! Discriminable Model is here to help.
#
# Simply specify your models desired type column, and provide a block to
# do the discrimination. If you want the whole STI-esque shebang of properly
# typed finder methods you can supply an array of 'discriminate_types' that will
# be used to apply an appropriate type.
#
# class MyModel < ActiveRecord::Base
# include DiscriminableModel
#
# use_type_column :foobar, discriminate_types: [:rad, :cool] do |my_foobar|
# my_foobar == 'rad' ? RadtasticModel : BoringModel
# end
# end
#
module DiscriminableModel
extend ActiveSupport::Concern
included do
class_attribute :type_column, instance_writer: false
class_attribute :discriminator, instance_writer: false
class_attribute :discriminate_types, instance_writer: false
end
module ClassMethods
def uses_type_column(type_column, discriminate_types: [], &block)
raise 'Subclasses should not override .uses_type_column' unless base_class?
self.type_column = type_column.to_sym
self.discriminator = block
self.discriminate_types = discriminate_types
end
def finder_needs_type_condition?
!base_class? && discriminate_types.present?
end
def sti_name
discriminate_type_for_klass(self)
end
def inheritance_column
type_column.to_s
end
def sti_type?
false
end
private
# We don't want to let this interface attempt to set type on create/new
# the #ensure_proper_type method will handle it for now.
def populatable_scope_attributes
scope_attributes.stringify_keys.reject { |k| k == type_column.to_s }
end
def discriminate_type_for_klass(klass)
discriminate_types.each_with_object(Multimap.new) { |type, types| types[klass_for_type(type)] = type }[klass]
end
def base_class?
self == base_class
end
# calls like Model.find(5) return the correct types.
def discriminate_class_for_record(record)
klass_for_type(record[type_column.to_s])
end
# Creates instances of the appropriate type based on the type attribute. We need to override this so
# calls like create return an appropriately typed model.
def subclass_from_attributes(attributes)
klass = klass_for_type(attributes[type_column])
klass == self ? nil : klass
end
# If attributes exist and contain the type column we want to use the
# attributes to build the custom subtype.
def subclass_from_attributes?(attributes)
attributes.present? && attributes.key?(type_column)
end
def klass_for_type(type)
discriminator.call(type)
end
end
def populate_with_current_scope_attributes
return unless self.class.scope_attributes?
self.class.send(:populatable_scope_attributes).each do |att,value|
send("#{att}=", value) if respond_to?("#{att}=")
end
end
def ensure_proper_type
klass = self.class
if klass.finder_needs_type_condition? && klass.sti_name.count == 1
write_attribute(klass.inheritance_column, klass.sti_name.first)
end
end
end
@cgat
Copy link

cgat commented Mar 15, 2018

This is working well for me on rails 4.2. Have you used this on rails 5.x? Out of curiosity, do you have an unit tests for this?

@Levii01
Copy link

Levii01 commented Aug 5, 2021

Great job! 👏

Small fix Rails 6.1.3.2 when an attribute to discriminate class is empty - for instance when just initialized a new object

    def subclass_from_attributes(attributes)
      klass = klass_for_type(attributes.try(:[], type_column))
      klass == self ? nil : klass
    end

@gregorw
Copy link

gregorw commented Apr 16, 2022

For any new devs stumbling upon this code, you may want to try the new and tested discriminable gem:

gem "discriminable"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment