Skip to content

Feature/custom links defined per resource#486

Open
akharris wants to merge 5 commits intoJSONAPI-Resources:masterfrom
akharris:feature/custom-links-defined-per-resource
Open

Feature/custom links defined per resource#486
akharris wants to merge 5 commits intoJSONAPI-Resources:masterfrom
akharris:feature/custom-links-defined-per-resource

Conversation

@akharris
Copy link
Copy Markdown
Contributor

based on conversation in #464 and some code I managed to extract from a recent spike using this library.

cc @ceritium

Overview

I've made some modifications since my initial PR. I've simplified the custom link building interface so that now the custom_link method simply takes a JSONAPI::Resource instance and a LinkBuilder object. I'm open to reverting to an older version of the PR. This current implementation does depend on knowing how to use internal parts of the gem since it largely relies on the passed-in link builder object.

I've also backtracked on having code that adds a link with an if condition. I think it's much cleaner to return a null for a custom link than it is to remove the entire node, and we can always re-add the capability to completely add/remove custom links based on Resource instance-level business logic.

Examples

In this example, the url simply hangs off the the normal 'self' link.

class SpecificationResource < JSONAPI::Resource
  attributes :title, :description #etc

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

generates the following

{
  "data": [
  {
    "id": "26",
    "type": "specifications",
    "links": {
      "self": "http://example.com/api/specifications/26",
      "raw": "http://example.com/api/specifications/26/raw.xml"
    }
}

Slightly more complicated custom link

class SpecificationResource < JSONAPI::Resource
  attributes :title, :description #etc

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

generates

{
  "data": [
  {
    "id": "26",
    "type": "specifications",
    "links": {
      "self": "http://example.com/api/specifications/26",
      "raw": "http://example.com/api/specifications/26/super/duper/path.csv"
    }
}
class ProjectProposalResource < JSONAPI::Resource
  attributes :title, :description, :approved, :zoning_code

  has_one :department
  has_many :tasks

  # make the `approved?` method available to the Resource instance
  delegate :approved? to: :model

  custom_link :some_weird_gov_metadata_standard, ->(source, link_builder)  do
    if source.approved?
      link_builder.self_link(source) + "/some/metadata/standard.xml"
    end
  end
end
# example.com/api/project-proposals
# 
# ProjectProposal 12 is approved but ProjectProposal 13 is not
{
  "data": [
  {
    "id": "12",
    "type": "projectProposals",
    "links": {
      "self": "http://example.com/api/project-proposals/12",
      "some_weird_gov_metadata_standard": "http://example.com/api/project-proposals/12/some_weird_gov_metadata_standard.xml"
    },
    "attributes": {
      ...
    }
  {
    "id": "13",
    "type": "projectProposals",
    "links": {
      "self": "http://example.com/api/project-proposals/13",
      "some_weird_gov_metadata_standard": null
    },
    "attributes": {
      ...
    }
  }
}

Here are a couple of contrived examples that use lambdas.

class ArticleResource < JSONAPI::Resource
  attributes :title, :body, :published_at

  # make the method `original_source` available to the model
  delegate :original_source, to: :model

  custom_link :original_source, ->(source, link_builder) do 
    source.original_source # url stored on the model
  end
end
{
  "data": [
  {
    "id": "2",
    "type": "articles",
    "links": {
      "self": "http://example.com/api/articles/2",
      "original_source": "http://www.cerebris.com/blog/2015/06/04/jsonapi-1-0/"
    }
}
class ArticleResource < JSONAPI::Resource
  attributes :title, :body, :published_at

  custom_link :v1_api_link, ->(source, link_builder) do 
    Figaro.env.domain + "/v1/some-other-part-of-your-api"
  end
end
{
  "data": [
  {
    "id": "2",
    "type": "articles",
    "links": {
      "self": "http://example.com/api/v2/articles/2",
      "v1_api_link": "http://example.com/v1/some-other-part-of-your-api"
    }
}

Comment thread lib/jsonapi/resource.rb Outdated
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.

The warning refers to the if option but the check is for the with option. Is this intended?

Also, this line isn't being covered in the tests. We should add a test for it, or wrap it with # :nocov: directives.

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.

yeah that's a typo, one that would've been caught with the spec I didn't write 😃

I'll add a spec that catches when a custom link type is made with no with lambda.

@beechnut
Copy link
Copy Markdown

This example

class ArticleResource < JSONAPI::Resource
  attributes :title, :body, :published_at

  custom_link :original_source, :custom, with: ->(instance) { instance.original_source }
end

doesn't work, in my experience, unless #original_source is an attribute on the Resource. Since instance is the resource itself, and not the model, it doesn't have access to methods defined on the model that are not defined on the resource.

In order to get my custom next-record and previous-record links to work, it was necessary to call methods on the model directly:

class ProjectResource < JSONAPI::Resource
  u = Rails.application.routes.url_helpers
  custom_link :next,     :custom, with: ->(i) { u.project_url(i.model.next.presence) if i.model.next }
  custom_link :previous, :custom, with: ->(i) { u.project_url(i.model.previous) if i.model.previous }
end

Comment thread lib/jsonapi/resource.rb Outdated
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.

Do we need the instance level custom_links and custom_links? methods? It seems we could just directly access the class level methods.

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.

probably not. In ResourceSerializer#relationship_links I call source.custom_links? but that could just as easily be changed to source.class.custom_links?

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.

Thanks. Any methods we put at the instance level limit the available names for attributes.

@akharris
Copy link
Copy Markdown
Contributor Author

@beechnut good catch, I updated the top comment with your corrections. 👍

I think the following should work as well.

class ProjectResource < JSONAPI::Resource
  u = Rails.application.routes.url_helpers
  delegate :presence, to: :model
  delegate :next, to: :model
  delegate :previous, to: :model

  custom_link :next,     :custom, with: ->(i) { u.project_url(i.next) if i.next }
  custom_link :previous, :custom, with: ->(i) { u.project_url(i.previous) if i.previous }
end

@akharris akharris force-pushed the feature/custom-links-defined-per-resource branch from 5e8802a to 60623e4 Compare October 15, 2015 21:04
@lgebhardt lgebhardt mentioned this pull request Oct 22, 2015
@akharris
Copy link
Copy Markdown
Contributor Author

sorry I've been taking some time off lately and will be offline the next two weeks. I'll resume work on this PR when I get back (unless someone else has added the same feature).

@akharris akharris force-pushed the feature/custom-links-defined-per-resource branch from 60623e4 to b6c9444 Compare October 24, 2015 01:14
@lgebhardt
Copy link
Copy Markdown
Contributor

In the new resource level meta feature I'm passing in custom_generation_options (https://github.com/cerebris/jsonapi-resources/blob/master/lib/jsonapi/resource_serializer.rb#L133) which contains the serializer. From there you can get the link builder. It would be nice if we could reuse the custom_generation_options function as the parameter for the custom links.

@akharris
Copy link
Copy Markdown
Contributor Author

@lgebhardt I'm back from my vacation. I'll look into incorporating custom_generation_options over the next week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants