Skip to content

Commit

Permalink
Merge pull request #320 from Netflix/release-1.4
Browse files Browse the repository at this point in the history
Release 1.4
  • Loading branch information
shishirmk authored Sep 20, 2018
2 parents 4afa5b8 + 90e0fee commit 92bcab0
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 32 deletions.
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,23 @@ class MovieSerializer
end
```

### 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
}
end
end
```

### Compound Document

Support for top-level and nested included associations through ` options[:include] `.
Expand Down Expand Up @@ -351,15 +368,15 @@ class MovieSerializer
include FastJsonapi::ObjectSerializer

attributes :name, :year
attribute :release_year, if: Proc.new do |record|
attribute :release_year, if: Proc.new { |record|
# Release year will only be serialized if it's greater than 1990
record.release_year > 1990
end
}

attribute :director, if: Proc.new do |record, params|
attribute :director, if: Proc.new { |record, params|
# The director will be serialized only if the :admin key of params is true
params && params[:admin] == true
end
}
end

# ...
Expand Down Expand Up @@ -409,6 +426,7 @@ serializer.serializable_hash
Option | Purpose | Example
------------ | ------------- | -------------
set_type | Type name of Object | ```set_type :movie ```
key | Key of Object | ```belongs_to :owner, key: :user ```
set_id | ID of Object | ```set_id :owner_id ```
cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds```
id_method_name | Set custom method name to get ID of an object | ```has_many :locations, id_method_name: :place_ids ```
Expand Down
34 changes: 26 additions & 8 deletions lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require 'active_support/core_ext/object'
require 'active_support/json'
require 'active_support/concern'
require 'active_support/inflector'
require 'fast_jsonapi/attribute'
Expand Down Expand Up @@ -65,7 +65,7 @@ def hash_for_collection
end

def serialized_json
self.class.to_json(serializable_hash)
ActiveSupport::JSON.encode(serializable_hash)
end

private
Expand Down Expand Up @@ -120,6 +120,7 @@ def inherited(subclass)
subclass.data_links = data_links
subclass.cached = cached
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
subclass.meta_to_serialize = meta_to_serialize
end

def reflected_record_type
Expand Down Expand Up @@ -194,7 +195,7 @@ def add_relationship(relationship)
self.relationships_to_serialize = {} if relationships_to_serialize.nil?
self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?

if !relationship.cached
self.uncachable_relationships_to_serialize[relationship.name] = relationship
else
Expand All @@ -218,6 +219,10 @@ def belongs_to(relationship_name, options = {}, &block)
add_relationship(relationship)
end

def meta(&block)
self.meta_to_serialize = block
end

def create_relationship(base_key, relationship_type, options, block)
name = base_key.to_sym
if relationship_type == :has_many
Expand All @@ -232,18 +237,31 @@ def create_relationship(base_key, relationship_type, options, block)
Relationship.new(
key: options[:key] || run_key_transform(base_key),
name: name,
id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym,
id_method_name: compute_id_method_name(
options[:id_method_name],
"#{base_serialization_key}#{id_postfix}".to_sym,
block
),
record_type: options[:record_type] || run_key_transform(base_key_sym),
object_method_name: options[:object_method_name] || name,
object_block: block,
serializer: compute_serializer_name(options[:serializer] || base_key_sym),
relationship_type: relationship_type,
cached: options[:cached],
polymorphic: fetch_polymorphic_option(options),
conditional_proc: options[:if]
conditional_proc: options[:if],
transform_method: @transform_method
)
end

def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, block)
if block.present?
custom_id_method_name || :id
else
custom_id_method_name || id_method_name_from_relationship
end
end

def compute_serializer_name(serializer_key)
return serializer_key unless serializer_key.is_a? Symbol
namespace = self.name.gsub(/()?\w+Serializer$/, '')
Expand Down Expand Up @@ -275,10 +293,10 @@ def validate_includes!(includes)
includes.detect do |include_item|
klass = self
parse_include_item(include_item).each do |parsed_include|
relationship_to_include = klass.relationships_to_serialize[parsed_include]
relationships_to_serialize = klass.relationships_to_serialize || {}
relationship_to_include = relationships_to_serialize[parsed_include]
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
raise NotImplementedError if relationship_to_include.polymorphic.is_a?(Hash)
klass = relationship_to_include.serializer.to_s.constantize
klass = relationship_to_include.serializer.to_s.constantize unless relationship_to_include.polymorphic.is_a?(Hash)
end
end
end
Expand Down
26 changes: 17 additions & 9 deletions lib/fast_jsonapi/relationship.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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
attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method

def initialize(
key:,
Expand All @@ -13,7 +13,8 @@ def initialize(
relationship_type:,
cached: false,
polymorphic:,
conditional_proc:
conditional_proc:,
transform_method:
)
@key = key
@name = name
Expand All @@ -26,6 +27,7 @@ def initialize(
@cached = cached
@polymorphic = polymorphic
@conditional_proc = conditional_proc
@transform_method = transform_method
end

def serialize(record, serialization_params, output_hash)
Expand Down Expand Up @@ -68,7 +70,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] ||= record.class.name.underscore.to_sym
associated_record_type = record_types[record.class] ||= run_key_transform(record.class.name.demodulize.underscore)
id_hash(record.id, associated_record_type)
end

Expand All @@ -86,14 +88,20 @@ def id_hash(id, record_type, default_return=false)
end

def fetch_id(record, params)
unless object_block.nil?
if object_block.present?
object = object_block.call(record, params)

return object.map(&:id) if object.respond_to? :map
return object.try(:id)
return object.map { |item| item.public_send(id_method_name) } if object.respond_to? :map
return object.try(id_method_name)
end

record.public_send(id_method_name)
end

def run_key_transform(input)
if self.transform_method.present?
input.to_s.send(*self.transform_method).to_sym
else
input.to_sym
end
end
end
end
end
27 changes: 20 additions & 7 deletions lib/fast_jsonapi/serialization_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class << self
:cache_length,
:race_condition_ttl,
:cached,
:data_links
:data_links,
:meta_to_serialize
end
end

Expand Down Expand Up @@ -57,6 +58,10 @@ def relationships_hash(record, relationships = nil, fieldset = nil, params = {})
end
end

def meta_hash(record, params = {})
meta_to_serialize.call(record, params)
end

def record_hash(record, fieldset, params = {})
if cached
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do
Expand All @@ -67,13 +72,15 @@ def record_hash(record, fieldset, params = {})
temp_hash[:links] = links_hash(record, params) if data_links.present?
temp_hash
end
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, params)) if uncachable_relationships_to_serialize.present?
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, params)) if uncachable_relationships_to_serialize.present?
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash
else
record_hash = id_hash(id_from_record(record), record_type, true)
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
record_hash[:relationships] = relationships_hash(record, nil, fieldset, params) if relationships_to_serialize.present?
record_hash[:links] = links_hash(record, params) if data_links.present?
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash
end
end
Expand Down Expand Up @@ -112,22 +119,28 @@ def get_included_records(record, includes_list, known_included_objects, fieldset
next unless relationships_to_serialize && relationships_to_serialize[item]
relationship_item = relationships_to_serialize[item]
next unless relationship_item.include_relationship?(record, params)
raise NotImplementedError if relationship_item.polymorphic.is_a?(Hash)
record_type = relationship_item.record_type
serializer = relationship_item.serializer.to_s.constantize
unless relationship_item.polymorphic.is_a?(Hash)
record_type = relationship_item.record_type
serializer = relationship_item.serializer.to_s.constantize
end
relationship_type = relationship_item.relationship_type

included_objects = relationship_item.fetch_associated_object(record, params)
next if included_objects.blank?
included_objects = [included_objects] unless relationship_type == :has_many

included_objects.each do |inc_obj|
if relationship_item.polymorphic.is_a?(Hash)
record_type = inc_obj.class.name.demodulize.underscore
serializer = self.compute_serializer_name(inc_obj.class.name.demodulize.to_sym).to_s.constantize
end

if remaining_items(items)
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets)
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty?
end

code = "#{record_type}_#{inc_obj.id}"
code = "#{record_type}_#{serializer.id_from_record(inc_obj)}"
next if known_included_objects.key?(code)

known_included_objects[code] = inc_obj
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.3"
VERSION = "1.4"
end
53 changes: 53 additions & 0 deletions spec/lib/object_serializer_class_methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,31 @@
end
end

describe '#has_many with block and id_method_name' do
before do
MovieSerializer.has_many(:awards, id_method_name: :imdb_award_id) do |movie|
movie.actors.map(&:awards).flatten
end
end

after do
MovieSerializer.relationships_to_serialize.delete(:awards)
end

context 'awards is not included' do
subject(:hash) { MovieSerializer.new(movie).serializable_hash }

it 'returns correct hash where id is obtained from the method specified via `id_method_name`' do
expected_award_data = movie.actors.map(&:awards).flatten.map do |actor|
{ id: actor.imdb_award_id.to_s, type: actor.class.name.downcase.to_sym }
end
serialized_award_data = hash[:data][:relationships][:awards][:data]

expect(serialized_award_data).to eq(expected_award_data)
end
end
end

describe '#belongs_to' do
subject(:relationship) { MovieSerializer.relationships_to_serialize[:area] }

Expand Down Expand Up @@ -249,6 +274,34 @@
end
end

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

before do
movie.release_year = 2008
MovieSerializer.meta do |movie|
{
years_since_release: year_since_release_calculator(movie.release_year)
}
end
end

after do
movie.release_year = nil
MovieSerializer.meta_to_serialize = nil
end

it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:meta]).to eq ({ years_since_release: year_since_release_calculator(movie.release_year) })
end

private

def year_since_release_calculator(release_year)
Date.current.year - release_year
end
end

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

Expand Down
Loading

0 comments on commit 92bcab0

Please sign in to comment.