From 76dd7b97b342f327ba05327d98c1dc67cb244e56 Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Wed, 30 Sep 2015 12:15:29 -0400 Subject: [PATCH 01/10] adds custom link --- lib/jsonapi/link_builder.rb | 22 +++++++++++++ lib/jsonapi/resource.rb | 23 +++++++++++++- lib/jsonapi/resource_serializer.rb | 5 +++ test/fixtures/active_record.rb | 17 ++++++++++ test/unit/resource/resource_test.rb | 26 +++++++++++++++ test/unit/serializer/serializer_test.rb | 42 +++++++++++++++++++++++++ 6 files changed, 134 insertions(+), 1 deletion(-) diff --git a/lib/jsonapi/link_builder.rb b/lib/jsonapi/link_builder.rb index 2ca5fef90..834ef41d3 100644 --- a/lib/jsonapi/link_builder.rb +++ b/lib/jsonapi/link_builder.rb @@ -49,8 +49,30 @@ def self_link(source) end end + def build_custom_links(source) + link_instructions = source.custom_links + custom_links = {} + + link_instructions.each do |link| + key = link.first + type = link.last[:type] + ext = link.last[:ext] + + custom_links[key] = send("build_custom_#{type}_link", key, source, ext) + end + + custom_links + end + private + def build_custom_self_link(key, source, ext=nil) + url = "#{ self_link(source) }/#{ key }" + url += ".#{ext}" if ext + + url + end + def build_engine_name scopes = module_scopes_from_class(primary_resource_klass) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 03f303c22..d7ec9a188 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -152,6 +152,14 @@ def meta(_options) {} end + def custom_links? + self.class.custom_links.size > 0 + end + + def custom_links + @custom_links ||= self.class.custom_links + end + private def save @@ -317,6 +325,7 @@ def inherited(subclass) end subclass._allowed_filters = (_allowed_filters || Set.new).dup + subclass._custom_links = (_custom_links || {}).dup type = subclass.name.demodulize.sub(/Resource$/, '').underscore subclass._type = type.pluralize.to_sym @@ -354,7 +363,19 @@ def resource_type_for(model) end end - attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_model_hints + attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_model_hints, :_custom_links + + def custom_links + @_custom_links ||= {} + end + + def custom_link(name, type, options={}) + link_manifest = { type: type.to_sym } + extension = options[:ext] || options[:extension] + link_manifest.merge!(ext: extension) if extension + + @_custom_links[name.to_sym] = link_manifest + end def create(context) new(create_model, context) diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index 8e4edfcb9..1e7a58511 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -221,6 +221,11 @@ def relationship_links(source) links = {} links[:self] = link_builder.self_link(source) + if source.custom_links? + customized_links = link_builder.build_custom_links(source) + links.merge!(customized_links) + end + links end diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index 244ee95ff..59c0cefe2 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1125,6 +1125,23 @@ class AuthorDetailResource < JSONAPI::Resource attributes :author_stuff end +class CustomLinkResource < 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, :self +end + module Api module V1 class WriterResource < JSONAPI::Resource diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index ef3132ec0..0b3bb3939 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -80,11 +80,37 @@ class RelatedResource < MyModule::RelatedResource end end +class CustomLinkTestResource < JSONAPI::Resource + model_name 'Post' + + custom_link :raw, :self +end + +class CustomLinkTestWithExtensionResource < JSONAPI::Resource + model_name 'Post' + + custom_link :raw, :self, ext: :xml +end + class ResourceTest < ActiveSupport::TestCase def setup @post = Post.first end + def test_custom_links? + assert_equal(CustomLinkTestResource.new(Post.first, {}).custom_links?, true) + end + + def test_custom_links + registered_custom_link = { raw: { :type => :self } } + assert_equal(CustomLinkTestResource.new(Post.first, {}).custom_links, registered_custom_link) + end + + def test_custom_links_with_extension + registered_custom_link = { raw: { type: :self, ext: :xml } } + assert_equal(CustomLinkTestWithExtensionResource.new(Post.first, {}).custom_links, registered_custom_link) + end + def test_model_name assert_equal("Post", PostResource._model_name) end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index ce1721a98..29ee01c4b 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1936,4 +1936,46 @@ class ::Questionable2Resource < JSONAPI::Resource out.strip ) end + + def test_custom_links + serialized_custom_link_resource = JSONAPI::ResourceSerializer.new(CustomLinkResource, base_url: 'http://example.com').serialize_to_hash(CustomLinkResource.new(Post.first, {})) + + custom_link_spec = { + data: { + type: 'customLinks', + id: '1', + attributes: { + title: "New post", + body: "A body!!!", + subject: "New post" + }, + links: { + self: "http://example.com/customLinks/1", + raw: "http://example.com/customLinks/1/raw" + }, + relationships: { + writer: { + links: { + self: "http://example.com/customLinks/1/relationships/writer", + related: "http://example.com/customLinks/1/writer" + } + }, + section: { + links: { + self: "http://example.com/customLinks/1/relationships/section", + related: "http://example.com/customLinks/1/section" + } + }, + comments: { + links: { + self: "http://example.com/customLinks/1/relationships/comments", + related: "http://example.com/customLinks/1/comments" + } + } + } + } + } + + assert_hash_equals(custom_link_spec, serialized_custom_link_resource) + end end From 4ec99c4ae7e886998049d07fe3c8972d183fdd2f Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Wed, 30 Sep 2015 13:34:48 -0400 Subject: [PATCH 02/10] adds ability to customize the path from the 'self' link --- lib/jsonapi/link_builder.rb | 18 +++++++--- lib/jsonapi/resource.rb | 5 ++- test/fixtures/active_record.rb | 18 ++++++++++ test/unit/resource/resource_test.rb | 12 +++++++ test/unit/serializer/serializer_test.rb | 45 +++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/lib/jsonapi/link_builder.rb b/lib/jsonapi/link_builder.rb index 834ef41d3..2f9dc2677 100644 --- a/lib/jsonapi/link_builder.rb +++ b/lib/jsonapi/link_builder.rb @@ -55,10 +55,11 @@ def build_custom_links(source) link_instructions.each do |link| key = link.first - type = link.last[:type] + link_info = link.last + type = link_info[:type] ext = link.last[:ext] - custom_links[key] = send("build_custom_#{type}_link", key, source, ext) + custom_links[key] = send("build_custom_#{type}_link", key, source, link_info) end custom_links @@ -66,13 +67,20 @@ def build_custom_links(source) private - def build_custom_self_link(key, source, ext=nil) - url = "#{ self_link(source) }/#{ key }" - url += ".#{ext}" if ext + def build_custom_self_link(key, source, link_info={}) + extension = link_info[:ext] + path = link_info[:relative_path] || key + + url = "#{ self_link(source) }/#{ path }" + url += ".#{extension}" if extension url end + # method for building custom links from relationships + def build_custom_relationship_link(key, source, link_info={}) + end + def build_engine_name scopes = module_scopes_from_class(primary_resource_klass) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index d7ec9a188..2c6148ceb 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -372,9 +372,12 @@ def custom_links def custom_link(name, type, options={}) link_manifest = { type: type.to_sym } extension = options[:ext] || options[:extension] + relative_path = options[:path] || options[:relative_path] + link_manifest.merge!(ext: extension) if extension + link_manifest.merge!(relative_path: relative_path) if relative_path - @_custom_links[name.to_sym] = link_manifest + @_custom_links[name] = link_manifest end def create(context) diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index 59c0cefe2..f975ff5d2 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1142,6 +1142,24 @@ def subject custom_link :raw, :self 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, :self, relative_path: "super/duper/path", ext: :xml +end + + module Api module V1 class WriterResource < JSONAPI::Resource diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index 0b3bb3939..e95d2dc45 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -92,6 +92,13 @@ class CustomLinkTestWithExtensionResource < JSONAPI::Resource custom_link :raw, :self, ext: :xml end +class CustomLinkTestWithCustomPathResource < JSONAPI::Resource + model_name 'Post' + + custom_link :raw, :self, relative_path: "super/duper/custom/path", ext: :xml +end + + class ResourceTest < ActiveSupport::TestCase def setup @post = Post.first @@ -111,6 +118,11 @@ def test_custom_links_with_extension assert_equal(CustomLinkTestWithExtensionResource.new(Post.first, {}).custom_links, registered_custom_link) end + def test_custom_links_with_relative_path_and_extension + registered_custom_link = { raw: { type: :self, ext: :xml, relative_path: "super/duper/custom/path" } } + assert_equal(CustomLinkTestWithCustomPathResource.new(Post.first, {}).custom_links, registered_custom_link) + end + def test_model_name assert_equal("Post", PostResource._model_name) end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index 29ee01c4b..92600c64b 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1978,4 +1978,49 @@ def test_custom_links assert_hash_equals(custom_link_spec, serialized_custom_link_resource) end + + def test_custom_links_with_custom_relative_paths + serialized_custom_link_resource = JSONAPI::ResourceSerializer + .new(CustomLinkWithRelativePathOptionResource, base_url: 'http://example.com') + .serialize_to_hash(CustomLinkWithRelativePathOptionResource.new(Post.first)) + + custom_link_spec = { + data: { + type: 'customLinkWithRelativePathOptions', + id: '1', + attributes: { + title: "New post", + body: "A body!!!", + subject: "New post" + }, + links: { + self: "http://example.com/customLinkWithRelativePathOptions/1", + raw: "http://example.com/customLinkWithRelativePathOptions/1/super/duper/path.xml" + }, + relationships: { + writer: { + links: { + self: "http://example.com/customLinkWithRelativePathOptions/1/relationships/writer", + related: "http://example.com/customLinkWithRelativePathOptions/1/writer" + } + }, + section: { + links: { + self: "http://example.com/customLinkWithRelativePathOptions/1/relationships/section", + related: "http://example.com/customLinkWithRelativePathOptions/1/section" + } + }, + comments: { + links: { + self: "http://example.com/customLinkWithRelativePathOptions/1/relationships/comments", + related: "http://example.com/customLinkWithRelativePathOptions/1/comments" + } + } + } + } + } + + assert_hash_equals(custom_link_spec, serialized_custom_link_resource) + end + end From b5704e84b6162bb215a2da6971fb6fc9e5e61578 Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Mon, 12 Oct 2015 14:35:10 -0400 Subject: [PATCH 03/10] adds option which allows a custom lambda that can call methods on a Resource instance to build a custom url --- lib/jsonapi/link_builder.rb | 27 +++-- lib/jsonapi/resource.rb | 25 +++-- test/fixtures/active_record.rb | 35 ++++++ test/unit/resource/resource_test.rb | 20 +++- test/unit/serializer/serializer_test.rb | 138 +++++++++++++++++++++++- 5 files changed, 226 insertions(+), 19 deletions(-) diff --git a/lib/jsonapi/link_builder.rb b/lib/jsonapi/link_builder.rb index 2f9dc2677..fde897d03 100644 --- a/lib/jsonapi/link_builder.rb +++ b/lib/jsonapi/link_builder.rb @@ -54,12 +54,21 @@ def build_custom_links(source) custom_links = {} link_instructions.each do |link| - key = link.first link_info = link.last + + key = link_info[:name] type = link_info[:type] - ext = link.last[:ext] + ext = link_info[:ext] + if_condition = link_info[:if] + next if if_condition && if_condition.call(source) == false + + case type.to_sym + when :self + custom_links[key] = self_link_extension(key, source, link_info) + when :custom + custom_links[key] = build_custom_link(key, source, link_info) + end - custom_links[key] = send("build_custom_#{type}_link", key, source, link_info) end custom_links @@ -67,9 +76,9 @@ def build_custom_links(source) private - def build_custom_self_link(key, source, link_info={}) + def self_link_extension(key, source, link_info={}) extension = link_info[:ext] - path = link_info[:relative_path] || key + path = link_info[:path] || key url = "#{ self_link(source) }/#{ path }" url += ".#{extension}" if extension @@ -77,8 +86,12 @@ def build_custom_self_link(key, source, link_info={}) url end - # method for building custom links from relationships - def build_custom_relationship_link(key, source, link_info={}) + def build_custom_link(key, source, link_info) + extension = link_info[:ext] + url = link_info[:with].call(source) + url += ".#{ extension }" if extension + + url end def build_engine_name diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 2c6148ceb..7d970fffd 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -153,7 +153,7 @@ def meta(_options) end def custom_links? - self.class.custom_links.size > 0 + custom_links.size > 0 end def custom_links @@ -370,14 +370,25 @@ def custom_links end def custom_link(name, type, options={}) - link_manifest = { type: type.to_sym } - extension = options[:ext] || options[:extension] - relative_path = options[:path] || options[:relative_path] + link_manifest = { name: name.to_sym, type: type.to_sym } - link_manifest.merge!(ext: extension) if extension - link_manifest.merge!(relative_path: relative_path) if relative_path + ext = options[:ext] || options[:extension] + path = options[:path] || options[:relative_path] + url = options[:url] + with = options[:with] + if_condition = options[:if] - @_custom_links[name] = link_manifest + if type == :custom && with.nil? + warn "#{ self.class.name }'s custom link #{name} needs a a lambda passed-in as the 'if' option, like `if: ->(instance) { # do stuff here }`" + end + + link_manifest.merge!(ext: ext) if ext + link_manifest.merge!(path: path) if path && !url + link_manifest.merge!(url: url) if url + link_manifest.merge!(with: with) if with + link_manifest.merge!(if: if_condition) if if_condition + + @_custom_links[name.to_sym] = link_manifest end def create(context) diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index f975ff5d2..f83f6700d 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1159,6 +1159,41 @@ def subject custom_link :raw, :self, relative_path: "super/duper/path", ext: :xml 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, :self, ext: :json, path: 'conditional/link', if: ->(instance) { instance.title == "JR Solves your serialization woes!" } +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, :custom, + with: ->(instance) { "http://external-api.com/posts/#{ instance.created_at.year }/#{ instance.created_at.month }/#{ instance.created_at.day }-#{ instance.subject.gsub(' ', '-') }"} +end + module Api module V1 diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index e95d2dc45..85ef40e32 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -95,9 +95,14 @@ class CustomLinkTestWithExtensionResource < JSONAPI::Resource class CustomLinkTestWithCustomPathResource < JSONAPI::Resource model_name 'Post' - custom_link :raw, :self, relative_path: "super/duper/custom/path", ext: :xml + custom_link :raw, :self, path: "super/duper/custom/path", ext: :xml end +class CustomLinkLambda< JSONAPI::Resource + model_name 'Post' + + custom_link :raw, :custom, with: ->(instance) { "http://external-api/posts/" + "#{ instance.created_at.year }/#{ instance.created_at.month}/#{ instance.created_at.day }" } +end class ResourceTest < ActiveSupport::TestCase def setup @@ -109,20 +114,27 @@ def test_custom_links? end def test_custom_links - registered_custom_link = { raw: { :type => :self } } + registered_custom_link = { raw: { name: :raw, type: :self } } assert_equal(CustomLinkTestResource.new(Post.first, {}).custom_links, registered_custom_link) end def test_custom_links_with_extension - registered_custom_link = { raw: { type: :self, ext: :xml } } + registered_custom_link = { raw: { name: :raw, type: :self, ext: :xml } } assert_equal(CustomLinkTestWithExtensionResource.new(Post.first, {}).custom_links, registered_custom_link) end def test_custom_links_with_relative_path_and_extension - registered_custom_link = { raw: { type: :self, ext: :xml, relative_path: "super/duper/custom/path" } } + registered_custom_link = { raw: { name: :raw, type: :self, ext: :xml, path: "super/duper/custom/path" } } assert_equal(CustomLinkTestWithCustomPathResource.new(Post.first, {}).custom_links, registered_custom_link) end + def test_custom_link_built_with_lambda + created_at = Post.first.created_at + custom_exteranl_url = CustomLinkLambda.new(Post.first, {}).custom_links[:raw][:with].call(Post.first) + + assert_equal(custom_exteranl_url, "http://external-api/posts/#{created_at.year}/#{created_at.month}/#{created_at.day}") + end + def test_model_name assert_equal("Post", PostResource._model_name) end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index 92600c64b..ed28d0eeb 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1982,7 +1982,7 @@ def test_custom_links def test_custom_links_with_custom_relative_paths serialized_custom_link_resource = JSONAPI::ResourceSerializer .new(CustomLinkWithRelativePathOptionResource, base_url: 'http://example.com') - .serialize_to_hash(CustomLinkWithRelativePathOptionResource.new(Post.first)) + .serialize_to_hash(CustomLinkWithRelativePathOptionResource.new(Post.first, {})) custom_link_spec = { data: { @@ -2023,4 +2023,140 @@ def test_custom_links_with_custom_relative_paths assert_hash_equals(custom_link_spec, serialized_custom_link_resource) end + def test_custom_links_with_if_condition_equals_false + serialized_custom_link_resource = JSONAPI::ResourceSerializer + .new(CustomLinkWithIfCondition, base_url: 'http://example.com') + .serialize_to_hash(CustomLinkWithIfCondition.new(Post.first, {})) + + custom_link_spec = { + data: { + type: 'customLinkWithIfConditions', + id: '1', + attributes: { + title: "New post", + body: "A body!!!", + subject: "New post" + }, + links: { + self: "http://example.com/customLinkWithIfConditions/1" + }, + relationships: { + writer: { + links: { + self: "http://example.com/customLinkWithIfConditions/1/relationships/writer", + related: "http://example.com/customLinkWithIfConditions/1/writer" + } + }, + section: { + links: { + self: "http://example.com/customLinkWithIfConditions/1/relationships/section", + related: "http://example.com/customLinkWithIfConditions/1/section" + } + }, + comments: { + links: { + self: "http://example.com/customLinkWithIfConditions/1/relationships/comments", + related: "http://example.com/customLinkWithIfConditions/1/comments" + } + } + } + } + } + + assert_hash_equals(custom_link_spec, serialized_custom_link_resource) + end + + def test_custom_links_with_if_condition_equals_true + serialized_custom_link_resource = JSONAPI::ResourceSerializer + .new(CustomLinkWithIfCondition, base_url: 'http://example.com') + .serialize_to_hash(CustomLinkWithIfCondition.new(Post.find_by(title: "JR Solves your serialization woes!"), {})) + + custom_link_spec = { + data: { + type: 'customLinkWithIfConditions', + id: '2', + attributes: { + title: "JR Solves your serialization woes!", + body: "Use JR", + subject: "JR Solves your serialization woes!" + }, + links: { + self: "http://example.com/customLinkWithIfConditions/2", + conditional_custom_link: "http://example.com/customLinkWithIfConditions/2/conditional/link.json" + }, + relationships: { + writer: { + links: { + self: "http://example.com/customLinkWithIfConditions/2/relationships/writer", + related: "http://example.com/customLinkWithIfConditions/2/writer" + } + }, + section: { + links: { + self: "http://example.com/customLinkWithIfConditions/2/relationships/section", + related: "http://example.com/customLinkWithIfConditions/2/section" + } + }, + comments: { + links: { + self: "http://example.com/customLinkWithIfConditions/2/relationships/comments", + related: "http://example.com/customLinkWithIfConditions/2/comments" + } + } + } + } + } + + assert_hash_equals(custom_link_spec, serialized_custom_link_resource) + end + + + def test_custom_links_with_lambda + # custom link is based on created_at timestamp of Post + post_created_at = Post.first.created_at + serialized_custom_link_resource = JSONAPI::ResourceSerializer + .new(CustomLinkWithLambda, base_url: 'http://example.com') + .serialize_to_hash(CustomLinkWithLambda.new(Post.first, {})) + + custom_link_spec = { + data: { + type: 'customLinkWithLambdas', + id: '1', + attributes: { + title: "New post", + body: "A body!!!", + subject: "New post", + createdAt: post_created_at + }, + links: { + self: "http://example.com/customLinkWithLambdas/1", + link_to_external_api: "http://external-api.com/posts/#{post_created_at.year}/#{post_created_at.month}/#{post_created_at.day}-New-post" + }, + relationships: { + writer: { + links: { + self: "http://example.com/customLinkWithLambdas/1/relationships/writer", + related: "http://example.com/customLinkWithLambdas/1/writer" + } + }, + section: { + links: { + self: "http://example.com/customLinkWithLambdas/1/relationships/section", + related: "http://example.com/customLinkWithLambdas/1/section" + } + }, + comments: { + links: { + self: "http://example.com/customLinkWithLambdas/1/relationships/comments", + related: "http://example.com/customLinkWithLambdas/1/comments" + } + } + } + } + } + + assert_hash_equals(custom_link_spec, serialized_custom_link_resource) + end + + end From f7dee7f8c97aca764658533e70f8be84b8998d0b Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Fri, 23 Oct 2015 22:14:13 -0400 Subject: [PATCH 04/10] simplifies custom link interface to take a symbol key and a lambda that takes a JSONAPI::Resource instance and a link_builder --- lib/jsonapi/link_builder.rb | 20 ++----------- lib/jsonapi/resource.rb | 30 ++----------------- lib/jsonapi/resource_serializer.rb | 2 +- test/fixtures/active_record.rb | 19 ++++++++---- test/unit/resource/resource_test.rb | 39 ++++++------------------- test/unit/serializer/serializer_test.rb | 25 ++++++++-------- 6 files changed, 41 insertions(+), 94 deletions(-) diff --git a/lib/jsonapi/link_builder.rb b/lib/jsonapi/link_builder.rb index fde897d03..95b0e6538 100644 --- a/lib/jsonapi/link_builder.rb +++ b/lib/jsonapi/link_builder.rb @@ -50,25 +50,11 @@ def self_link(source) end def build_custom_links(source) - link_instructions = source.custom_links + link_instructions = source.class.custom_links custom_links = {} - link_instructions.each do |link| - link_info = link.last - - key = link_info[:name] - type = link_info[:type] - ext = link_info[:ext] - if_condition = link_info[:if] - next if if_condition && if_condition.call(source) == false - - case type.to_sym - when :self - custom_links[key] = self_link_extension(key, source, link_info) - when :custom - custom_links[key] = build_custom_link(key, source, link_info) - end - + link_instructions.each do |key, custom_link_lambda| + custom_links[key] = custom_link_lambda.call(source, self) end custom_links diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 7d970fffd..a5ae609e5 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -152,14 +152,6 @@ def meta(_options) {} end - def custom_links? - custom_links.size > 0 - end - - def custom_links - @custom_links ||= self.class.custom_links - end - private def save @@ -369,26 +361,8 @@ def custom_links @_custom_links ||= {} end - def custom_link(name, type, options={}) - link_manifest = { name: name.to_sym, type: type.to_sym } - - ext = options[:ext] || options[:extension] - path = options[:path] || options[:relative_path] - url = options[:url] - with = options[:with] - if_condition = options[:if] - - if type == :custom && with.nil? - warn "#{ self.class.name }'s custom link #{name} needs a a lambda passed-in as the 'if' option, like `if: ->(instance) { # do stuff here }`" - end - - link_manifest.merge!(ext: ext) if ext - link_manifest.merge!(path: path) if path && !url - link_manifest.merge!(url: url) if url - link_manifest.merge!(with: with) if with - link_manifest.merge!(if: if_condition) if if_condition - - @_custom_links[name.to_sym] = link_manifest + def custom_link(name, func) + @_custom_links[name.to_sym] = func end def create(context) diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index 1e7a58511..c075edb00 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -221,7 +221,7 @@ def relationship_links(source) links = {} links[:self] = link_builder.self_link(source) - if source.custom_links? + if source.class.custom_links customized_links = link_builder.build_custom_links(source) links.merge!(customized_links) end diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index f83f6700d..ee3cc746b 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1125,7 +1125,7 @@ class AuthorDetailResource < JSONAPI::Resource attributes :author_stuff end -class CustomLinkResource < JSONAPI::Resource +class SimpleCustomLinkResource < JSONAPI::Resource model_name 'Post' attributes :title, :body, :subject @@ -1139,7 +1139,7 @@ def subject filters :writer - custom_link :raw, :self + custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw" } end class CustomLinkWithRelativePathOptionResource < JSONAPI::Resource @@ -1156,7 +1156,9 @@ def subject filters :writer - custom_link :raw, :self, relative_path: "super/duper/path", ext: :xml + custom_link :raw, ->(source, link_builder) do + link_builder.self_link(source) + "/super/duper/path.xml" + end end class CustomLinkWithIfCondition < JSONAPI::Resource @@ -1173,7 +1175,11 @@ def subject filters :writer - custom_link :conditional_custom_link, :self, ext: :json, path: 'conditional/link', if: ->(instance) { instance.title == "JR Solves your serialization woes!" } + 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 @@ -1190,8 +1196,9 @@ def subject filters :writer - custom_link :link_to_external_api, :custom, - with: ->(instance) { "http://external-api.com/posts/#{ instance.created_at.year }/#{ instance.created_at.month }/#{ instance.created_at.day }-#{ instance.subject.gsub(' ', '-') }"} + 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 diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index 85ef40e32..9caaad5be 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -83,25 +83,30 @@ class RelatedResource < MyModule::RelatedResource class CustomLinkTestResource < JSONAPI::Resource model_name 'Post' - custom_link :raw, :self + custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw" } end class CustomLinkTestWithExtensionResource < JSONAPI::Resource model_name 'Post' - custom_link :raw, :self, ext: :xml + custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw.xml" } end class CustomLinkTestWithCustomPathResource < JSONAPI::Resource model_name 'Post' - custom_link :raw, :self, path: "super/duper/custom/path", ext: :xml + 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, :custom, with: ->(instance) { "http://external-api/posts/" + "#{ instance.created_at.year }/#{ instance.created_at.month}/#{ instance.created_at.day }" } + 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 @@ -109,32 +114,6 @@ def setup @post = Post.first end - def test_custom_links? - assert_equal(CustomLinkTestResource.new(Post.first, {}).custom_links?, true) - end - - def test_custom_links - registered_custom_link = { raw: { name: :raw, type: :self } } - assert_equal(CustomLinkTestResource.new(Post.first, {}).custom_links, registered_custom_link) - end - - def test_custom_links_with_extension - registered_custom_link = { raw: { name: :raw, type: :self, ext: :xml } } - assert_equal(CustomLinkTestWithExtensionResource.new(Post.first, {}).custom_links, registered_custom_link) - end - - def test_custom_links_with_relative_path_and_extension - registered_custom_link = { raw: { name: :raw, type: :self, ext: :xml, path: "super/duper/custom/path" } } - assert_equal(CustomLinkTestWithCustomPathResource.new(Post.first, {}).custom_links, registered_custom_link) - end - - def test_custom_link_built_with_lambda - created_at = Post.first.created_at - custom_exteranl_url = CustomLinkLambda.new(Post.first, {}).custom_links[:raw][:with].call(Post.first) - - assert_equal(custom_exteranl_url, "http://external-api/posts/#{created_at.year}/#{created_at.month}/#{created_at.day}") - end - def test_model_name assert_equal("Post", PostResource._model_name) end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index ed28d0eeb..a07e979ec 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1937,12 +1937,12 @@ class ::Questionable2Resource < JSONAPI::Resource ) end - def test_custom_links - serialized_custom_link_resource = JSONAPI::ResourceSerializer.new(CustomLinkResource, base_url: 'http://example.com').serialize_to_hash(CustomLinkResource.new(Post.first, {})) + def test_simple_custom_links + serialized_custom_link_resource = JSONAPI::ResourceSerializer.new(SimpleCustomLinkResource, base_url: 'http://example.com').serialize_to_hash(SimpleCustomLinkResource.new(Post.first, {})) custom_link_spec = { data: { - type: 'customLinks', + type: 'simpleCustomLinks', id: '1', attributes: { title: "New post", @@ -1950,26 +1950,26 @@ def test_custom_links subject: "New post" }, links: { - self: "http://example.com/customLinks/1", - raw: "http://example.com/customLinks/1/raw" + self: "http://example.com/simpleCustomLinks/1", + raw: "http://example.com/simpleCustomLinks/1/raw" }, relationships: { writer: { links: { - self: "http://example.com/customLinks/1/relationships/writer", - related: "http://example.com/customLinks/1/writer" + self: "http://example.com/simpleCustomLinks/1/relationships/writer", + related: "http://example.com/simpleCustomLinks/1/writer" } }, section: { links: { - self: "http://example.com/customLinks/1/relationships/section", - related: "http://example.com/customLinks/1/section" + self: "http://example.com/simpleCustomLinks/1/relationships/section", + related: "http://example.com/simpleCustomLinks/1/section" } }, comments: { links: { - self: "http://example.com/customLinks/1/relationships/comments", - related: "http://example.com/customLinks/1/comments" + self: "http://example.com/simpleCustomLinks/1/relationships/comments", + related: "http://example.com/simpleCustomLinks/1/comments" } } } @@ -2038,7 +2038,8 @@ def test_custom_links_with_if_condition_equals_false subject: "New post" }, links: { - self: "http://example.com/customLinkWithIfConditions/1" + self: "http://example.com/customLinkWithIfConditions/1", + conditional_custom_link: nil }, relationships: { writer: { From ec43644806d1198ee3b6e89d3400292383e0ce63 Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Fri, 23 Oct 2015 22:35:27 -0400 Subject: [PATCH 05/10] updates READMA --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/README.md b/README.md index da2db61dc..9e5cf45d8 100644 --- a/README.md +++ b/README.md @@ -395,6 +395,74 @@ Model hints inherit from parent resources, but are not global in scope. The `mod `resource` named parameters. `model` takes an ActiveRecord class or class name (defaults to the model name), and `resource` takes a resource type or a resource class (defaults to the current resource's type). +#### 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` From 77da66d707f7955ba868e1e0c6b000018540cdb6 Mon Sep 17 00:00:00 2001 From: Reid Beels Date: Mon, 14 Dec 2015 21:50:33 +0100 Subject: [PATCH 06/10] Make ResourceSerializer private API more consistent --- lib/jsonapi/resource_serializer.rb | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index c075edb00..97a50f656 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -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 @@ -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? @@ -159,7 +158,12 @@ 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 relationships_hash(source, include_directives) relationships = source.class._relationships requested = requested_fields(source.class) fields = relationships.keys @@ -197,7 +201,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) @@ -208,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 end @@ -217,7 +221,7 @@ def relationship_data(source, include_directives) end end - def relationship_links(source) + def links_hash(source) links = {} links[:self] = link_builder.self_link(source) From 20f1f810ab780ee0469b0b9687e15ff65f3ee061 Mon Sep 17 00:00:00 2001 From: Reid Beels Date: Mon, 14 Dec 2015 22:01:58 +0100 Subject: [PATCH 07/10] Use `custom_generation_options` for custom_link generation --- lib/jsonapi/link_builder.rb | 11 ----------- lib/jsonapi/resource_serializer.rb | 17 +++++++++++------ test/fixtures/active_record.rb | 12 ++++++------ 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/lib/jsonapi/link_builder.rb b/lib/jsonapi/link_builder.rb index 95b0e6538..a29224647 100644 --- a/lib/jsonapi/link_builder.rb +++ b/lib/jsonapi/link_builder.rb @@ -49,17 +49,6 @@ 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={}) diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index 97a50f656..886890397 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -222,15 +222,20 @@ def relationships_hash(source, include_directives) end def links_hash(source) - links = {} - links[:self] = link_builder.self_link(source) + custom_links_hash(source).merge( + self: link_builder.self_link(source) + ) + end + + def custom_links_hash(source) + link_instructions = source.class.custom_links || {} + custom_links = {} - if source.class.custom_links - customized_links = link_builder.build_custom_links(source) - links.merge!(customized_links) + link_instructions.each do |key, custom_link_lambda| + custom_links[key] = custom_link_lambda.call(source, custom_generation_options) end - links + custom_links end def already_serialized?(type, id) diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index ee3cc746b..53227fd27 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1139,7 +1139,7 @@ def subject filters :writer - custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw" } + custom_link :raw, ->(source, options) { options[:serializer].link_builder.self_link(source) + "/raw" } end class CustomLinkWithRelativePathOptionResource < JSONAPI::Resource @@ -1156,8 +1156,8 @@ def subject filters :writer - custom_link :raw, ->(source, link_builder) do - link_builder.self_link(source) + "/super/duper/path.xml" + custom_link :raw, ->(source, options) do + options[:serializer].link_builder.self_link(source) + "/super/duper/path.xml" end end @@ -1175,9 +1175,9 @@ def subject filters :writer - custom_link :conditional_custom_link, ->(source, link_builder) do + custom_link :conditional_custom_link, ->(source, options) do if source.title == "JR Solves your serialization woes!" - link_builder.self_link(source) + "/conditional/link.json" + options[:serializer].link_builder.self_link(source) + "/conditional/link.json" end end end @@ -1196,7 +1196,7 @@ def subject filters :writer - custom_link :link_to_external_api, ->(source, link_builder) do + custom_link :link_to_external_api, ->(source, options) do "http://external-api.com/posts/#{ source.created_at.year }/#{ source.created_at.month }/#{ source.created_at.day }-#{ source.subject.gsub(' ', '-') }" end end From 5d91808a4ca13a3db4a3d1d76c1cc9a2a48a38a1 Mon Sep 17 00:00:00 2001 From: Reid Beels Date: Tue, 15 Dec 2015 16:32:54 +0100 Subject: [PATCH 08/10] Simplify custom links as an override similar to Resource#meta --- lib/jsonapi/link_builder.rb | 18 ---------------- lib/jsonapi/resource.rb | 20 +++++++++--------- lib/jsonapi/resource_serializer.rb | 28 ++++++++++--------------- test/fixtures/active_record.rb | 20 +++++++++++------- test/unit/resource/resource_test.rb | 19 ++++++++++------- test/unit/serializer/serializer_test.rb | 1 - 6 files changed, 45 insertions(+), 61 deletions(-) diff --git a/lib/jsonapi/link_builder.rb b/lib/jsonapi/link_builder.rb index a29224647..2ca5fef90 100644 --- a/lib/jsonapi/link_builder.rb +++ b/lib/jsonapi/link_builder.rb @@ -51,24 +51,6 @@ def self_link(source) 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) - url += ".#{ extension }" if extension - - url - end - def build_engine_name scopes = module_scopes_from_class(primary_resource_klass) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index a5ae609e5..f64560fb8 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -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 @@ -317,7 +326,6 @@ def inherited(subclass) end subclass._allowed_filters = (_allowed_filters || Set.new).dup - subclass._custom_links = (_custom_links || {}).dup type = subclass.name.demodulize.sub(/Resource$/, '').underscore subclass._type = type.pluralize.to_sym @@ -355,15 +363,7 @@ def resource_type_for(model) end end - attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_model_hints, :_custom_links - - def custom_links - @_custom_links ||= {} - end - - def custom_link(name, func) - @_custom_links[name.to_sym] = func - end + attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_model_hints def create(context) new(create_model, context) diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index 886890397..89d5407b0 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -163,6 +163,17 @@ def meta_hash(source) (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) @@ -221,23 +232,6 @@ def relationships_hash(source, include_directives) end end - def links_hash(source) - custom_links_hash(source).merge( - self: link_builder.self_link(source) - ) - end - - def custom_links_hash(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, custom_generation_options) - end - - custom_links - end - def already_serialized?(type, id) type = format_key(type) @included_objects.key?(type) && @included_objects[type].key?(id) diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index 53227fd27..d9cc2ceb4 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1139,7 +1139,9 @@ def subject filters :writer - custom_link :raw, ->(source, options) { options[:serializer].link_builder.self_link(source) + "/raw" } + def custom_links(options) + { raw: options[:serializer].link_builder.self_link(self) + "/raw" } + end end class CustomLinkWithRelativePathOptionResource < JSONAPI::Resource @@ -1156,8 +1158,8 @@ def subject filters :writer - custom_link :raw, ->(source, options) do - options[:serializer].link_builder.self_link(source) + "/super/duper/path.xml" + def custom_links(options) + { raw: options[:serializer].link_builder.self_link(self) + "/super/duper/path.xml" } end end @@ -1175,9 +1177,9 @@ def subject filters :writer - custom_link :conditional_custom_link, ->(source, options) do - if source.title == "JR Solves your serialization woes!" - options[:serializer].link_builder.self_link(source) + "/conditional/link.json" + 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 @@ -1196,8 +1198,10 @@ def subject filters :writer - custom_link :link_to_external_api, ->(source, options) do - "http://external-api.com/posts/#{ source.created_at.year }/#{ source.created_at.month }/#{ source.created_at.day }-#{ source.subject.gsub(' ', '-') }" + 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 diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index 9caaad5be..be773914b 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -83,29 +83,34 @@ class RelatedResource < MyModule::RelatedResource class CustomLinkTestResource < JSONAPI::Resource model_name 'Post' - custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw" } + def custom_links(options) + {raw: options[:serializer].link_builder.self_link(self) + "/raw"} + end end class CustomLinkTestWithExtensionResource < JSONAPI::Resource model_name 'Post' - custom_link :raw, ->(source, link_builder) { link_builder.self_link(source) + "/raw.xml" } + def custom_links(options) + {raw: options[:serializer].link_builder.self_link(self) + "/raw.xml"} + end end class CustomLinkTestWithCustomPathResource < JSONAPI::Resource model_name 'Post' - custom_link :raw, ->(source, link_builder) do - link_builder.self_link(source) + "/super/duper/path.xml" + def custom_links(options) + {raw: options[:serializer].link_builder.self_link(self) + "/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 }" + def custom_links(options) + { + raw: "http://external-api/posts/#{ created_at.year }/#{ created_at.month}/#{ created_at.day }" + } end end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index a07e979ec..117831662 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -2039,7 +2039,6 @@ def test_custom_links_with_if_condition_equals_false }, links: { self: "http://example.com/customLinkWithIfConditions/1", - conditional_custom_link: nil }, relationships: { writer: { From 22b2f8f65556c86f951ed57f5be4b709b08b026a Mon Sep 17 00:00:00 2001 From: Reid Beels Date: Tue, 15 Dec 2015 17:34:12 +0100 Subject: [PATCH 09/10] Update README for new custom_links implementation --- README.md | 135 +++++++++++++++++++++++++++--------------------------- 1 file changed, 67 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 9e5cf45d8..d6d284f53 100644 --- a/README.md +++ b/README.md @@ -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) @@ -395,74 +396,6 @@ Model hints inherit from parent resources, but are not global in scope. The `mod `resource` named parameters. `model` takes an ActiveRecord class or class name (defaults to the model name), and `resource` takes a resource type or a resource class (defaults to the current resource's type). -#### 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` @@ -997,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 From 9ff1fd5242a8934a3fe34478e3c6f25ebf259263 Mon Sep 17 00:00:00 2001 From: Reid Beels Date: Wed, 10 Feb 2016 12:45:27 +0100 Subject: [PATCH 10/10] Remove unused CustomLink test resources --- test/unit/resource/resource_test.rb | 34 ----------------------------- 1 file changed, 34 deletions(-) diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index be773914b..ef3132ec0 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -80,40 +80,6 @@ class RelatedResource < MyModule::RelatedResource end end -class CustomLinkTestResource < JSONAPI::Resource - model_name 'Post' - - def custom_links(options) - {raw: options[:serializer].link_builder.self_link(self) + "/raw"} - end -end - -class CustomLinkTestWithExtensionResource < JSONAPI::Resource - model_name 'Post' - - def custom_links(options) - {raw: options[:serializer].link_builder.self_link(self) + "/raw.xml"} - end -end - -class CustomLinkTestWithCustomPathResource < JSONAPI::Resource - model_name 'Post' - - def custom_links(options) - {raw: options[:serializer].link_builder.self_link(self) + "/super/duper/path.xml"} - end -end - -class CustomLinkLambda< JSONAPI::Resource - model_name 'Post' - - def custom_links(options) - { - raw: "http://external-api/posts/#{ created_at.year }/#{ created_at.month}/#{ created_at.day }" - } - end -end - class ResourceTest < ActiveSupport::TestCase def setup @post = Post.first