ForestAdmin/forest-rails

View on GitHub
app/serializers/forest_liana/serializer_factory.rb

Summary

Maintainability
F
4 days
Test Coverage
D
60%
require 'jsonapi-serializers'

module ForestLiana
  class SerializerFactory

    def self.define_serializer(active_record_class, serializer)
      serializer_name = self.build_serializer_name(active_record_class)

      if ForestLiana::UserSpace.const_defined?(serializer_name, false)
        ForestLiana::UserSpace.send(:remove_const, serializer_name)
      end

      # NOTICE: Create the serializer in the UserSpace to avoid conflicts with
      # serializer created from integrations, actions, segments, etc.
      ForestLiana::UserSpace.const_set(serializer_name, serializer)
    end

    def self.get_serializer_name(active_record_class)
      if defined?(::Intercom::Conversation) &&
        active_record_class == ::Intercom::Conversation
        "ForestLiana::IntercomConversationSerializer"
      elsif defined?(::Intercom::User) &&
        active_record_class == ::Intercom::User
        "ForestLiana::IntercomAttributeSerializer"
      elsif defined?(::Stripe::Charge) &&
        active_record_class == ::Stripe::Charge
        "ForestLiana::StripePaymentSerializer"
      elsif defined?(::Stripe::Card) &&
        active_record_class == ::Stripe::Card
        "ForestLiana::StripeCardSerializer"
      elsif defined?(::Stripe::Invoice) &&
        active_record_class == ::Stripe::Invoice
        "ForestLiana::StripeInvoiceSerializer"
      elsif defined?(::Stripe::Subscription) &&
        active_record_class == ::Stripe::Subscription
        "ForestLiana::StripeSubscriptionSerializer"
      elsif defined?(::Stripe::BankAccount) &&
        active_record_class == ::Stripe::BankAccount
        "ForestLiana::StripeBankAccountSerializer"
      elsif active_record_class == ForestLiana::Model::Stat
        "ForestLiana::StatSerializer"
      elsif active_record_class == ForestLiana::MixpanelEvent
        "ForestLiana::MixpanelEventSerializer"
      else
        serializer_name = self.build_serializer_name(active_record_class)
        "ForestLiana::UserSpace::#{serializer_name}"
      end
    end

    def initialize(is_smart_collection = false)
      @is_smart_collection = is_smart_collection
    end

    def serializer_for(active_record_class)
      serializer = Class.new {
        include ForestAdmin::JSONAPI::Serializer

        def self_link
          "/forest#{super.underscore}"
        end

        def type
          ForestLiana.name_for(object.class).demodulize
        end

        def format_name(attribute_name)
          attribute_name.to_s
        end

        def unformat_name(attribute_name)
          attribute_name.to_s
        end

        def relationship_self_link(attribute_name)
          nil
        end

        def relationship_related_link(attribute_name)
          ret = {}

          # Has many smart field
          current = self.has_many_relationships[attribute_name]
          if current.try(:[], :options).try(:[], :name) == attribute_name
            ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/#{attribute_name}"
            return ret
          end

          if intercom_integration?
            case attribute_name
            when :intercom_conversations
              ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/intercom_conversations"
            when :intercom_attributes
              ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/intercom_attributes"
            end
          end

          if stripe_integration?
            case attribute_name
            when :stripe_payments
              ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/stripe_payments"
            when :stripe_invoices
              ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/stripe_invoices"
            when :stripe_cards
              ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/stripe_cards"
            when :stripe_subscriptions
              ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/stripe_subscriptions"
            when :stripe_bank_accounts
              ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/stripe_bank_accounts"
            end
          end

          if mixpanel_integration?
            case attribute_name
            when :mixpanel_last_events
              ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/mixpanel_last_events"
            end
          end

          if ret[:href].blank?
            begin
              if @options[:include].try(:include?, attribute_name.to_s)
                object.send(attribute_name)
              end

              SchemaUtils.many_associations(object.class).each do |a|
                if a.name == attribute_name
                  ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/relationships/#{attribute_name}"
                end
              end
            rescue TypeError, ActiveRecord::StatementInvalid, NoMethodError
              puts "Cannot load the association #{attribute_name} on #{object.class.name} #{object.id}."
            end
          end

          ret
        end

        private

        def intercom_integration?
          ForestLiana.integrations
            .try(:[], :intercom)
            .try(:[], :mapping)
            .try(:include?, object.class.name)
        end

        def stripe_integration?
          mapping = ForestLiana.integrations
            .try(:[], :stripe)
            .try(:[], :mapping)

          if mapping
            collection_names = mapping.map do |collection_name_and_field|
              collection_name_and_field.split('.')[0]
            end
            collection_names.include?(object.class.name)
          else
            false
          end
        end

        def mixpanel_integration?
          mapping = ForestLiana.integrations
            .try(:[], :mixpanel)
            .try(:[], :mapping)

          if mapping
            collection_names = mapping.map do |collection_name_and_field|
              collection_name_and_field.split('.')[0]
            end
            collection_names.include?(object.class.name)
          else
            false
          end
        end
      }

      unless @is_smart_collection
        attributes(active_record_class).each do |attribute|
          serializer.attribute(attribute) do |x|
            begin
              object.send(attribute)
            rescue
              nil
            end
          end
        end

        # NOTICE: Format time type fields during the serialization.
        attributes_time(active_record_class).each do |attribute|
          serializer.attribute(attribute) do |x|
            begin
              value = object.send(attribute)
              if value
                match = /(\d{2}:\d{2}:\d{2})/.match(value.to_s)
                (match && match[1]) ? match[1] : nil
              else
                nil
              end
            rescue
              nil
            end
          end
        end

        # NOTICE: Format serialized fields.
        attributes_serialized(active_record_class).each do |attr, serialization|
          serializer.attribute(attr) do |x|
            begin
              value = object.send(attr)
              value ? value.to_json : nil
            rescue
              nil
            end
          end
        end

        # NOTICE: Format CarrierWave url attribute
        if active_record_class.respond_to?(:mount_uploader)
          active_record_class.uploaders.each do |key, value|
            serializer.attribute(key) do |x|
              begin
                object.send(key).try(:url)
              rescue
                nil
              end
            end
          end
        end

        # NOTICE: Format Paperclip url attribute
        if active_record_class.respond_to?(:attachment_definitions)
          active_record_class.attachment_definitions.each do |key, value|
            serializer.attribute(key) do |x|
              begin
                object.send(key)
              rescue
                nil
              end
            end
          end
        end

        # NOTICE: Format ActsAsTaggable attribute
        if active_record_class.try(:taggable?) &&
          active_record_class.respond_to?(:acts_as_taggable) &&
          active_record_class.acts_as_taggable.respond_to?(:to_a)
          active_record_class.acts_as_taggable.to_a.each do |key, value|
            serializer.attribute(key) do |x|
              begin
                object.send(key).map(&:name)
              rescue
                nil
              end
            end
          end
        end

        # NOTICE: Format Devise attributes
        if active_record_class.respond_to?(:devise_modules?)
          serializer.attribute('password') do |x|
            '**********'
          end
        end

        SchemaUtils.associations(active_record_class).each do |a|
          begin
            if SchemaUtils.polymorphic?(a)
              serializer.send(serializer_association(a), a.name) {
                if [:has_one, :belongs_to].include?(a.macro)
                  begin
                    object.send(a.name)
                  rescue ActiveRecord::RecordNotFound
                    nil
                  end
                else
                  []
                end
              }
            elsif SchemaUtils.model_included?(a.klass)
              serializer.send(serializer_association(a), a.name) {
                if [:has_one, :belongs_to].include?(a.macro)
                  begin
                    object.send(a.name)
                  rescue ActiveRecord::RecordNotFound
                    nil
                  end
                else
                  []
                end
              }
            end
          rescue NameError
            # NOTICE: Let this error silent, a bad association warning will be
            # displayed in the schema adapter.
          end
        end
      end

      # Intercom
      if has_intercom_integration?(active_record_class.name)
        serializer.send(:has_many, :intercom_conversations) { }
        serializer.send(:has_many, :intercom_attributes) { }
      end

      # Stripe
      if has_stripe_integration?(active_record_class.name)
        serializer.send(:has_many, :stripe_payments) { }
        serializer.send(:has_many, :stripe_invoices) { }
        serializer.send(:has_many, :stripe_cards) { }
        serializer.send(:has_many, :stripe_subscriptions) { }
        serializer.send(:has_many, :stripe_bank_accounts) { }
      end

      # Mixpanel
      if has_mixpanel_integration?(active_record_class.name)
        serializer.send(:has_many, :mixpanel_last_events) { }
      end

      ForestLiana::SerializerFactory.define_serializer(active_record_class, serializer)

      serializer
    end

    private

    def self.build_serializer_name(active_record_class)
      component_prefix = ForestLiana.component_prefix(active_record_class)
      serializer_name = "#{component_prefix}Serializer"
    end

    def key(active_record_class)
      active_record_class.to_s.tableize.to_sym
    end

    def has_intercom_integration?(collection_name)
      ForestLiana.integrations
        .try(:[], :intercom)
        .try(:[], :mapping)
        .try(:include?, collection_name)
    end

    def has_stripe_integration?(collection_name)
      mapping = ForestLiana.integrations
        .try(:[], :stripe)
        .try(:[], :mapping)

      if mapping
        collection_names = mapping.map do |collection_name_and_field|
          collection_name_and_field.split('.')[0]
        end
        collection_names.include?(collection_name)
      else
        false
      end
    end

    def has_mixpanel_integration?(collection_name)
      mapping = ForestLiana.integrations
        .try(:[], :mixpanel)
        .try(:[], :mapping)

      if mapping
        collection_names = mapping.map do |collection_name_and_field|
          collection_name_and_field.split('.')[0]
        end
        collection_names.include?(collection_name)
      else
        false
      end
    end

    def serializer_association(association)
      case association.macro
      when :has_one, :belongs_to
        :has_one
      when :has_many, :has_and_belongs_to_many
        :has_many
      end
    end

    def attributes(active_record_class)
      return [] if @is_smart_collection

      active_record_class.column_names.select do |column_name|
        !association?(active_record_class, column_name)
      end
    end

    def attributes_time(active_record_class)
      return [] if @is_smart_collection

      active_record_class.column_names.select do |column_name|
        if Rails::VERSION::MAJOR > 4
          active_record_class.column_for_attribute(column_name).type == :time
        else
          active_record_class.column_types[column_name].type == :time
        end
      end
    end

    def attributes_serialized(active_record_class)
      return [] if @is_smart_collection

      if Rails::VERSION::MAJOR >= 5
        attributes(active_record_class).select do |attribute|
          active_record_class.type_for_attribute(attribute).class ==
            ::ActiveRecord::Type::Serialized
        end
      else
        # NOTICE: Silent deprecation warnings for removed
        #         "serialized_attributes" in Rails 5
        ActiveSupport::Deprecation.silence do
          active_record_class.serialized_attributes
        end
      end
    end

    def association?(active_record_class, column_name)
      foreign_keys(active_record_class).include?(column_name)
    end

    def foreign_keys(active_record_class)
      begin
        SchemaUtils.belongs_to_associations(active_record_class).map(&:foreign_key)
        SchemaUtils.belongs_to_associations(active_record_class)
                   .select { |association| !SchemaUtils.polymorphic?(association) }
                   .map(&:foreign_key)
      rescue => err
        # Association foreign_key triggers an error. Put the stacktrace and
        # returns no foreign keys.
        puts err.backtrace
        []
      end
    end
  end
end