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

(WIP) Fix Serializer Lookup Chain for Namespaces #1883

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

(WIP) Fix Serializer Lookup Chain for Namespaces #1883

wants to merge 3 commits into from

Conversation

thewatts
Copy link

Purpose

This fixes dynamic lookup of namespaced serializers through relationships.

Changes

This adjusts the internal API of the serializer method used for dynamic lookup by adding an additional constant string to the lookup chain used to find the related serializer.

Caveats

No tests. I'm a testing guy - so, if someone can point me to where I should be testing this in the project, I will 100% write them.

I'm guessing I'll need to start with test/serializers/associations_test.rb

Also - this is currently only working for a single nested resource, I'll adjust in short order to provide a dynamic amount of namespaced items.

Related GitHub issues

Not sure

Additional helpful information

-- The Problem:

Currently, when using a relationship from within a namespace, ex:

module Api
  class LessonSerializer < ApplicationSerializer
    belongs_to :trait
  end
end

The method to fetch a serializer builds a lookup chain of possible
constant names that gets looped over with safe_constantize. When using
relationships from within a namespace (like above), the lookup chain
would return this:

[
  "Api::LessonSerializer::TraitSerializer",
  "::TraitSerializer",
]

This works fine the first time, as calling .safe_constantize on
"Api::LessonSerializer::TraitSerializer" returns the correct constant
in this case: Api::TraitSerializer.

However, the second time that the lookup has to happen (from a
different relationship), ex:

module Api
  class CourseSerializer < ApplicationSerializer
    belongs_to :trait
  end
end

The lookup chain returns a similar response:

[
  "Api::CourseSerializer::TraitSerializer",
  "::TraitSerializer",
]

But at this point, calling .safe_constantize on
"Api::CourseSerializer::TraitSerializer" returns nil.

Here's an example from the Rails console:
safe_constantize example

It looks like the main problem lies in this issue here
(many thanks to @willcosgrove for finding it!)

So this means that calling .safe_constantize on the same name string
returns the constant the first time (based on
ActiveSupport::Dependencies), but the second time it gets called on
the same string it returns nil.

safe_constantize of the same string

-- The Fix:

I get the namespace of the name of the parent serializer, and use that
to also build and add a string to the lookup chain array.

So in our first example above, it adds: "Api::TraitSerializer" to the
lookup chain, allowing the serializer to be found every time.

@mention-bot
Copy link

@thewatts, thanks for your PR! By analyzing the annotation information on this pull request, we identified @bf4, @beauby and @steveklabnik to be potential reviewers

-- The Problem:

Currently, when using a relationship from within a namespace, ex:

```ruby
module Api
  class LessonSerializer < ApplicationSerializer
    belongs_to :trait
  end
end
```

The method to fetch a serializer builds a lookup chain of possible
constant names that gets looped over with `safe_constantize`. When using
relationships from within a namespace (like above), the lookup chain
would return this:

```ruby
[
  "Api::LessonSerializer::TraitSerializer",
  "::TraitSerializer",
]
```

This works fine the first time, as calling `.safe_constantize` on
`"Api::LessonSerializer::TraitSerializer"` returns the correct constant
in this case: `Api::TraitSerializer`.

However, the _second_ time that the lookup has to happen (from a
different relationship), ex:

```ruby
module Api
  class CourseSerializer < ApplicationSerializer
    belongs_to :trait
  end
end
```

The lookup chain returns a similar response:

```ruby
[
  "Api::CourseSerializer::TraitSerializer",
  "::TraitSerializer",
]
```

But at this point, calling `.safe_constantize` on
`"Api::CourseSerializer::TraitSerializer"` returns `nil`.

Here's an example from the Rails console:
![safe_constantize example](https://thewatts.s3-us-west-1.amazonaws.com/screenshots/Screen-Shot-2016-08-16-10-45-05.png)

It looks like the main problem [lies in this issue here](rails/rails#9286)
(many thanks to @willcosgrove for finding it!)

So this means that calling `.safe_constantize` on the same name string
returns the constant the first time (based on
`ActiveSupport::Dependencies`), but the _second_ time it gets called on
the same string it returns `nil`.

![safe_constantize of the same string](https://thewatts.s3-us-west-1.amazonaws.com/screenshots/Screen-Shot-2016-08-16-10-53-15.png)

-- The Fix:

I get the namespace of the `name` of the parent serializer, and use that
to also build and add a string to the lookup chain array.

So in our first example above, it adds: `"Api::TraitSerializer"` to the
lookup chain, allowing the serializer to be found every time.
@NullVoxPopuli
Copy link
Contributor

NullVoxPopuli commented Aug 16, 2016

Hey @thewatts thanks for your PR!

There have actually been many approaches to this before:

My favorite: #1757 (bias though)
#1436
#1338
#1295
#1196
#1157

I think that rails issue you linked to comes down to how ruby does constant lookup in an auto-loading environment (rails) http://cirw.in/blog/constant-lookup.html

That said, for tests, I'd would add to the main AM::Serializer tests and add tests for the look up chain method

@NullVoxPopuli
Copy link
Contributor

I suppose you'd also wasn't to add integration tests to action controller tests somewhere. Maybe a new file for lookup?

@thewatts
Copy link
Author

@NullVoxPopuli thanks for the reply!

And sorry for the late reply on my end - that's quite the chain of issues/PRs to follow.
(I'll admit - I skimmed to get the gist).

Honestly, dynamically defining namespace by controller (or through configuration as in your PR) hadn't crossed my mind.

I had just noticed that the dynamic lookup of some belongs_to relationships were failing when two different serializers shared the same relationship resource.

I've looked at making things more dynamic, so that deeper nested items could be found correctly (ie: Api::V1::SuperDuper::TraitSerializer). I've added the commit to this PR.

I can see the desire for configuration though, especially for relationships where you have a V2 serializer that needs to pull a serializer relationship from a V1 type.

Ex:

module Api
  module V1
    class TraitSerializer < ApplicationSerializer
    end
  end
end

module Api
  module V2
    class LessonSerializer < ApplicationSerializer
      belongs_to :trait
    end
  end
end

In that case - Api::V1::TraitSerializer wouldn't be found.


Though my additions create a quick fix (at least to my own problem) - it looks like there's already some solid direction going with those other PRs.

Has there been a consensus on the final direction? If so - I won't be offended if this PR is closed ;)

@thewatts
Copy link
Author

@NullVoxPopuli if this PR is helpful, I absolutely don't mind adding tests (and of course - fixing the one I just broke 😅 )

tmp_namespace = name.to_s
name_namespaces = [tmp_namespace]

until tmp_namespace.empty?
Copy link
Contributor

Choose a reason for hiding this comment

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

do you need to have two loops here? I imagine you could add to the chain array as you deconstantized the namespaces.

Could you also add some documentation (comments) in here explaining how the chain array looks after different steps of this process?

Copy link
Author

Choose a reason for hiding this comment

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

@NullVoxPopuli Good call! Thanks for the feedback!

Adjustments added in most recent commit. Let me know your thoughts!

@NullVoxPopuli
Copy link
Contributor

NullVoxPopuli commented Aug 16, 2016

@thewatts I think this is def worth while (it's why I've worked on it a bit). It's also essential for API versioning. (for that, I'd also add prior versions to the lookup chain)

I can't remember what the hesitation is in changing this is though. Maybe @bf4 can speak more to that.

@thewatts
Copy link
Author

@NullVoxPopuli for the lookup chain and versioning... would we try to specifically look for versioned namespaces?

ex: if V2 exists, assume V1 does as well? That could be tricky, since technically relationships could work both ways. V1 of a Serializer starts using a V2 of another.


chain
if self != ActiveModel::Serializer
until parent_namespace.empty?
Copy link
Contributor

Choose a reason for hiding this comment

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

This is getting there! Why not use parent namespace.each?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, I think using .each would turn this in to too much work. Haha

Copy link
Author

Choose a reason for hiding this comment

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

We could def use .each if we split the string on :: - but we'd have to build the namespaces a little differently.

I'm game for either.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the way you have it may be the cleanest.
Just trying to think of a way it could possibly throw an exception.. (I haven't thought of one so far)

@NullVoxPopuli
Copy link
Contributor

Could you add a change log entry as well?
Thanks!

chain.push("#{resource_namespace}::#{serializer_class_name}")
# Loops over a namespaced Serializer's name to add multiple
# namespaced serializer candidates for lookup in the `lookup_chain` from
# the "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.

This is the kind of documentation I'd like to see all over AMS :-)

Copy link
Author

Choose a reason for hiding this comment

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

Yeah it's a tough balance - solid documentation can be a bonus - but it's the first thing to become stale unless tightly monitored.

Typically I try to have the code be self documenting (so to speak), but at this point we're deep into the belly of the beast :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, everyone's goal is self-documenting code. But as long as churn is low, documentation is great to have. I don't see serializer lookup changing very often :-)

@bf4
Copy link
Member

bf4 commented Aug 17, 2016

Just wanna let you all know I'm aware of this but haven't yet looked at it :)

serializer_class_name = "#{resource_class_name}Serializer"

chain.push("#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer
chain.push("#{resource_namespace}::#{serializer_class_name}")
# Loops over a namespaced Serializer's name to add multiple
Copy link
Member

Choose a reason for hiding this comment

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

This kind of comment should be above the method definition, no?

@NullVoxPopuli
Copy link
Contributor

is this still an issue on master?

@thewatts
Copy link
Author

@NullVoxPopuli - I see that you just pushed an update here, #1968.

This specific PR was for trying to auto-build the namespace lookup chain used during declarations in the Serializer classes.

ex:

module Api
  module V1
    class AuthorSerializer < ApplicationSerializer
      has_many :posts # this would fail to fine the correct serializer

My apologies for not staying on top of it - shortly after the PR was submitted - I found out I was losing my job @ my employer (cutbacks). I've since been in the transition to other employment.

It looks like your PR adds the ability to specify the namespace directly in a before_action callback which may render the needs for this PR obsolete.

If you still want it in master - I can rebase off of master and make the necessary adjustments, add tests, and update the Changelog (will take me a week or so to get the available time).

If not - no worries 😄

@NullVoxPopuli
Copy link
Contributor

sorry for your loss -- company cut backs are never fun -- even for those that stay.

Anywho, for this PR, I'd like to see some tests so that we know exactly what situation your PR addresses.

but being able to specify a namespace hopefully will help a ton of people.

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