Skip to content

Commit

Permalink
Merge pull request #342 from Netflix/release-1.5
Browse files Browse the repository at this point in the history
Release 1.5
  • Loading branch information
shishirmk authored Nov 3, 2018
2 parents 92bcab0 + ee76e0c commit fdcaed6
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 40 deletions.
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ end
```

### Links Per Object
Links are defined in FastJsonapi using the `link` method. By default, link are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized.
Links are defined in FastJsonapi using the `link` method. By default, links are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized.

You can configure the method to use on the object for example a link with key `self` will get set to the value returned by a method called `url` on the movie object.

Expand All @@ -245,15 +245,38 @@ 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.
#### Links on a Relationship

You can specify [relationship links](http://jsonapi.org/format/#document-resource-object-relationships) by using the `links:` option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see [related resource links](http://jsonapi.org/format/#document-resource-object-related-resource-links))

```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer

has_many :actors, links: {
self: :url,
related: -> (object) {
"https://movies.com/#{object.id}/actors"
}
}
end
```

This will create a `self` reference for the relationship, and a `related` link for loading the actors relationship later. NB: This will not automatically disable loading the data in the relationship, you'll need to do that using the `lazy_load_data` option:

```ruby
has_many :actors, lazy_load_data: true, links: {
self: :url,
related: -> (object) {
"https://movies.com/#{object.id}/actors"
}
}
```

### 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
meta do |movie|
{
years_since_release: Date.current.year - movie.year
Expand Down Expand Up @@ -427,9 +450,9 @@ 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 ```
set_id | ID of Object | ```set_id :owner_id ``` or ```set_id { |record| "#{record.name.downcase}-#{record.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 ```
id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, `id_method_name` is invoked on the return value of the block instead of the resource object) | ```has_many :locations, id_method_name: :place_ids ```
object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ```
record_type | Set custom Object Type for a relationship | ```belongs_to :owner, record_type: :user```
serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor``` or ```has_many :actors, serializer: MyApp::Api::V1::ActorSerializer```
Expand Down
19 changes: 14 additions & 5 deletions lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# frozen_string_literal: true

require 'active_support/time'
require 'active_support/json'
require 'active_support/concern'
require 'active_support/inflector'
require 'active_support/core_ext/numeric/time'
require 'fast_jsonapi/attribute'
require 'fast_jsonapi/relationship'
require 'fast_jsonapi/link'
Expand Down Expand Up @@ -72,6 +74,7 @@ def serialized_json

def process_options(options)
@fieldsets = deep_symbolize(options[:fields].presence || {})
@params = {}

return if options.blank?

Expand Down Expand Up @@ -117,7 +120,7 @@ def inherited(subclass)
subclass.transform_method = transform_method
subclass.cache_length = cache_length
subclass.race_condition_ttl = race_condition_ttl
subclass.data_links = data_links
subclass.data_links = data_links.dup if data_links.present?
subclass.cached = cached
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
subclass.meta_to_serialize = meta_to_serialize
Expand All @@ -143,7 +146,11 @@ def set_key_transform(transform_name)
self.transform_method = mapping[transform_name.to_sym]

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

def run_key_transform(input)
Expand All @@ -163,8 +170,8 @@ def set_type(type_name)
self.record_type = run_key_transform(type_name)
end

def set_id(id_name)
self.record_id = id_name
def set_id(id_name = nil, &block)
self.record_id = block || id_name
end

def cache_options(cache_options)
Expand Down Expand Up @@ -250,7 +257,9 @@ def create_relationship(base_key, relationship_type, options, block)
cached: options[:cached],
polymorphic: fetch_polymorphic_option(options),
conditional_proc: options[:if],
transform_method: @transform_method
transform_method: @transform_method,
links: options[:links],
lazy_load_data: options[:lazy_load_data]
)
end

Expand Down
23 changes: 18 additions & 5 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, :transform_method
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

def initialize(
key:,
Expand All @@ -14,7 +14,9 @@ def initialize(
cached: false,
polymorphic:,
conditional_proc:,
transform_method:
transform_method:,
links:,
lazy_load_data: false
)
@key = key
@name = name
Expand All @@ -28,14 +30,19 @@ def initialize(
@polymorphic = polymorphic
@conditional_proc = conditional_proc
@transform_method = transform_method
@links = links || {}
@lazy_load_data = lazy_load_data
end

def serialize(record, serialization_params, output_hash)
if include_relationship?(record, serialization_params)
empty_case = relationship_type == :has_many ? [] : nil
output_hash[key] = {
data: ids_hash_from_record_and_relationship(record, serialization_params) || empty_case
}

output_hash[key] = {}
unless lazy_load_data
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case
end
add_links_hash(record, serialization_params, output_hash) if links.present?
end
end

Expand Down Expand Up @@ -96,6 +103,12 @@ def fetch_id(record, params)
record.public_send(id_method_name)
end

def add_links_hash(record, params, output_hash)
output_hash[key][:links] = links.each_with_object({}) do |(key, method), hash|
Link.new(key: key, method: method).serialize(record, params, hash)\
end
end

def run_key_transform(input)
if self.transform_method.present?
input.to_s.send(*self.transform_method).to_sym
Expand Down
7 changes: 4 additions & 3 deletions lib/fast_jsonapi/serialization_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ def record_hash(record, fieldset, params = {})
end

def id_from_record(record)
return record.send(record_id) if record_id
raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
record.id
return record_id.call(record) if record_id.is_a?(Proc)
return record.send(record_id) if record_id
raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
record.id
end

# Override #to_json for alternative implementation
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
82 changes: 82 additions & 0 deletions spec/lib/extensions/active_record_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,85 @@ class Account < ActiveRecord::Base
File.delete(@db_file) if File.exist?(@db_file)
end
end

describe 'active record has_one through' do
# Setup DB
before(:all) do
@db_file = "test_two.db"

# Open a database
db = SQLite3::Database.new @db_file

# Create tables
db.execute_batch <<-SQL
create table forests (
id int primary key,
name varchar(30)
);
create table trees (
id int primary key,
forest_id int,
name varchar(30),
FOREIGN KEY (forest_id) REFERENCES forests(id)
);
create table fruits (
id int primary key,
tree_id int,
name varchar(30),
FOREIGN KEY (tree_id) REFERENCES trees(id)
);
SQL

# Insert records
db.execute_batch <<-SQL
insert into forests values (1, 'sherwood');
insert into trees values (2, 1,'pine');
insert into fruits values (3, 2, 'pine nut');
insert into fruits(id,name) values (4,'apple');
SQL
end

# Setup Active Record
before(:all) do
class Forest < ActiveRecord::Base
has_many :trees
end

class Tree < ActiveRecord::Base
belongs_to :forest
end

class Fruit < ActiveRecord::Base
belongs_to :tree
has_one :forest, through: :tree
end

ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => @db_file
)
end

context 'revenue' do
it 'has an forest_id' do
expect(Fruit.find(3).respond_to?(:forest_id)).to be true
expect(Fruit.find(3).forest_id).to eq 1
expect(Fruit.find(3).forest.name).to eq "sherwood"
end

it 'has nil if tree id not available' do
expect(Fruit.find(4).respond_to?(:tree_id)).to be true
expect(Fruit.find(4).forest_id).to eq nil
end
end

# Clean up DB
after(:all) do
File.delete(@db_file) if File.exist?(@db_file)
end
end
2 changes: 1 addition & 1 deletion spec/lib/object_serializer_attribute_param_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def viewed?(user)

class MovieSerializer
attribute :viewed do |movie, params|
params ? movie.viewed?(params[:user]) : false
params[:user] ? movie.viewed?(params[:user]) : false
end

attribute :no_param_attribute do |movie|
Expand Down
Loading

0 comments on commit fdcaed6

Please sign in to comment.