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

Make Adapters registerable so they are not namespace-constrained #1017

Merged
merged 5 commits into from
Sep 11, 2015

Conversation

bf4
Copy link
Member

@bf4 bf4 commented Jul 17, 2015

Changes:

  • Introduce Adapter::lookup for use by Serializer.adapter
  • Move Adapter-finding logic from Adapter::adapter_class into Adapter::lookup
  • Fix circular load warning as described in cf6a074
  • Allowed for gem devs to use a Gemfile.local for gems not in the gemspec or Gemfile e.g. pry-byebug (which was my usecase).

Background Ref:

Introduced interfaces:

  • non-inherited methods
ActiveModel::Serializer::Adapter.adapter_map  # a Hash<adapter_name, adapter_class>
ActiveModel::Serializer::Adapter.adapters     # an Array<adapter_name>
ActiveModel::Serializer::Adapter.register(name, klass) # adds an adapter to the adapter_map
ActiveModel::Serializer::Adapter.lookup(name_or_klass)    # raises UnknownAdapterError error when adapter not found
  • Automatically register adapters when subclassing
     def self.inherited(subclass)
       ActiveModel::Serializer::Adapter.register(subclass.to_s.demodulize,
subclass)
     end
  • Preserves subclass method ::adapter_class(adapter)
     def self.adapter_class(adapter)
       ActiveModel::Serializer::Adapter.lookup(adapter)
     end
  • Serializer.adapter now uses Adapter.lookup(config.adapter) rather than have
    duplicate logic

TODO

Test

  • ActiveModel::Serializer::Adapter.adapter_map
  • ActiveModel::Serializer::Adapter.adapters
  • ActiveModel::Serializer::Adapter.register(name, klass)
  • ActiveModel::Serializer::Adapter.get(name_or_klass)
  • ActiveModel::Serializer::Adapter::inherited
  • ActiveModel::Serializer.adapter

TO CONSIDER

  • wrap 'finding adapter by name' logic outside of 'get' method
  • Change exception from ArgumentError to UnknownAdapter

TODO in future PR:

  • Change Adapter to module (backwards compatible public methods) and make Adapter::Base or Adapter::AbstractClass

Full commit message from cf6a074

Ensure inheritance hooks run
I was seeing transient failures where adapters may not be registered.

e.g. https://travis-ci.org/rails-api/active_model_serializers/builds/77735382

Since we're using the Adapter, JsonApi, and Json classes
as namespaces, some of the conventions we use for modules don't apply.
Basically, we don't want to define the class anywhere besides itself.
Otherwise, the inherited hooks may not run, and some adapters may not
be registered.

For example:

If we have a class Api `class Api; end`
And Api is also used as a namespace for `Api::Product`
And the classes are defined in different files.

In one file:

```ruby
class Api
  autoload :Product
  def self.inherited(subclass)
    puts
    p [:inherited, subclass.name]
    puts
  end
end
```

And in another:

```ruby
class Api
  class Product < Api
    def sell_sell_sell!
      # TODO: sell
    end
  end
end
```

If we load the Api class file first, the inherited hook will be defined on the class
so that when we load the Api::Product class, we'll see the output:

```plain
[ :inherited, Api::Product]
```

However, if we load the Api::Product class first, since it defines the `Api` class
and then inherited from it, the Api file was never loaded, the hook never defined,
and thus never run.

By defining the class as `class Api::Product < Api` We ensure the the Api class
MUST be defined, and thus, the hook will be defined and run and so sunshine and unicorns.

Appendix:

The below would work, but triggers a circular reference warning.
It's also not recommended to mix require with autoload.

```ruby
require 'api'
class Api
  class Product < Api
    def sell_sell_sell!
      # TODO: sell
    end
  end
end
```

This failure scenario was introduced by removing the circular reference warnings in
#1067

@bf4 bf4 force-pushed the registerable_adapters branch 3 times, most recently from a19096d to 0d617cd Compare August 21, 2015 02:36
@bf4
Copy link
Member Author

bf4 commented Aug 21, 2015

@joaomdmoura Ready for review!

klass = ActiveModel::Serializer::Adapter.adapter_class(:json_simple)
assert_nil klass
end

Copy link
Member Author

Choose a reason for hiding this comment

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

moved to adapter_for file

@bf4 bf4 force-pushed the registerable_adapters branch 3 times, most recently from 92df577 to 8da44d7 Compare August 21, 2015 06:01
@@ -11,6 +13,64 @@ class Adapter

attr_reader :serializer

# Only the Adapter class has these methods.
# None of the sublasses have them.
Copy link
Member

Choose a reason for hiding this comment

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

👍

@bf4
Copy link
Member Author

bf4 commented Aug 28, 2015

rebased off of master...

@bf4
Copy link
Member Author

bf4 commented Aug 28, 2015

You'll likely want to take a look at the commit message in cf6a074

@bf4 bf4 mentioned this pull request Aug 30, 2015
super
@result
end
class ActiveModel::Serializer::Adapter::FlattenJson < ActiveModel::Serializer::Adapter::Json

Choose a reason for hiding this comment

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

I find the previous style more readable. That's also the syntax (I think) Rails uses internally, perhaps it would be a good idea to align with Rails code style.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Ruby Style Guide encourages the previous style

Copy link
Member Author

Choose a reason for hiding this comment

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

even for classes as namespaces? link plz? (besides that it doesn't work in this case, I think it's wrong)

ADAPTER_MAP
end

# @return Array<adapter_name>
Copy link
Contributor

Choose a reason for hiding this comment

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

Better:

@return [Array<Symbol>] list of adapter names

See http://yardoc.org/types.html

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, just me being a little lazy because we're not actually using yardoc (yet?)

bf4 added a commit to bf4/active_model_serializers that referenced this pull request Sep 9, 2015
Per rails-api#1017 (comment)
comment by sandstrom in discussion of the inherited hook

> I'm thinking that it would be better to register adapters manually, without using the hook, i.e.
> have people call ActiveModel::Serializer::Adapter.register directly (perhaps in an initializer).

> Possibly, some inspiration can be taken from how ActiveJob adapters are wired[1].

> [1] https://github.com/rails/rails/blob/master/activejob/lib/active_job/queue_adapter.rb
Changes:

- Introduce Adapter::get for use by Serializer.adapter
- Move Adapter-finding logic from Adapter::adapter_class into Adapter::get

Introduced interfaces:

- non-inherited methods
```ruby
ActiveModel::Serializer::Adapter.adapter_map     # a Hash<adapter_name, adapter_class>
ActiveModel::Serializer::Adapter.adapters        # an Array<adapter_name>
ActiveModel::Serializer::Adapter.register(name, klass) # adds an adapter to the adapter_map
ActiveModel::Serializer::Adapter.get(name_or_klass)    # raises Argument error when adapter not found
```

- Automatically register adapters when subclassing

```ruby
      def self.inherited(subclass)
        ActiveModel::Serializer::Adapter.register(subclass.to_s.demodulize, subclass)
      end
```

- Preserves subclass method `::adapter_class(adapter)`

```ruby
      def self.adapter_class(adapter)
        ActiveModel::Serializer::Adapter.get(adapter)
      end
```

- Serializer.adapter now uses `Adapter.get(config.adapter)` rather than have duplicate logic
bf4 added a commit to bf4/active_model_serializers that referenced this pull request Sep 9, 2015
Per rails-api#1017 (comment)
comment by sandstrom in discussion of the inherited hook

> I'm thinking that it would be better to register adapters manually, without using the hook, i.e.
> have people call ActiveModel::Serializer::Adapter.register directly (perhaps in an initializer).

> Possibly, some inspiration can be taken from how ActiveJob adapters are wired[1].

> [1] https://github.com/rails/rails/blob/a11571cec3213753d63ac3e6b4bb3b97fe2594a6/activejob/lib/active_job/queue_adapter.rb#L52-L56
bf4 added a commit to bf4/active_model_serializers that referenced this pull request Sep 9, 2015
Per rails-api#1017 (comment)
comment by sandstrom in discussion of the inherited hook

> I'm thinking that it would be better to register adapters manually, without using the hook, i.e.
> have people call ActiveModel::Serializer::Adapter.register directly (perhaps in an initializer).

> Possibly, some inspiration can be taken from how ActiveJob adapters are wired[1].

> [1] https://github.com/rails/rails/blob/a11571cec3213753d63ac3e6b4bb3b97fe2594a6/activejob/lib/active_job/queue_adapter.rb#L52-L56
@bf4
Copy link
Member Author

bf4 commented Sep 9, 2015

@joaomdmoura @bolshakov @sandstrom All cleaned up!

@sandstrom
Copy link

@bf4 Awesome work on this!

While we disagreed on some of the details, I'm overall very positive to making adapters more flexible. ⛵

@bolshakov
Copy link
Contributor

👍

@bf4
Copy link
Member Author

bf4 commented Sep 9, 2015

Failures on appveyor

 1) Failure:
ActiveModel::Serializer::AdapterForTest#test_adapters [C:/projects/active-model-serializers/test/serializers/adapter_for_test.rb:73]:
--- expected
+++ actual
@@ -1 +1 @@
-["flatten_json", "json", "json_api"]
+["flatten_json", "json", "json_api", "null"]
 1) Failure:
ActionController::Serialization::ImplicitSerializerTest#test_cache_expiration_on_update [C:/projects/active-model-serializers/test/action_controller/serialization_test.rb:394]:
--- expected
+++ actual
@@ -1 +1 @@
-"{\"id\":1,\"title\":\"ZOMG a New Post\",\"body\":\"Body\",\"comments\":[{\"id\":1,\"body\":\"ZOMG A COMMENT\"}],\"blog\":{\"id\":999,\"name\":\"Custom blog\"},\"author\":{\"id\":1,\"name\":\"Joao Moura.\"}}"
+"{\"id\":1,\"title\":\"New Post\",\"body\":\"Body\",\"comments\":[{\"id\":1,\"body\":\"ZOMG A COMMENT\"}],\"blog\":{\"id\":999,\"name\":\"Custom blog\"},\"author\":{\"id\":1,\"name\":\"Joao Moura.\"}}"

I was seeing transient failures where adapters may not be registered.

e.g. https://travis-ci.org/rails-api/active_model_serializers/builds/77735382

Since we're using the Adapter, JsonApi, and Json classes
as namespaces, some of the conventions we use for modules don't apply.
Basically, we don't want to define the class anywhere besides itself.
Otherwise, the inherited hooks may not run, and some adapters may not
be registered.

For example:

If we have a class Api `class Api; end`
And Api is also used as a namespace for `Api::Product`
And the classes are defined in different files.

In one file:

```ruby
class Api
  autoload :Product
  def self.inherited(subclass)
    puts
    p [:inherited, subclass.name]
    puts
  end
end
```

And in another:

```ruby
class Api
  class Product < Api
    def sell_sell_sell!
      # TODO: sell
    end
  end
end
```

If we load the Api class file first, the inherited hook will be defined on the class
so that when we load the Api::Product class, we'll see the output:

```plain
[ :inherited, Api::Product]
```

However, if we load the Api::Product class first, since it defines the `Api` class
and then inherited from it, the Api file was never loaded, the hook never defined,
and thus never run.

By defining the class as `class Api::Product < Api` We ensure the the Api class
MUST be defined, and thus, the hook will be defined and run and so sunshine and unicorns.

Appendix:

The below would work, but triggers a circular reference warning.
It's also not recommended to mix require with autoload.

```ruby
require 'api'
class Api
  class Product < Api
    def sell_sell_sell!
      # TODO: sell
    end
  end
end
```

This failure scenario was introduced by removing the circular reference warnings in
rails-api#1067

Style note:

To make diffs on the adapters smalleer and easier to read, I've maintained the same
identention that was in the original file.  I've decided to prefer ease of reading
the diff over style, esp. since we may later return to the preferred class declaration style.

 with '#' will be ignored, and an empty message aborts the commit.
Per rails-api#1017 (comment)
comment by sandstrom in discussion of the inherited hook

> I'm thinking that it would be better to register adapters manually, without using the hook, i.e.
> have people call ActiveModel::Serializer::Adapter.register directly (perhaps in an initializer).

> Possibly, some inspiration can be taken from how ActiveJob adapters are wired[1].

> [1] https://github.com/rails/rails/blob/a11571cec3213753d63ac3e6b4bb3b97fe2594a6/activejob/lib/active_job/queue_adapter.rb#L52-L56
@bf4
Copy link
Member Author

bf4 commented Sep 9, 2015

Fixed the build

@joaomdmoura
Copy link
Member

Nice work! Really thank you everyone for helping out with this one!
It's a big one and will definitely help us into building new functionalities.
Merging.

joaomdmoura added a commit that referenced this pull request Sep 11, 2015
Make Adapters registerable so they are not namespace-constrained
@joaomdmoura joaomdmoura merged commit 4399c1a into rails-api:master Sep 11, 2015
@@ -62,6 +60,12 @@ def resource_identifier_type_for(serializer)
end
end

def add_relationships(resource, name, serializers)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why, why, why? :D

Copy link
Member Author

Choose a reason for hiding this comment

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

Rebase foul

beauby added a commit to beauby/active_model_serializers that referenced this pull request Sep 14, 2015
beauby added a commit to beauby/active_model_serializers that referenced this pull request Sep 14, 2015
NullVoxPopuli added a commit that referenced this pull request Sep 14, 2015
Remove legacy method accidentally reintroduced in #1017
andrewle added a commit to andrewle/active_model_serializers that referenced this pull request Sep 17, 2015
* master: (170 commits)
  Comment private accessor warnings
  Distinguish options ivar from local; Extract latent Adapter::CachedSerializer
  Fix or skip appveyor failure on cache expiration
  Fixing the travis build svg to amster
  updating version to new release
  Remove duplicate test helper
  outside controller use tutorial
  rubocop-fixes
  Remove unnecessary parentheses accidentally reintroduced in rails-api#1017.
  Remove legacy method accidentally reintroduced in rails-api#1017.
  Update README with nested included association example.
  Split `serializable_hash` into two methods.
  Refactor `add_links` in JSONAPI adapter.
  Fix Markdown to adapters documentation
  Extended format for JSONAPI `include` option.
  Updating wording on cache expiry in README
  Fix typo in fieldset exception
  Fixed indentation in readme under 'using without render'
  Documentation for serializing resources without render
  Get rid of unnecessary instance variables, and implied dependencies.
  ...

Conflicts:
	lib/active_model/serializer/adapter/json_api.rb
@bf4 bf4 deleted the registerable_adapters branch August 31, 2016 06:19
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.

None yet

5 participants