From cc109284725b35977dd16ea2279c882a25114c5d Mon Sep 17 00:00:00 2001 From: Ben Mills Date: Tue, 1 Mar 2016 19:23:55 -0700 Subject: [PATCH] Provide Rails url_helpers via SerializationContext --- CHANGELOG.md | 2 + Rakefile | 30 +++++----- docs/general/getting_started.md | 9 +++ docs/general/rendering.md | 32 +++++++++- docs/general/serializers.md | 10 ++-- lib/active_model/serializer/links.rb | 6 +- .../adapter/json_api/link.rb | 5 ++ lib/active_model_serializers/railtie.rb | 5 ++ .../serialization_context.rb | 16 ++++- .../railtie_test_isolated.rb | 6 ++ .../serialization_context_test.rb | 18 ------ .../serialization_context_test_isolated.rb | 58 +++++++++++++++++++ test/adapter/json_api/links_test.rb | 29 ++++++---- test/support/isolated_unit.rb | 1 + test/support/rails_app.rb | 2 + 15 files changed, 177 insertions(+), 52 deletions(-) delete mode 100644 test/active_model_serializers/serialization_context_test.rb create mode 100644 test/active_model_serializers/serialization_context_test_isolated.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1466f340c..972be1252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ Breaking changes: Features: +- [#1550](https://github.com/rails-api/active_model_serializers/pull/1550) Add + Rails url_helpers to `SerializationContext` for use in links. (@remear, @bf4) - [#1004](https://github.com/rails-api/active_model_serializers/pull/1004) JSON API errors object implementation. - Only implements `detail` and `source` as derived from `ActiveModel::Error` - Provides checklist of remaining questions and remaining parts of the spec. diff --git a/Rakefile b/Rakefile index 94de4fe5e..04c28a1a8 100644 --- a/Rakefile +++ b/Rakefile @@ -45,25 +45,23 @@ Rake::TestTask.new do |t| end desc 'Run isolated tests' -task isolated: ['test:isolated:railtie'] +task isolated: ['test:isolated'] namespace :test do - namespace :isolated do + task :isolated do desc 'Run isolated tests for Railtie' - task :railtie do - require 'shellwords' - dir = File.dirname(__FILE__) - file = Shellwords.shellescape("#{dir}/test/active_model_serializers/railtie_test_isolated.rb") - dir = Shellwords.shellescape(dir) - - # https://github.com/rails/rails/blob/3d590add45/railties/lib/rails/generators/app_base.rb#L345-L363 - _bundle_command = Gem.bin_path('bundler', 'bundle') - require 'bundler' - Bundler.with_clean_env do - command = "-w -I#{dir}/lib -I#{dir}/test #{file}" + require 'shellwords' + dir = File.dirname(__FILE__) + dir = Shellwords.shellescape(dir) + isolated_test_files = FileList['test/**/*_test_isolated.rb'] + # https://github.com/rails/rails/blob/3d590add45/railties/lib/rails/generators/app_base.rb#L345-L363 + _bundle_command = Gem.bin_path('bundler', 'bundle') + require 'bundler' + Bundler.with_clean_env do + isolated_test_files.all? do |test_file| + command = "-w -I#{dir}/lib -I#{dir}/test #{Shellwords.shellescape(test_file)}" full_command = %("#{Gem.ruby}" #{command}) - system(full_command) or # rubocop:disable Style/AndOr - fail 'Failures' - end + system(full_command) + end or fail 'Failures' # rubocop:disable Style/AndOr end end end diff --git a/docs/general/getting_started.md b/docs/general/getting_started.md index cd207159a..d9d08ae35 100644 --- a/docs/general/getting_started.md +++ b/docs/general/getting_started.md @@ -96,3 +96,12 @@ class PostsController < ApplicationController end ``` + +If you wish to use Rails url helpers for link generation, e.g., `link(:resources) { resources_url }`, ensure your application sets +`Rails.application.routes.default_url_options`. + +```ruby +Rails.application.routes.default_url_options = { + host: 'example.com' +} +``` diff --git a/docs/general/rendering.md b/docs/general/rendering.md index b83493257..0f79321f7 100644 --- a/docs/general/rendering.md +++ b/docs/general/rendering.md @@ -103,7 +103,10 @@ PR please :) #### links -##### How to add top-level links +If you wish to use Rails url helpers for link generation, e.g., `link(:resources) { resources_url }`, ensure your application sets +`Rails.application.routes.default_url_options`. + +##### Top-level JsonApi supports a [links object](http://jsonapi.org/format/#document-links) to be specified at top-level, that you can specify in the `render`: @@ -144,6 +147,33 @@ That's the result: This feature is specific to JsonApi, so you have to use the use the [JsonApi Adapter](adapters.md#jsonapi) + +##### Resource-level + +In your serializer, define each link in one of the following methods: + +As a static string + +```ruby +link :link_name, 'https://example.com/resource' +``` + +As a block to be evaluated. When using Rails, URL helpers are available. +Ensure your application sets `Rails.application.routes.default_url_options`. + +```ruby +link :link_name_ do + "https://example.com/resource/#{object.id}" +end + +link(:link_name) { "https://example.com/resource/#{object.id}" } + +link(:link_name) { resource_url(object) } + +link(:link_name) { url_for(controller: 'controller_name', action: 'index', only_path: false) } + +``` + ### serializer_opts #### include diff --git a/docs/general/serializers.md b/docs/general/serializers.md index 65ccaa1a7..d4a9e757b 100644 --- a/docs/general/serializers.md +++ b/docs/general/serializers.md @@ -135,13 +135,15 @@ With the `:json_api` adapter, the previous serializers would be rendered as: #### ::link -e.g. - ```ruby -link :other, 'https://example.com/resource' link :self do - href "https://example.com/link_author/#{object.id}" + href "https://example.com/link_author/#{object.id}" end +link :author { link_author_url(object) } +link :link_authors { link_authors_url } +link :other, 'https://example.com/resource' +link :posts { link_author_posts_url(object) } +``` ``` #### #object diff --git a/lib/active_model/serializer/links.rb b/lib/active_model/serializer/links.rb index c079d4e17..1322adb0a 100644 --- a/lib/active_model/serializer/links.rb +++ b/lib/active_model/serializer/links.rb @@ -20,9 +20,11 @@ def inherited(base) # Define a link on a serializer. # @example - # link :self { "//example.com/posts/#{object.id}" } + # link(:self) { resource_url(object) } # @example - # link :self, "//example.com/user" + # link(:self) { "http://example.com/resource/#{object.id}" } + # @example + # link :resource, "http://example.com/resource" # def link(name, value = nil, &block) _links[name] = block || value diff --git a/lib/active_model_serializers/adapter/json_api/link.rb b/lib/active_model_serializers/adapter/json_api/link.rb index 3408a98e2..255f875a5 100644 --- a/lib/active_model_serializers/adapter/json_api/link.rb +++ b/lib/active_model_serializers/adapter/json_api/link.rb @@ -1,7 +1,12 @@ +require 'active_support/core_ext/module/delegation' + module ActiveModelSerializers module Adapter class JsonApi class Link + include SerializationContext.url_helpers + delegate :default_url_options, to: SerializationContext + def initialize(serializer, value) @object = serializer.object @scope = serializer.scope diff --git a/lib/active_model_serializers/railtie.rb b/lib/active_model_serializers/railtie.rb index 6572d9d1e..1d95ceac7 100644 --- a/lib/active_model_serializers/railtie.rb +++ b/lib/active_model_serializers/railtie.rb @@ -15,6 +15,11 @@ class Railtie < Rails::Railtie end end + initializer 'active_model_serializers.prepare_serialization_context' do + SerializationContext.url_helpers = Rails.application.routes.url_helpers + SerializationContext.default_url_options = Rails.application.routes.default_url_options + end + # This hook is run after the action_controller railtie has set the configuration # based on the *environment* configuration and before any config/initializers are run # and also before eager_loading (if enabled). diff --git a/lib/active_model_serializers/serialization_context.rb b/lib/active_model_serializers/serialization_context.rb index a373d6869..d7f8aba9e 100644 --- a/lib/active_model_serializers/serialization_context.rb +++ b/lib/active_model_serializers/serialization_context.rb @@ -1,10 +1,24 @@ module ActiveModelSerializers class SerializationContext + class << self + attr_writer :url_helpers, :default_url_options + end + attr_reader :request_url, :query_parameters - def initialize(request) + def initialize(request, options = {}) @request_url = request.original_url[/\A[^?]+/] @query_parameters = request.query_parameters + @url_helpers = options.delete(:url_helpers) || self.class.url_helpers + @default_url_options = options.delete(:default_url_options) || self.class.default_url_options + end + + def self.url_helpers + @url_helpers ||= Module.new + end + + def self.default_url_options + @default_url_options ||= {} end end end diff --git a/test/active_model_serializers/railtie_test_isolated.rb b/test/active_model_serializers/railtie_test_isolated.rb index 2e2818ed6..21f6c178e 100644 --- a/test/active_model_serializers/railtie_test_isolated.rb +++ b/test/active_model_serializers/railtie_test_isolated.rb @@ -18,6 +18,12 @@ class WithRails < RailtieTest "ActionController::Serialization should be included in ActionController::Base, but isn't" end + test 'prepares url_helpers for SerializationContext' do + assert ActiveModelSerializers::SerializationContext.url_helpers.respond_to? :url_for + assert_equal Rails.application.routes.default_url_options, + ActiveModelSerializers::SerializationContext.default_url_options + end + test 'sets the ActiveModelSerializers.logger to Rails.logger' do refute_nil Rails.logger refute_nil ActiveModelSerializers.logger diff --git a/test/active_model_serializers/serialization_context_test.rb b/test/active_model_serializers/serialization_context_test.rb deleted file mode 100644 index 940e65e5a..000000000 --- a/test/active_model_serializers/serialization_context_test.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'test_helper' - -class ActiveModelSerializers::SerializationContextTest < ActiveSupport::TestCase - def create_context - request = Minitest::Mock.new - request.expect(:original_url, 'original_url') - request.expect(:query_parameters, 'query_parameters') - - ActiveModelSerializers::SerializationContext.new(request) - end - - def test_create_context_with_request_url_and_query_parameters - context = create_context - - assert_equal context.request_url, 'original_url' - assert_equal context.query_parameters, 'query_parameters' - end -end diff --git a/test/active_model_serializers/serialization_context_test_isolated.rb b/test/active_model_serializers/serialization_context_test_isolated.rb new file mode 100644 index 000000000..981d80752 --- /dev/null +++ b/test/active_model_serializers/serialization_context_test_isolated.rb @@ -0,0 +1,58 @@ +# Execute this test in isolation +require 'support/isolated_unit' +require 'minitest/mock' + +class SerializationContextTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def create_request + request = Minitest::Mock.new + request.expect(:original_url, 'original_url') + request.expect(:query_parameters, 'query_parameters') + end + + class WithRails < SerializationContextTest + setup do + require 'rails' + require 'active_model_serializers' + make_basic_app + @context = ActiveModelSerializers::SerializationContext.new(create_request) + end + + test 'create context with request url and query parameters' do + assert_equal @context.request_url, 'original_url' + assert_equal @context.query_parameters, 'query_parameters' + end + + test 'url_helpers is set up for Rails url_helpers' do + assert_equal Module, ActiveModelSerializers::SerializationContext.url_helpers.class + assert ActiveModelSerializers::SerializationContext.url_helpers.respond_to? :url_for + end + + test 'default_url_options returns Rails.application.routes.default_url_options' do + assert_equal Rails.application.routes.default_url_options, + ActiveModelSerializers::SerializationContext.default_url_options + end + end + + class WithoutRails < SerializationContextTest + setup do + require 'active_model_serializers/serialization_context' + @context = ActiveModelSerializers::SerializationContext.new(create_request) + end + + test 'create context with request url and query parameters' do + assert_equal @context.request_url, 'original_url' + assert_equal @context.query_parameters, 'query_parameters' + end + + test 'url_helpers is a module when Rails is not present' do + assert_equal Module, ActiveModelSerializers::SerializationContext.url_helpers.class + refute ActiveModelSerializers::SerializationContext.url_helpers.respond_to? :url_for + end + + test 'default_url_options return a Hash' do + assert Hash, ActiveModelSerializers::SerializationContext.default_url_options.class + end + end +end diff --git a/test/adapter/json_api/links_test.rb b/test/adapter/json_api/links_test.rb index 09c499ed3..87f22644d 100644 --- a/test/adapter/json_api/links_test.rb +++ b/test/adapter/json_api/links_test.rb @@ -7,18 +7,24 @@ class LinksTest < ActiveSupport::TestCase LinkAuthor = Class.new(::Model) class LinkAuthorSerializer < ActiveModel::Serializer link :self do - href "//example.com/link_author/#{object.id}" + href "http://example.com/link_author/#{object.id}" meta stuff: 'value' end - - link :other, '//example.com/resource' - + link(:author) { link_author_url(object.id) } + link(:link_authors) { url_for(controller: 'link_authors', action: 'index', only_path: false) } + link(:posts) { link_author_posts_url(object.id) } + link :resource, 'http://example.com/resource' link :yet_another do - "//example.com/resource/#{object.id}" + "http://example.com/resource/#{object.id}" end end def setup + Rails.application.routes.draw do + resources :link_authors do + resources :posts + end + end @post = Post.new(id: 1337, comments: [], author: nil) @author = LinkAuthor.new(id: 1337, posts: [@post]) end @@ -29,7 +35,7 @@ def test_toplevel_links adapter: :json_api, links: { self: { - href: '//example.com/posts', + href: 'http://example.com/posts', meta: { stuff: 'value' } @@ -37,7 +43,7 @@ def test_toplevel_links }).serializable_hash expected = { self: { - href: '//example.com/posts', + href: 'http://example.com/posts', meta: { stuff: 'value' } @@ -68,13 +74,16 @@ def test_resource_links hash = serializable(@author, adapter: :json_api).serializable_hash expected = { self: { - href: '//example.com/link_author/1337', + href: 'http://example.com/link_author/1337', meta: { stuff: 'value' } }, - other: '//example.com/resource', - yet_another: '//example.com/resource/1337' + author: 'http://example.com/link_authors/1337', + link_authors: 'http://example.com/link_authors', + resource: 'http://example.com/resource', + posts: 'http://example.com/link_authors/1337/posts', + yet_another: 'http://example.com/resource/1337' } assert_equal(expected, hash[:data][:links]) end diff --git a/test/support/isolated_unit.rb b/test/support/isolated_unit.rb index 50362239d..34f186618 100644 --- a/test/support/isolated_unit.rb +++ b/test/support/isolated_unit.rb @@ -63,6 +63,7 @@ def make_basic_app # Set a fake logger to avoid creating the log directory automatically fake_logger = Logger.new(nil) config.logger = fake_logger + Rails.application.routes.default_url_options = { host: 'example.com' } end @app.respond_to?(:secrets) && @app.secrets.secret_key_base = '3b7cd727ee24e8444053437c36cc66c4' diff --git a/test/support/rails_app.rb b/test/support/rails_app.rb index ced830dc5..7f74e4ba1 100644 --- a/test/support/rails_app.rb +++ b/test/support/rails_app.rb @@ -10,6 +10,8 @@ class ActiveModelSerializers::RailsApplication < Rails::Application config.action_controller.perform_caching = true ActionController::Base.cache_store = :memory_store + + Rails.application.routes.default_url_options = { host: 'example.com' } end end ActiveModelSerializers::RailsApplication.initialize!