Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespaced serializer lookup for associations #1196

Closed
wants to merge 11 commits into from

Conversation

beauby
Copy link
Contributor

@beauby beauby commented Sep 24, 2015

Given the following situation:

module MyNamespace
  class MyModel < ActiveRecord::Base
    has_many :my_related_models
  end
  class MyRelatedModel < ActiveRecord::Base
    ...
  end
end

module MyNamespace
  class MyModelSerializer < ActiveModel::Serializer
    has_many :my_related_models
    ...
  end
  class MyRelatedModelSerializer < ActiveModel::Serializer
    ...
  end
end

AMS currently has troubles inferring the right serializer class for MyRelatedModel (it should be MyNamespace::MyRelatedModelSerializer, but it only looks up MyRelatedModelSerializer). This PR fixes that by keeping trace of the "root" serializer, that is the first non-ArraySerializer serializer in the chain of nested serializers, and looking up for a serializer in that "root serializer"'s namespace. Especially cool nor polymorphic associations in the namespaced scenario.

Solves stuff such as #886, #916, #879 but without having to manually specify the namespace.

Based on #1193 because it sort of paved the way.

[TODO] Add some tests.

@@ -108,20 +110,29 @@ def self.digest_caller_file(caller_line)
Digest::MD5.hexdigest(serializer_file_contents)
end

def self.get_serializer_for(klass)
def self.get_serializer_for(klass, parent_serializer)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add some yardoc to various parts of this method? explaining what each section does, and what scenarios will be applicable to each block of code?

@NullVoxPopuli
Copy link
Contributor

I like this overall, but here is a scenario, which I'd like your opinion on:

rather than MyNamespace::MyModel correlating to MyNamespace::MyModelSerializer, If someone wanted to use a different set of serializers for different versions of their API, like desiring API::V1::MyModelsController to use API::V1::MyModelSerializer, do you think it would be too complicated to support the namespace lookup on both the model and the controller? (always falling back to MyModelSerializer, of course)

Back when I was at my previous company, the lack of controller namespacing was the one thing I didn't like about AMS. ha :-) (there was a requirement to have a versioned API, which it turned out ended up not being all that important :-\ )

But anyway, just wanted your thoughts on controller namespacing as well - but maybe that should be in an RFC

@beauby
Copy link
Contributor Author

beauby commented Sep 24, 2015

@NullVoxPopuli Just added the controller namespace lookup.

serializer_class ||= namespaced_serializer_class_name.safe_constantize
elsif controller
controller_namespaced_serializer_class_name = "#{controller.class.name.deconstantize}::#{serializer_class_name}"
serializer_class ||= controller_namespaced_serializer_class_name.safe_constantize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if this serializer isn't found? like, what if someone wants to use a straight up PostSerializer in /blog/id/posts?

tests for this would be great :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I use ||=. What this piece does is look for a RelatedSerializer serializer in:

  • CurrentSerializer::RelatedSerializer (which would be ActiveModel::Serializer::RelatedSerializer if it is not actually a related serializer, but that would fail so it's ok)
  • revert to My::Name::Space::RelatedSerializer if called from My::Name::Space::CurrentSerializer
  • or revert to My::Controller::Name::Space::RelatedSerializer if called from My::Controller::Name::Space::SomeController
  • finally, revert to ::RelatedSerializer if none of the above exist

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part could clearly be improved though. Suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh ok, I thought it was going the other way (if RelatedSerializer isn't found, try the namespaced version)

in any case, tests for this new logic would be stellar.

chain = []

# Look for a serializer nested inside the current serializer first, if inside a user-defined serializer
chain.push("#{self}::#{serializer_class_name}") if self.class != ActiveModel::Serializer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That kind of check makes me believe it might be time to make serializer_for an instance method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been around for a while, so I'd rather deprecate it

@NullVoxPopuli
Copy link
Contributor

thanks for the docs!

@beauby
Copy link
Contributor Author

beauby commented Sep 24, 2015

Tests fail because of the lack of lazy enumerators in ruby 1.9.3.

@@ -25,7 +25,8 @@ def get_serializer(resource, options = {})
"Please pass 'adapter: false' or see ActiveSupport::SerializableResource.new"
options[:adapter] = false
end
serializable_resource = ActiveModel::SerializableResource.new(resource, options)
serializable_options = options.merge(controller: self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not mutate?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also why change options when not serializing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and do we need the whole controller? I'd rather not and looks like you just want the class name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we just need the class name but there's virtually 0 overhead to getting the instance, so why not?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you make it easy to use the controller directly, people will, and the scope will creep and and new ams will gain responsibilities it shouldn't gave and cause pain and bugs to users and maintainers

Failure of Interface segregation, not keepig it narrow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, the real issue lies in the fact that we miss a layer of separation between the serializer definition and the serializer instances.

@itsmechlark
Copy link

+1

@@ -16,7 +16,9 @@ Features:
- [#1172](https://github.com/rails-api/active_model_serializers/pull/1172) Better serializer registration, get more than just the first module (@bf4)
- [#1158](https://github.com/rails-api/active_model_serializers/pull/1158) Add support for wildcards in `include` option (@beauby)
- [#1127](https://github.com/rails-api/active_model_serializers/pull/1127) Add support for nested
associations for JSON and Attributes adapters via the `include` option (@NullVoxPopuli, @beauby).
associations for JSON and Attributes adapters via the `include` option (@NullVoxPopuli, @beauby)
- [#1193](https://github.com/rails-api/active_model_serializers/pull/1193) Add support for inline nested serializers (@beauby)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1193?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is based on #1193, that's why.

@bf4
Copy link
Member

bf4 commented Oct 2, 2015

Example test case

module ActiveModel
  class Serializer
    class SerializerForTest < Minitest::Test
      class Tweet < Model; end
      class Share < Model; end
      class TweetSerializer < ActiveModel::Serializer
        attributes :id, :body, :date
        has_many :shares
        class ShareSerializer < ActiveModel::Serializer
          attributes :id, :platform, :date
        end
      end
      class ShareSerializer < ActiveModel::Serializer
        attributes :id, :platform, :date
      end

      def test_serializer_for_namespaced_serializers
        tweet = Tweet.new
        tweet.shares << share = Share.new
        # called from the has_many :shares on TweetSerializer 
        # when passed a tweet with shares, which returns an ArraySerializer
        # which uses the TweetSerializer::ShareSerializer on each share
        assert_equal(TweetSerializer.serializer_for(share), TweetSerializer::ShareSerializer)
        assert_equal(Serializer.serializer_for(share), ShareSerializer)
        # assert_equal(HasManyReflection.new(:shares, {}).build_association(tweet_serializer, tweet_serializer.instance_options), TweetSerializer::ShareSerializer)
      end

      class ActiveModel::Serializer

        def self.serializer_for(resource)
          serializers_cache.fetch_or_store(klass) do
            serializer_class_name = "#{klass.name}Serializer"
            tries = 0
            serializer_base_class = self
            serializer_class = nil
            begin
              serializer = serializer_base_class.constants.find {|constant| constant.to_s == serializer_class_name}
              # serializer_class = serializer_class_name.safe_constantize # or whatever
              if serializer
                serializer_class = const_get(:"#{serializer}")
              elsif tries < 1
                tries = 1
                serializer_base_class = Serializer # or some better logic to figure out the nesting, like the amazing `klass.to_s.split('::')[0..-2].join('::').constantize` or using nesting...
                retry # just being lazy for now
              end
            end

            if serializer_class
              serializer_class
            elsif klass.superclass
              get_serializer_for(klass.superclass)
            else
              nil
            end
          end
          class Reflection

            def build_association(subject, parent_serializer_options)
            association_value = subject.send(name)
            # ? association_value = subject.public_send(name)
            # ? association_value = subject.read_attribute_for_serialization(name)
            reflection_options = options.dup
            # serializer_class = ActiveModel::Serializer.serializer_for(association_value, reflection_options)
            serializer_class = subject.class.serializer_for(association_value, reflection_options)
            # ... etc
          end

          end

        end             
      end

    end
  end
end

drafted, not tested

@bf4
Copy link
Member

bf4 commented Oct 2, 2015

Closed by #1029 ?

@beauby
Copy link
Contributor Author

beauby commented Oct 5, 2015

Closing this in favor of #1225/#1226.

@bf4
Copy link
Member

bf4 commented Jan 15, 2016

ref: #1436

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

Successfully merging this pull request may close these issues.

4 participants