From f3bbf0c17b89b754ddc0c4d0f54add83b10eb9c7 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Thu, 25 Jan 2024 14:29:19 -0600 Subject: [PATCH] fix: more flexible polymorphic types lookup (#1434) * fix: more flexible polymorphic types lookup * test: add polymorphic lookup tests they pass on v-11-dev I'm going to look into the existing lookup warnings now ``` [POLYMORPHIC TYPE NOT FOUND] No polymorphic types found for fileable [POLYMORPHIC TYPE] No polymorphic types found for FilePropertiesResource fileable [POLYMORPHIC TYPE NOT FOUND] No polymorphic types found for respondent [POLYMORPHIC TYPE] No polymorphic types found for QuestionResource respondent [POLYMORPHIC TYPE NOT FOUND] No polymorphic types found for respondent [POLYMORPHIC TYPE] No polymorphic types found for AnswerResource respondent [POLYMORPHIC TYPE NOT FOUND] No polymorphic types found for keepable [POLYMORPHIC TYPE] No polymorphic types found for KeeperResource keepable ``` * Revert "test: add polymorphic lookup tests" This reverts commit 0979a7243b6bc816dd2327d3ff23f70209c52dce. * feat: easily clear the lookup * feat: add a descendents strategy * test: polymorphic type lookup * feat: make polymorphic type lookup configurable * feat: clear polymorphic lookup after initialize --- lib/jsonapi/relationship.rb | 2 +- lib/jsonapi/resources/railtie.rb | 4 ++ lib/jsonapi/utils/polymorphic_types_lookup.rb | 71 ++++++++++++++++--- .../utils/polymorphic_types_lookup_test.rb | 35 +++++++++ 4 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 test/unit/utils/polymorphic_types_lookup_test.rb diff --git a/lib/jsonapi/relationship.rb b/lib/jsonapi/relationship.rb index bff416da..1378d8e7 100644 --- a/lib/jsonapi/relationship.rb +++ b/lib/jsonapi/relationship.rb @@ -193,7 +193,7 @@ def polymorphic_type def setup_implicit_relationships_for_polymorphic_types(exclude_linkage_data: true) types = self.class.polymorphic_types(_relation_name) unless types.present? - warn "No polymorphic types found for #{parent_resource.name} #{_relation_name}" + warn "[POLYMORPHIC TYPE] No polymorphic types found for #{parent_resource.name} #{_relation_name}" return end diff --git a/lib/jsonapi/resources/railtie.rb b/lib/jsonapi/resources/railtie.rb index 10fb3c05..02050879 100644 --- a/lib/jsonapi/resources/railtie.rb +++ b/lib/jsonapi/resources/railtie.rb @@ -18,6 +18,10 @@ class Railtie < ::Rails::Railtie ::JSONAPI::MimeTypes.parser.call(body) } end + + initializer "jsonapi_resources.initialize", after: :initialize do + JSONAPI::Utils::PolymorphicTypesLookup.polymorphic_types_lookup_clear! + end end end end diff --git a/lib/jsonapi/utils/polymorphic_types_lookup.rb b/lib/jsonapi/utils/polymorphic_types_lookup.rb index 2a72673e..7457410a 100644 --- a/lib/jsonapi/utils/polymorphic_types_lookup.rb +++ b/lib/jsonapi/utils/polymorphic_types_lookup.rb @@ -5,26 +5,79 @@ module Utils module PolymorphicTypesLookup extend self - def polymorphic_types(name) - polymorphic_types_lookup[name.to_sym] + singleton_class.attr_accessor :build_polymorphic_types_lookup_strategy + self.build_polymorphic_types_lookup_strategy = + :build_polymorphic_types_lookup_from_object_space + + def polymorphic_types(name, rebuild: false) + polymorphic_types_lookup(rebuild: rebuild).fetch(name.to_sym, &handle_polymorphic_type_name_found) + end + + def handle_polymorphic_type_name_found + @handle_polymorphic_type_name_found ||= lambda do |name| + warn "[POLYMORPHIC TYPE NOT FOUND] No polymorphic types found for #{name}" + nil + end end - def polymorphic_types_lookup + def polymorphic_types_lookup(rebuild: false) + polymorphic_types_lookup_clear! if rebuild @polymorphic_types_lookup ||= build_polymorphic_types_lookup end + def polymorphic_types_lookup_clear! + @polymorphic_types_lookup = nil + end + def build_polymorphic_types_lookup - {}.tap do |hash| + public_send(build_polymorphic_types_lookup_strategy) + end + + def build_polymorphic_types_lookup_from_descendants + {}.tap do |lookup| + ActiveRecord::Base + .descendants + .select(&:name) + .reject(&:abstract_class) + .select(&:model_name).map {|klass| + add_polymorphic_types_lookup(klass: klass, lookup: lookup) + } + end + end + + def build_polymorphic_types_lookup_from_object_space + {}.tap do |lookup| ObjectSpace.each_object do |klass| next unless Module === klass - if ActiveRecord::Base > klass - klass.reflect_on_all_associations(:has_many).select { |r| r.options[:as] }.each do |reflection| - (hash[reflection.options[:as]] ||= []) << klass.name.underscore - end - end + next unless ActiveRecord::Base > klass + add_polymorphic_types_lookup(klass: klass, lookup: lookup) end end end + + # TODO(BF): Consider adding the following conditions + # is_active_record_inspectable = true + # is_active_record_inspectable &&= klass.respond_to?(:reflect_on_all_associations, true) + # is_active_record_inspectable &&= format_polymorphic_klass_type(klass).present? + # return unless is_active_record_inspectable + def add_polymorphic_types_lookup(klass:, lookup:) + klass.reflect_on_all_associations(:has_many).select { |r| r.options[:as] }.each do |reflection| + (lookup[reflection.options[:as]] ||= []) << format_polymorphic_klass_type(klass).underscore + end + end + + # TODO(BF): Consider adding the following conditions + # klass.name || + # begin + # klass.model_name.name + # rescue ArgumentError => ex + # # klass.base_class may be nil + # warn "[POLYMORPHIC TYPE] #{__callee__} #{klass} #{ex.inspect}" + # nil + # end + def format_polymorphic_klass_type(klass) + klass.name + end end end end diff --git a/test/unit/utils/polymorphic_types_lookup_test.rb b/test/unit/utils/polymorphic_types_lookup_test.rb new file mode 100644 index 00000000..838ed878 --- /dev/null +++ b/test/unit/utils/polymorphic_types_lookup_test.rb @@ -0,0 +1,35 @@ +require File.expand_path('../../../test_helper', __FILE__) + +class PolymorphicTypesLookupTest < ActiveSupport::TestCase + def setup + JSONAPI::Utils::PolymorphicTypesLookup.polymorphic_types_lookup_clear! + end + + def test_build_polymorphic_types_lookup_from_object_space + expected = { + :imageable=>["product", "document"] + } + actual = JSONAPI::Utils::PolymorphicTypesLookup.build_polymorphic_types_lookup_from_object_space + actual_keys = actual.keys.sort + assert_equal(actual_keys, expected.keys.sort) + actual_keys.each do |actual_key| + actual_values = actual[actual_key].sort + expected_values = expected[actual_key].sort + assert_equal(actual_values, expected_values) + end + end + + def test_build_polymorphic_types_lookup_from_descendants + expected = { + :imageable=>["document", "product"] + } + actual = JSONAPI::Utils::PolymorphicTypesLookup.build_polymorphic_types_lookup_from_descendants + actual_keys = actual.keys.sort + assert_equal(actual_keys, expected.keys.sort) + actual_keys.each do |actual_key| + actual_values = actual[actual_key].sort + expected_values = expected[actual_key].sort + assert_equal(actual_values, expected_values) + end + end +end