Skip to content
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
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,74 @@ class AuthorResource < JSONAPI::Resource
end
```

#### Custom Links

You can define custom links for Resource classes with the #custom_link method which takes a symbolized key and a lambda that accepts a source (Resource instance) and a link builder. For example:

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

has_one :organizer

custom_link :minutes, ->(source, link_builder) { link_builder.self_link(source) + "/minutes" }
end
````

This will create a custom link with the key `minutes` that is generated using the lambda. Here's an example JSON output

````
{
"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": {...}
},
//...
]
}
````

If you set an if condition in the lambda that is not met the custom link will simply be set to null. Here's an example using the same class as above with a slight modification to the lambda.

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

delegate :approved?, to: :model

has_one :organizer

custom_link :minutes, ->(source, link_builder) do
if source.approved?
link_builder.self_link(source) + "/minutes"
end
end
end
````

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

#### Relationships

Related resources need to be specified in the resource. These may be declared with the `relationship` or the `has_one`
Expand Down
29 changes: 29 additions & 0 deletions lib/jsonapi/link_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,37 @@ def self_link(source)
end
end

def build_custom_links(source)
link_instructions = source.class.custom_links
custom_links = {}

link_instructions.each do |key, custom_link_lambda|
custom_links[key] = custom_link_lambda.call(source, self)
end

custom_links
end

private

def self_link_extension(key, source, link_info={})
extension = link_info[:ext]
path = link_info[:path] || key

url = "#{ self_link(source) }/#{ path }"
url += ".#{extension}" if extension

url
end

def build_custom_link(key, source, link_info)
extension = link_info[:ext]
url = link_info[:with].call(source)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it make sense to pass in the link_builder to the lambda? That way the methods it provides could be used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 that's an awesome idea and could make custom_link simpler. I'll see where I can take it in the next few days.

url += ".#{ extension }" if extension

url
end

def build_engine_name
scopes = module_scopes_from_class(primary_resource_klass)

Expand Down
11 changes: 10 additions & 1 deletion lib/jsonapi/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ def inherited(base)
base._attributes = (_attributes || {}).dup
base._relationships = (_relationships || {}).dup
base._allowed_filters = (_allowed_filters || Set.new).dup
base._custom_links = (_custom_links || {}).dup

type = base.name.demodulize.sub(/Resource$/, '').underscore
base._type = type.pluralize.to_sym
Expand All @@ -283,7 +284,15 @@ def resource_for(type)
resource
end

attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator
attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_custom_links

def custom_links
@_custom_links ||= {}
end

def custom_link(name, func)
@_custom_links[name.to_sym] = func
end

def create(context)
new(create_model, context)
Expand Down
5 changes: 5 additions & 0 deletions lib/jsonapi/resource_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ def relationship_links(source)
links = {}
links[:self] = url_generator.self_link(source)

if source.class.custom_links
customized_links = url_generator.build_custom_links(source)
links.merge!(customized_links)
end

links
end

Expand Down
77 changes: 77 additions & 0 deletions test/fixtures/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,83 @@ class WebPageResource < JSONAPI::Resource
attribute :link
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

custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw" }
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

custom_link :raw, ->(source, link_builder) do
link_builder.self_link(source) + "/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

custom_link :conditional_custom_link, ->(source, link_builder) do
if source.title == "JR Solves your serialization woes!"
link_builder.self_link(source) + "/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

custom_link :link_to_external_api, ->(source, link_builder) do
"http://external-api.com/posts/#{ source.created_at.year }/#{ source.created_at.month }/#{ source.created_at.day }-#{ source.subject.gsub(' ', '-') }"
end
end


module Api
module V1
class WriterResource < JSONAPI::Resource
Expand Down
29 changes: 29 additions & 0 deletions test/unit/resource/resource_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ class MyNamespacedResource < JSONAPI::Resource
end
end

class CustomLinkTestResource < JSONAPI::Resource
model_name 'Post'

custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw" }
end

class CustomLinkTestWithExtensionResource < JSONAPI::Resource
model_name 'Post'

custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw.xml" }
end

class CustomLinkTestWithCustomPathResource < JSONAPI::Resource
model_name 'Post'

custom_link :raw, ->(source, link_builder) do
link_builder.self_link(source) + "/super/duper/path.xml"
end
end

class CustomLinkLambda< JSONAPI::Resource
model_name 'Post'

custom_link :raw, ->(source, link_builder) do
"http://external-api/posts/" +
"#{ instance.created_at.year }/#{ instance.created_at.month}/#{ instance.created_at.day }"
end
end

class ResourceTest < ActiveSupport::TestCase
def setup
@post = Post.first
Expand Down
Loading