Skip to content

Support automatic pluralization of types #376

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

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 94 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ Fast JSON API serialized 250 records in 3.01 ms
* [Object Serialization](#object-serialization)
* [Compound Document](#compound-document)
* [Key Transforms](#key-transforms)
* [Pluralize Type](#pluralize-type)
* [Collection Serialization](#collection-serialization)
* [Caching](#caching)
* [Params](#params)
* [Conditional Attributes](#conditional-attributes)
* [Conditional Relationships](#conditional-relationships)
* [Sparse Fieldsets](#sparse-fieldsets)
* [Using helper methods](#using-helper-methods)
* [Contributing](#contributing)


Expand Down Expand Up @@ -151,7 +153,7 @@ json_string = MovieSerializer.new(movie).serialized_json
```

### Key Transforms
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform:

```ruby
class MovieSerializer
Expand All @@ -160,7 +162,7 @@ class MovieSerializer
set_key_transform :camel
end
```
Here are examples of how these options transform the keys
Here are examples of how these options transform the keys.

```ruby
set_key_transform :camel # "some_key" => "SomeKey"
Expand All @@ -169,6 +171,29 @@ set_key_transform :dash # "some_key" => "some-key"
set_key_transform :underscore # "some_key" => "some_key"
```

### Pluralize Type

By default fast_jsonapi does not pluralize type names. You can turn pluralization on using this syntax:

```ruby
class AwardSerializer
include FastJsonapi::ObjectSerializer
belongs_to :actor
pluralize_type true # "award" => "awards"
end
```

Relationship types are not automatically pluralized, even when their base types have `pluralize_type` set. Pluralization can be enabled in the relationship definition.

```ruby
class ActorSerializer
include FastJsonapi::ObjectSerializer
has_many :awards, pluralize_type: true # "award" => "awards"
end
```

The most common use case for this feature is to easily migrate from serialization engines that pluralize by default, such as AMS.

### Attributes
Attributes are defined in FastJsonapi using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute.

Expand Down Expand Up @@ -276,7 +301,12 @@ This will create a `self` reference for the relationship, and a `related` link f
### Meta Per Resource

For every resource in the collection, you can include a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.


```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer

meta do |movie|
{
years_since_release: Date.current.year - movie.year
Expand Down Expand Up @@ -444,6 +474,68 @@ serializer = MovieSerializer.new(movie, { fields: { movie: [:name] } })
serializer.serializable_hash
```

### Using helper methods

You can mix-in code from another ruby module into your serializer class to reuse functions across your app.

Since a serializer is evaluated in a the context of a `class` rather than an `instance` of a class, you need to make sure that your methods act as `class` methods when mixed in.


##### Using ActiveSupport::Concern

``` ruby

module AvatarHelper
extend ActiveSupport::Concern

class_methods do
def avatar_url(user)
user.image.url
end
end
end

class UserSerializer
include FastJsonapi::ObjectSerializer

include AvatarHelper # mixes in your helper method as class method

set_type :user

attributes :name, :email

attribute :avatar do |user|
avatar_url(user)
end
end

```

##### Using Plain Old Ruby

``` ruby
module AvatarHelper
def avatar_url(user)
user.image.url
end
end

class UserSerializer
include FastJsonapi::ObjectSerializer

extend AvatarHelper # mixes in your helper method as class method

set_type :user

attributes :name, :email

attribute :avatar do |user|
avatar_url(user)
end
end

```

### Customizable Options

Option | Purpose | Example
Expand Down
23 changes: 22 additions & 1 deletion lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def inherited(subclass)
subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
subclass.transform_method = transform_method
subclass.pluralized_type = pluralized_type
subclass.cache_length = cache_length
subclass.race_condition_ttl = race_condition_ttl
subclass.data_links = data_links.dup if data_links.present?
Expand Down Expand Up @@ -153,6 +154,17 @@ def set_key_transform(transform_name)
end
end

def pluralize_type(pluralize)
self.pluralized_type = pluralize

# ensure that the record type is correctly transformed
if record_type
set_type(record_type)
elsif reflected_record_type
set_type(reflected_record_type)
end
end

def run_key_transform(input)
if self.transform_method.present?
input.to_s.send(*@transform_method).to_sym
Expand All @@ -161,13 +173,21 @@ def run_key_transform(input)
end
end

def run_key_pluralization(input)
if self.pluralized_type
input.to_s.pluralize.to_sym
else
input.to_sym
end
end

def use_hyphen
warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead')
set_key_transform :dash
end

def set_type(type_name)
self.record_type = run_key_transform(type_name)
self.record_type = run_key_transform(run_key_pluralization(type_name))
end

def set_id(id_name = nil, &block)
Expand Down Expand Up @@ -250,6 +270,7 @@ def create_relationship(base_key, relationship_type, options, block)
block
),
record_type: options[:record_type] || run_key_transform(base_key_sym),
pluralize_type: options[:pluralize_type],
object_method_name: options[:object_method_name] || name,
object_block: block,
serializer: compute_serializer_name(options[:serializer] || base_key_sym),
Expand Down
16 changes: 13 additions & 3 deletions lib/fast_jsonapi/relationship.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
module FastJsonapi
class Relationship
attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :lazy_load_data
attr_reader :key, :name, :id_method_name, :record_type, :pluralized_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :lazy_load_data

def initialize(
key:,
name:,
id_method_name:,
record_type:,
pluralize_type:,
object_method_name:,
object_block:,
serializer:,
Expand All @@ -21,7 +22,8 @@ def initialize(
@key = key
@name = name
@id_method_name = id_method_name
@record_type = record_type
@pluralized_type = pluralize_type
@record_type = run_key_pluralization(record_type)
@object_method_name = object_method_name
@object_block = object_block
@serializer = serializer
Expand Down Expand Up @@ -77,7 +79,7 @@ def ids_hash_from_record_and_relationship(record, params = {})

def id_hash_from_record(record, record_types)
# memoize the record type within the record_types dictionary, then assigning to record_type:
associated_record_type = record_types[record.class] ||= run_key_transform(record.class.name.demodulize.underscore)
associated_record_type = record_types[record.class] ||= run_key_transform(run_key_pluralization(record.class.name.demodulize.underscore))
id_hash(record.id, associated_record_type)
end

Expand Down Expand Up @@ -116,5 +118,13 @@ def run_key_transform(input)
input.to_sym
end
end

def run_key_pluralization(input)
if self.pluralized_type
input.to_s.pluralize.to_sym
else
input.to_sym
end
end
end
end
1 change: 1 addition & 0 deletions lib/fast_jsonapi/serialization_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class << self
:cachable_relationships_to_serialize,
:uncachable_relationships_to_serialize,
:transform_method,
:pluralized_type,
:record_type,
:record_id,
:cache_length,
Expand Down
2 changes: 1 addition & 1 deletion lib/fast_jsonapi/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module FastJsonapi
VERSION = "1.4"
VERSION = "1.5"
end
59 changes: 59 additions & 0 deletions spec/lib/object_serializer_class_methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,63 @@ def year_since_release_calculator(release_year)
end
end
end

describe '#pluralize_type' do
subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash }

before do
MovieSerializer.pluralize_type pluralize
end

after do
MovieSerializer.pluralize_type nil
MovieSerializer.set_type :movie
end

context 'when pluralize is true' do
let(:pluralize) { true }

it 'returns correct hash which type equals pluralized value' do
expect(serializable_hash[:data][:type]).to eq :movies
end
end

context 'when pluralize is false' do
let(:pluralize) { false }

it 'returns correct hash which type equals non-pluralized value' do
expect(serializable_hash[:data][:type]).to eq :movie
end
end
end

describe '#pluralize_type after #set_type' do
Copy link

@glyoko glyoko Apr 4, 2019

Choose a reason for hiding this comment

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

As it stands there's an issue where type is not correctly pluralized in relationships. The following test shows this:

    subject(:serializable_hash) { MovieSerializer.new(movie, include: [:actors]).serializable_hash }
    # ...
    context 'when pluralizing a relationship type after #set_type' do
      before do
        ActorSerializer.pluralize_type true
      end

      after do
        ActorSerializer.pluralize_type nil
      end

      it 'returns correct hash which relationship type equals transformed set_type value' do
        expect(serializable_hash[:data][:relationships][:actors][:data][0][:type]).to eq(:actors)
        expect(serializable_hash[:included][0][:type]).to eq(:actors)
      end
    end

This also means there's a mismatch between relationship and include types. Currently:

> serializable_hash[:data][:relationships][:actors][:data][0][:type]
# => :actor
> serializable_hash[:included][:type]
# => :actors

Copy link
Author

Choose a reason for hiding this comment

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

Unfortunately, as far as I understand, fast_jsonapi does not actually "know" internally what serializer is associated with any given relationship. The relationship data is built via simply reflecting on the object and constructing the abbreviated relationship structure. In theory it's possible to make it aware of the serializer, but see my original "Additional Note" -- I ended up being worried about missing performance targets, so I didn't implement a change here.

In order to make this scenario work, I implemented the ability to mark a specific relationship as plural with pluralize_type: true. (Check out object_serializer_pluralization_spec.rb.) This isn't ideal, but it solves this scenario in lieu of a higher-level decision about perf targets.

Copy link

Choose a reason for hiding this comment

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

I sort of glossed over that note when I first reviewed this PR, and after banging my head against this for a bit, I'm coming to the same conclusion... I'm going to investigate this a bit more because it is important for my use case, but I understand that the complexity involved in such a refactor may not be worth the effort.

At any rate it may not be worth including in this PR, as it already achieves the majority of what it sets out to do.

subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash }

before do
MovieSerializer.set_type type_name
MovieSerializer.pluralize_type true
end

after do
MovieSerializer.pluralize_type nil
MovieSerializer.set_type :movie
end

context 'when sets singular type name' do
let(:type_name) { :film }
Copy link

Choose a reason for hiding this comment

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

Every example in this group should be using let(:type_name) { :film }, so this should be describe-level.

Copy link
Author

Choose a reason for hiding this comment

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

Can you explain more about why this should be the case?

Copy link

@glyoko glyoko Apr 4, 2019

Choose a reason for hiding this comment

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

Look at line 541. That's directly setting the type to be plural in the before block, before the MovieSerializer.pluralize_type true line is even hit. In fact, you could comment out line 524 and that test would still pass.

Given that, both examples should be given a singular type name for these tests to check anything useful. In fact, you could even get rid of the let altogether and just say MovieSerializer.set_type :film on line 523.


it 'returns correct hash which type equals transformed set_type value' do
expect(serializable_hash[:data][:type]).to eq :films
end
end

context 'when sets plural type name' do
let(:type_name) { :films }
Copy link

Choose a reason for hiding this comment

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

Should remove this line, or else pluralize_type true isn't doing anything. This line manually sets the type to :films.


it 'returns correct hash which type equals transformed set_type value' do
expect(serializable_hash[:data][:type]).to eq :films
end
end
end
end
21 changes: 17 additions & 4 deletions spec/lib/object_serializer_inheritance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,23 @@ class EmployeeSerializer < UserSerializer
has_one :account
end

class LegacyUserSerializer < UserSerializer
pluralize_type true
end

class LegacyEmployeeSerializer < LegacyUserSerializer
attributes :location
attributes :compensation

has_one :account
end

it 'sets the correct record type' do
expect(EmployeeSerializer.reflected_record_type).to eq :employee
expect(EmployeeSerializer.record_type).to eq :employee
end

context 'when testing inheritance of attributes' do

it 'includes parent attributes' do
subclass_attributes = EmployeeSerializer.attributes_to_serialize
superclass_attributes = UserSerializer.attributes_to_serialize
Expand Down Expand Up @@ -157,12 +167,15 @@ class EmployeeSerializer < UserSerializer
end
end

context 'when test inheritence of other attributes' do

it 'inherits the tranform method' do
context 'when testing inheritence of other attributes' do
it 'inherits the transform method' do
EmployeeSerializer
expect(UserSerializer.transform_method).to eq EmployeeSerializer.transform_method
end

it 'inherits pluralized_type' do
LegacyEmployeeSerializer
expect(LegacyUserSerializer.pluralized_type).to eq LegacyEmployeeSerializer.pluralized_type
end
end
end
Loading