Skip to content
Merged
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ backed by ActiveRecord models or by custom objects.
* [Pagination] (#pagination)
* [Included relationships (side-loading resources)] (#included-relationships-side-loading-resources)
* [Resource meta] (#resource-meta)
* [Custom Links] (#resource-meta)
* [Callbacks] (#callbacks)
* [Controllers] (#controllers)
* [Namespaces] (#namespaces)
Expand Down Expand Up @@ -929,6 +930,72 @@ method is called with an `options` has. The `options` hash will contain the foll
* `:serializer` -> the serializer instance
* `:serialization_options` -> the contents of the `serialization_options` method on the controller.

#### Custom Links

Custom links can be included for each resource by overriding the `custom_links` method. If a non empty hash is returned from `custom_links`, it will be merged with the default links hash containing the resource's `self` link. The `custom_links` method is called with the same `options` hash used by for [resource meta information](#resource-meta). The `options` hash contains the following:

* `:serializer` -> the serializer instance
* `:serialization_options` -> the contents of the `serialization_options` method on the controller.

For example:

```ruby
class CityCouncilMeeting < JSONAPI::Resource
attribute :title, :location, :approved

def custom_links(options)
{ minutes: options[:serialzer].link_builder.self_link(self) + "/minutes" }
end
end
```

This will create a custom link with the key `minutes`, which will be merged with the default `self` link, like so:

```json
{
"data": [
{
"id": "1",
"type": "cityCouncilMeetings",
"links": {
"self": "http://city.gov/api/city-council-meetings/1",
"minutes": "http://city.gov/api/city-council-meetings/1/minutes"
},
"attributes": {...}
},
//...
]
}
```

Of course, the `custom_links` method can include logic to include links only when relevant:

````ruby
class CityCouncilMeeting < JSONAPI::Resource
attribute :title, :location, :approved

delegate :approved?, to: :model

def custom_links(options)
extra_links = {}
if approved?
extra_links[:minutes] = options[:serialzer].link_builder.self_link(self) + "/minutes"
end
extra_links
end
end
```

It's also possibly to suppress the default `self` link by returning a hash with `{self: nil}`:

````ruby
class Selfless < JSONAPI::Resource
def custom_links(options)
{self: nil}
end
end
```

#### Callbacks

`ActiveSupport::Callbacks` is used to provide callback functionality, so the behavior is very similar to what you may be
Expand Down
9 changes: 9 additions & 0 deletions lib/jsonapi/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ def meta(_options)
{}
end

# Override this to return custom links
# must return a hash, which will be merged with the default { self: 'self-url' } links hash
# links keys will be not be formatted with the key formatter for the serializer by default.
# They can however use the serializer's format_key and format_value methods if desired
# the _options hash will contain the serializer and the serialization_options
def custom_links(_options)
{}
end

private

def save
Expand Down
44 changes: 26 additions & 18 deletions lib/jsonapi/resource_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,18 @@ def object_hash(source, include_directives)

obj_hash['type'] = format_key(source.class._type.to_s)

links = relationship_links(source)
links = links_hash(source)
obj_hash['links'] = links unless links.empty?

attributes = attribute_hash(source)
attributes = attributes_hash(source)
obj_hash['attributes'] = attributes unless attributes.empty?

relationships = relationship_data(source, include_directives)
relationships = relationships_hash(source, include_directives)
obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?

meta = source.meta(custom_generation_options)
if meta.is_a?(Hash) && !meta.empty?
obj_hash['meta'] = meta
end
meta = meta_hash(source)
obj_hash['meta'] = meta unless meta.empty?

obj_hash
end

Expand All @@ -139,7 +138,7 @@ def requested_fields(klass)
end
end

def attribute_hash(source)
def attributes_hash(source)
requested = requested_fields(source.class)
fields = source.fetchable_fields & source.class._attributes.keys.to_a
fields = requested & fields unless requested.nil?
Expand All @@ -159,7 +158,23 @@ def custom_generation_options
}
end

def relationship_data(source, include_directives)
def meta_hash(source)
meta = source.meta(custom_generation_options)
(meta.is_a?(Hash) && meta) || {}
end

def links_hash(source)
{
self: link_builder.self_link(source)
}.merge(custom_links_hash(source)).compact
end

def custom_links_hash(source)
custom_links = source.custom_links(custom_generation_options)
(custom_links.is_a?(Hash) && custom_links) || {}
end

def relationships_hash(source, include_directives)
relationships = source.class._relationships
requested = requested_fields(source.class)
fields = relationships.keys
Expand Down Expand Up @@ -197,7 +212,7 @@ def relationship_data(source, include_directives)
if include_linkage && !relationships_only
add_included_object(id, object_hash(resource, ia))
elsif include_linked_children || relationships_only
relationship_data(resource, ia)
relationships_hash(resource, ia)
end
end
elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
Expand All @@ -208,7 +223,7 @@ def relationship_data(source, include_directives)
if include_linkage && !relationships_only
add_included_object(id, object_hash(resource, ia))
elsif include_linked_children || relationships_only
relationship_data(resource, ia)
relationships_hash(resource, ia)
end
end
end
Expand All @@ -217,13 +232,6 @@ def relationship_data(source, include_directives)
end
end

def relationship_links(source)
links = {}
links[:self] = link_builder.self_link(source)

links
end

def already_serialized?(type, id)
type = format_key(type)
@included_objects.key?(type) && @included_objects[type].key?(id)
Expand Down
81 changes: 81 additions & 0 deletions test/fixtures/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,87 @@ class AuthorDetailResource < JSONAPI::Resource
attributes :author_stuff
end

class SimpleCustomLinkResource < JSONAPI::Resource
model_name 'Post'
attributes :title, :body, :subject

def subject
@model.title
end

has_one :writer, foreign_key: 'author_id', class_name: 'Writer'
has_one :section
has_many :comments, acts_as_set: false

filters :writer

def custom_links(options)
{ raw: options[:serializer].link_builder.self_link(self) + "/raw" }
end
end

class CustomLinkWithRelativePathOptionResource < JSONAPI::Resource
model_name 'Post'
attributes :title, :body, :subject

def subject
@model.title
end

has_one :writer, foreign_key: 'author_id', class_name: 'Writer'
has_one :section
has_many :comments, acts_as_set: false

filters :writer

def custom_links(options)
{ raw: options[:serializer].link_builder.self_link(self) + "/super/duper/path.xml" }
end
end

class CustomLinkWithIfCondition < JSONAPI::Resource
model_name 'Post'
attributes :title, :body, :subject

def subject
@model.title
end

has_one :writer, foreign_key: 'author_id', class_name: 'Writer'
has_one :section
has_many :comments, acts_as_set: false

filters :writer

def custom_links(options)
if title == "JR Solves your serialization woes!"
{conditional_custom_link: options[:serializer].link_builder.self_link(self) + "/conditional/link.json"}
end
end
end

class CustomLinkWithLambda < JSONAPI::Resource
model_name 'Post'
attributes :title, :body, :subject, :created_at

def subject
@model.title
end

has_one :writer, foreign_key: 'author_id', class_name: 'Writer'
has_one :section
has_many :comments, acts_as_set: false

filters :writer

def custom_links(options)
{
link_to_external_api: "http://external-api.com/posts/#{ created_at.year }/#{ created_at.month }/#{ created_at.day }-#{ subject.gsub(' ', '-') }"
}
end
end


module Api
module V1
class WriterResource < JSONAPI::Resource
Expand Down
Loading