From 9528b23ec85bd277783fdc6a3dac3d170492d925 Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Wed, 30 Sep 2015 12:15:29 -0400 Subject: [PATCH 1/5] 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 122f3b66c..22f7e9dc9 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 5bde68d6d..a5b5e5d2d 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -118,6 +118,14 @@ def records_for(relation_name) _model.public_send relation_name end + def custom_links? + self.class.custom_links.size > 0 + end + + def custom_links + @custom_links ||= self.class.custom_links + end + private def save @@ -265,6 +273,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 @@ -283,7 +292,19 @@ 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, 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 73ee8b7f9..28c52a724 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -206,6 +206,11 @@ def relationship_links(source) links = {} links[:self] = url_generator.self_link(source) + if source.custom_links? + customized_links = url_generator.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 c27adf0de..d83a484a9 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1051,6 +1051,23 @@ class WebPageResource < JSONAPI::Resource attribute :link 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 7088fc3bd..8abc3f841 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -51,11 +51,37 @@ class MyNamespacedResource < JSONAPI::Resource 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(PostResource._model_name, 'Post') end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index 75e5928bf..362be96ae 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1871,4 +1871,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 34f64769519aaf4bea64056015d7961506c38f05 Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Wed, 30 Sep 2015 13:34:48 -0400 Subject: [PATCH 2/5] 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 22f7e9dc9..8a7324b6e 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 a5b5e5d2d..2ef77b3f7 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -301,9 +301,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 d83a484a9..298947944 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1068,6 +1068,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 8abc3f841..ba102f8b6 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -63,6 +63,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 @@ -82,6 +89,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(PostResource._model_name, 'Post') end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index 362be96ae..3f53bda76 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1913,4 +1913,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 b6c94445f15c881dd2f45b11281b2bc899bef9a8 Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Mon, 12 Oct 2015 14:35:10 -0400 Subject: [PATCH 3/5] 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 8a7324b6e..0ceb38538 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 2ef77b3f7..40463cdb7 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -119,7 +119,7 @@ def records_for(relation_name) end def custom_links? - self.class.custom_links.size > 0 + custom_links.size > 0 end def custom_links @@ -299,14 +299,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 298947944..f116f5b76 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1085,6 +1085,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 ba102f8b6..674d7ea10 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -66,9 +66,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 @@ -80,20 +85,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(PostResource._model_name, 'Post') end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index 3f53bda76..82d8f57f1 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1917,7 +1917,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: { @@ -1958,4 +1958,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 638fa18ec76a926428f43e6f51470393b9f3a689 Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Fri, 23 Oct 2015 22:14:13 -0400 Subject: [PATCH 4/5] 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 0ceb38538..8cc223141 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 40463cdb7..b6888fc9a 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -118,14 +118,6 @@ def records_for(relation_name) _model.public_send relation_name end - def custom_links? - custom_links.size > 0 - end - - def custom_links - @custom_links ||= self.class.custom_links - end - private def save @@ -298,26 +290,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 28c52a724..96a25afe5 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -206,7 +206,7 @@ def relationship_links(source) links = {} links[:self] = url_generator.self_link(source) - if source.custom_links? + if source.class.custom_links customized_links = url_generator.build_custom_links(source) links.merge!(customized_links) end diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index f116f5b76..a3f7eb138 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1051,7 +1051,7 @@ class WebPageResource < JSONAPI::Resource attribute :link end -class CustomLinkResource < JSONAPI::Resource +class SimpleCustomLinkResource < JSONAPI::Resource model_name 'Post' attributes :title, :body, :subject @@ -1065,7 +1065,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 @@ -1082,7 +1082,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 @@ -1099,7 +1101,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 @@ -1116,8 +1122,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 674d7ea10..85dc2ac3a 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -54,25 +54,30 @@ class MyNamespacedResource < JSONAPI::Resource 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 @@ -80,32 +85,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(PostResource._model_name, 'Post') end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index 82d8f57f1..f1702890d 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1872,12 +1872,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", @@ -1885,26 +1885,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" } } } @@ -1973,7 +1973,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 05cbf3ed211fc9e048e01b7c14d5934df2558c2b Mon Sep 17 00:00:00 2001 From: Alexander Harris Date: Fri, 23 Oct 2015 22:35:27 -0400 Subject: [PATCH 5/5] updates READMA --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/README.md b/README.md index 3b42a07ef..0cc142862 100644 --- a/README.md +++ b/README.md @@ -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`