From 7c397eb34717c00e12c4ca0b85b750751626c5bc Mon Sep 17 00:00:00 2001 From: Ben Mills Date: Wed, 9 Mar 2016 20:25:18 -0700 Subject: [PATCH] Provide key case translation --- CHANGELOG.md | 1 + docs/general/configuration_options.md | 86 ++- docs/general/key_transform.md | 34 ++ docs/general/rendering.md | 6 + lib/action_controller/serialization.rb | 4 +- lib/active_model/serializer/configuration.rb | 1 + lib/active_model_serializers/adapter/base.rb | 24 + lib/active_model_serializers/adapter/json.rb | 3 +- .../adapter/json_api.rb | 15 +- lib/active_model_serializers/key_transform.rb | 38 ++ .../serialization_context.rb | 3 +- test/adapter/json/key_case_test.rb | 93 ++++ test/adapter/json_api/key_case_test.rb | 500 ++++++++++++++++++ .../adapter/json_api/pagination_links_test.rb | 1 + 14 files changed, 787 insertions(+), 22 deletions(-) create mode 100644 docs/general/key_transform.md create mode 100644 lib/active_model_serializers/key_transform.rb create mode 100644 test/adapter/json/key_case_test.rb create mode 100644 test/adapter/json_api/key_case_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a46a83f4..061d55bb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Breaking changes: Features: +- [#1574](https://github.com/rails-api/active_model_serializers/pull/1574) Provide key translation. (@remear) - [#1494](https://github.com/rails-api/active_model_serializers/pull/1494) Make serializers serializalbe (using the Attributes adapter by default). (@bf4) - [#1550](https://github.com/rails-api/active_model_serializers/pull/1550) Add diff --git a/docs/general/configuration_options.md b/docs/general/configuration_options.md index f6ca86353..54fa5d90c 100644 --- a/docs/general/configuration_options.md +++ b/docs/general/configuration_options.md @@ -2,26 +2,84 @@ # Configuration Options -The following configuration options can be set on `ActiveModelSerializers.config`, -preferably inside an initializer. +The following configuration options can be set on +`ActiveModelSerializers.config`, preferably inside an initializer. ## General -- `adapter`: The [adapter](adapters.md) to use. Possible values: `:attributes, :json, :json_api`. Default: `:attributes`. -- `serializer_lookup_enabled`: When `false`, serializers must be explicitly specified. Default: `true` +##### adapter + +The [adapter](adapters.md) to use. + +Possible values: + +- `:attributes` (default) +- `:json` +- `:json_api` + +##### serializer_lookup_enabled + +Enable automatic serializer lookup. + +Possible values: + +- `true` (default) +- `false` + +When `false`, serializers must be explicitly specified. + +##### key_transform + +The [key transform](key_transform.md) to use. + +Possible values: + +- `:camel` - ExampleKey +- `:camel_lower` - exampleKey +- `:dashed` - example-key +- `:unaltered` - the original, unaltered key + ## JSON API -- `jsonapi_resource_type`: Whether the `type` attributes of resources should be singular or plural. Possible values: `:singular, :plural`. Default: `:plural`. -- `jsonapi_include_toplevel_object`: Whether to include a [top level JSON API member](http://jsonapi.org/format/#document-jsonapi-object) - in the response document. - Default: `false`. -- Used when `jsonapi_include_toplevel_object` is `true`: - - `jsonapi_version`: The latest version of the spec the API conforms to. - Default: `'1.0'`. - - `jsonapi_toplevel_meta`: Optional metadata. Not included if empty. - Default: `{}`. + +##### jsonapi_resource_type + +Set the `type` attributes of resources to singular or plural. + +Possible values: + +- `:singular` +- `:plural` (default) + +##### jsonapi_include_toplevel_object + +Include a [top level JSON API member](http://jsonapi.org/format/#document-jsonapi-object) +in the response document. + +Possible values: + +- `true` +- `false` (default) + +##### jsonapi_version + +The latest version of the spec to which the API conforms. + +Default: `'1.0'`. + +*Used when `jsonapi_include_toplevel_object` is `true`* + +##### jsonapi_toplevel_meta + +Optional top-level metadata. Not included if empty. + +Default: `{}`. + +*Used when `jsonapi_include_toplevel_object` is `true`* + ## Hooks -To run a hook when ActiveModelSerializers is loaded, use `ActiveSupport.on_load(:action_controller) do end` +To run a hook when ActiveModelSerializers is loaded, use +`ActiveSupport.on_load(:action_controller) do end` diff --git a/docs/general/key_transform.md b/docs/general/key_transform.md new file mode 100644 index 000000000..d19f2d0a5 --- /dev/null +++ b/docs/general/key_transform.md @@ -0,0 +1,34 @@ +[Back to Guides](../README.md) + +# Key Transforms + +Key transforms modify the keys in serialized responses. + +Provided key transforms: + +- `:camel` - ExampleKey +- `:camel_lower` - exampleKey +- `:dashed` - example-key +- `:unaltered` - the original, unaltered key + + +Key translation precedence is as follows: + +##### SerializableResource option + +`key_transform` is provided as an option via render. + +```render json: posts, each_serializer: PostSerializer, key_transform: :camel_lower``` + +##### Configuration option + +`key_transform` is set in `ActiveModelSerializers.config.key_transform`. + +```ActiveModelSerializers.config.key_transform = :camel_lower``` + +##### Adapter default + +Each adapter has a default key transform configured: + +- `Json` - `:unaltered` +- `JsonApi` - `:dashed` diff --git a/docs/general/rendering.md b/docs/general/rendering.md index 28fdaa36b..7b073e000 100644 --- a/docs/general/rendering.md +++ b/docs/general/rendering.md @@ -79,6 +79,12 @@ PR please :) PR please :) +#### key_transform + +```render json: posts, each_serializer: PostSerializer, key_transform: :camel_lower``` + +See [Key Transforms](key_transforms.md) for more informaiton. + #### meta A `meta` member can be used to include non-standard meta-information. `meta` can diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index fb5a03a36..cc7b8ba7d 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -56,7 +56,9 @@ def use_adapter? [:_render_option_json, :_render_with_renderer_json].each do |renderer_method| define_method renderer_method do |resource, options| - options.fetch(:serialization_context) { options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request) } + options.fetch(:serialization_context) do + options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request, options) + end serializable_resource = get_serializer(resource, options) super(serializable_resource, options) end diff --git a/lib/active_model/serializer/configuration.rb b/lib/active_model/serializer/configuration.rb index b347958c8..1553e632d 100644 --- a/lib/active_model/serializer/configuration.rb +++ b/lib/active_model/serializer/configuration.rb @@ -26,6 +26,7 @@ def config.array_serializer # Make JSON API top-level jsonapi member opt-in # ref: http://jsonapi.org/format/#document-top-level config.jsonapi_include_toplevel_object = false + config.key_transform = nil config.schema_path = 'test/support/schemas' end diff --git a/lib/active_model_serializers/adapter/base.rb b/lib/active_model_serializers/adapter/base.rb index 10701eef9..5c4924633 100644 --- a/lib/active_model_serializers/adapter/base.rb +++ b/lib/active_model_serializers/adapter/base.rb @@ -1,6 +1,9 @@ +require 'active_model_serializers/key_transform' + module ActiveModelSerializers module Adapter class Base + include ActiveModelSerializers::KeyTransform # Automatically register adapters when subclassing def self.inherited(subclass) ActiveModelSerializers::Adapter.register(subclass) @@ -51,6 +54,27 @@ def include_meta(json) json[meta_key] = meta unless meta.blank? json end + + def default_key_transform + :unaltered + end + + # Determines the transform to use in order of precedence: + # serialization context, global config, adapter default. + # + # @param serialization_context [Object] the SerializationContext + # @return [Symbol] the transform to use + def key_transform(serialization_context) + serialization_context.key_transform || + ActiveModelSerializers.config.key_transform || + default_key_transform + end + + def transform_key_casing!(value, serialization_context) + return value unless serialization_context + transform = key_transform(serialization_context) + KeyTransform.send(transform, value) + end end end end diff --git a/lib/active_model_serializers/adapter/json.rb b/lib/active_model_serializers/adapter/json.rb index 3cc364365..7046d782c 100644 --- a/lib/active_model_serializers/adapter/json.rb +++ b/lib/active_model_serializers/adapter/json.rb @@ -3,7 +3,8 @@ module Adapter class Json < Base def serializable_hash(options = nil) options ||= {} - { root => Attributes.new(serializer, instance_options).serializable_hash(options) } + serialized_hash = { root => Attributes.new(serializer, instance_options).serializable_hash(options) } + transform_key_casing!(serialized_hash, options[:serialization_context]) end end end diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb index cf0c0c6b4..3233121cb 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -37,15 +37,20 @@ def initialize(serializer, options = {}) @fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields)) end + def default_key_transform + :dashed + end + # {http://jsonapi.org/format/#crud Requests are transactional, i.e. success or failure} # {http://jsonapi.org/format/#document-top-level data and errors MUST NOT coexist in the same document.} def serializable_hash(options = nil) options ||= {} - if serializer.success? - success_document(options) - else - failure_document - end + document = if serializer.success? + success_document(options) + else + failure_document + end + transform_key_casing!(document, options[:serialization_context]) end # {http://jsonapi.org/format/#document-top-level Primary data} diff --git a/lib/active_model_serializers/key_transform.rb b/lib/active_model_serializers/key_transform.rb new file mode 100644 index 000000000..8d9c375bf --- /dev/null +++ b/lib/active_model_serializers/key_transform.rb @@ -0,0 +1,38 @@ +module ActiveModelSerializers + module KeyTransform + # Transforms keys to UpperCamelCase or PascalCase. + # + # @example: + # "some_key" => "SomeKey", + # @see {https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L66-L76 ActiveSupport::Inflector.camelize} + def camel(hash) + hash.deep_transform_keys! { |key| key.to_s.camelize.to_sym } + end + + # Transforms keys to camelCase. + # + # @example: + # "some_key" => "someKey", + # @see {https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L66-L76 ActiveSupport::Inflector.camelize} + def camel_lower(hash) + hash.deep_transform_keys! { |key| key.to_s.camelize(:lower).to_sym } + end + + # Transforms keys to dashed-case. + # This is the default case for the JsonApi adapter. + # + # @example: + # "some_key" => "some-key", + # @see {https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L185-L187 ActiveSupport::Inflector.dasherize} + def dashed(hash) + hash.deep_transform_keys! { |key| key.to_s.dasherize.to_sym } + end + + # Returns the hash unaltered + def unaltered(hash) + hash + end + + module_function :camel, :camel_lower, :dashed, :unaltered + end +end diff --git a/lib/active_model_serializers/serialization_context.rb b/lib/active_model_serializers/serialization_context.rb index d7f8aba9e..3c521f517 100644 --- a/lib/active_model_serializers/serialization_context.rb +++ b/lib/active_model_serializers/serialization_context.rb @@ -4,13 +4,14 @@ class << self attr_writer :url_helpers, :default_url_options end - attr_reader :request_url, :query_parameters + attr_reader :request_url, :query_parameters, :key_transform 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 + @key_transform = options.delete(:key_transform) end def self.url_helpers diff --git a/test/adapter/json/key_case_test.rb b/test/adapter/json/key_case_test.rb new file mode 100644 index 000000000..17219f3c6 --- /dev/null +++ b/test/adapter/json/key_case_test.rb @@ -0,0 +1,93 @@ +require 'test_helper' + +module ActiveModelSerializers + module Adapter + class Json + class KeyCaseTest < ActiveSupport::TestCase + def mock_request(key_transform = nil) + context = Minitest::Mock.new + context.expect(:request_url, URI) + context.expect(:query_parameters, {}) + context.expect(:key_transform, key_transform) + @options = {} + @options[:serialization_context] = context + end + + Post = Class.new(::Model) + class PostSerializer < ActiveModel::Serializer + attributes :id, :title, :body, :publish_at + end + + def setup + ActionController::Base.cache_store.clear + @blog = Blog.new(id: 1, name: 'My Blog!!', special_attribute: 'neat') + serializer = CustomBlogSerializer.new(@blog) + @adapter = ActiveModelSerializers::Adapter::Json.new(serializer) + end + + def test_key_transform_default + mock_request + assert_equal({ + blog: { id: 1, special_attribute: 'neat', articles: nil } + }, @adapter.serializable_hash(@options)) + end + + def test_key_transform_global_config + mock_request + result = with_config(key_transform: :camel_lower) do + @adapter.serializable_hash(@options) + end + assert_equal({ + blog: { id: 1, specialAttribute: 'neat', articles: nil } + }, result) + end + + def test_key_transform_serialization_ctx_overrides_global_config + mock_request(:camel) + result = with_config(key_transform: :camel_lower) do + @adapter.serializable_hash(@options) + end + assert_equal({ + Blog: { Id: 1, SpecialAttribute: 'neat', Articles: nil } + }, result) + end + + def test_key_transform_undefined + mock_request(:blam) + result = nil + assert_raises NoMethodError do + result = @adapter.serializable_hash(@options) + end + end + + def test_key_transform_dashed + mock_request(:dashed) + assert_equal({ + blog: { id: 1, :"special-attribute" => 'neat', articles: nil } + }, @adapter.serializable_hash(@options)) + end + + def test_key_transform_unaltered + mock_request(:unaltered) + assert_equal({ + blog: { id: 1, special_attribute: 'neat', articles: nil } + }, @adapter.serializable_hash(@options)) + end + + def test_key_transform_camel + mock_request(:camel) + assert_equal({ + Blog: { Id: 1, SpecialAttribute: 'neat', Articles: nil } + }, @adapter.serializable_hash(@options)) + end + + def test_key_transform_camel_lower + mock_request(:camel_lower) + assert_equal({ + blog: { id: 1, specialAttribute: 'neat', articles: nil } + }, @adapter.serializable_hash(@options)) + end + end + end + end +end diff --git a/test/adapter/json_api/key_case_test.rb b/test/adapter/json_api/key_case_test.rb new file mode 100644 index 000000000..910769604 --- /dev/null +++ b/test/adapter/json_api/key_case_test.rb @@ -0,0 +1,500 @@ +require 'test_helper' + +module ActiveModelSerializers + module Adapter + class JsonApi + class KeyCaseTest < ActiveSupport::TestCase + Post = Class.new(::Model) + class PostSerializer < ActiveModel::Serializer + type 'posts' + attributes :title, :body, :publish_at + belongs_to :author + has_many :comments + + link(:self) { post_url(object.id) } + link(:post_authors) { post_authors_url(object.id) } + link(:subscriber_comments) { post_comments_url(object.id) } + + meta do + { + rating: 5, + favorite_count: 10 + } + end + end + + Author = Class.new(::Model) + class AuthorSerializer < ActiveModel::Serializer + type 'authors' + attributes :first_name, :last_name + end + + Comment = Class.new(::Model) + class CommentSerializer < ActiveModel::Serializer + type 'comments' + attributes :body + belongs_to :author + end + + def mock_request(key_transform = nil) + context = Minitest::Mock.new + context.expect(:request_url, URI) + context.expect(:query_parameters, {}) + context.expect(:key_transform, key_transform) + context.expect(:url_helpers, Rails.application.routes.url_helpers) + @options = {} + @options[:serialization_context] = context + end + + def setup + Rails.application.routes.draw do + resources :posts do + resources :authors + resources :comments + end + end + @publish_at = 1.day.from_now + @author = Author.new(id: 1, first_name: 'Bob', last_name: 'Jones') + @comment1 = Comment.new(id: 7, body: 'cool', author: @author) + @comment2 = Comment.new(id: 12, body: 'awesome', author: @author) + @post = Post.new(id: 1337, title: 'Title 1', body: 'Body 1', + author: @author, comments: [@comment1, @comment2], + publish_at: @publish_at) + @comment1.post = @post + @comment2.post = @post + end + + def test_success_document_key_transform_default + mock_request + serializer = PostSerializer.new(@post) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + assert_equal({ + data: { + id: '1337', + type: 'posts', + attributes: { + title: 'Title 1', + body: 'Body 1', + :"publish-at" => @publish_at + }, + relationships: { + author: { + data: { id: '1', type: 'authors' } + }, + comments: { + data: [ + { id: '7', type: 'comments' }, + { id: '12', type: 'comments' } + ] } + }, + links: { + self: 'http://example.com/posts/1337', + :"post-authors" => 'http://example.com/posts/1337/authors', + :"subscriber-comments" => 'http://example.com/posts/1337/comments' + }, + meta: { rating: 5, :"favorite-count" => 10 } + } + }, result) + end + + def test_success_document_key_transform_global_config + mock_request + result = with_config(key_transform: :camel_lower) do + serializer = PostSerializer.new(@post) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + adapter.serializable_hash(@options) + end + assert_equal({ + data: { + id: '1337', + type: 'posts', + attributes: { + title: 'Title 1', + body: 'Body 1', + publishAt: @publish_at + }, + relationships: { + author: { + data: { id: '1', type: 'authors' } + }, + comments: { + data: [ + { id: '7', type: 'comments' }, + { id: '12', type: 'comments' } + ] } + }, + links: { + self: 'http://example.com/posts/1337', + postAuthors: 'http://example.com/posts/1337/authors', + subscriberComments: 'http://example.com/posts/1337/comments' + }, + meta: { rating: 5, favoriteCount: 10 } + } + }, result) + end + + def test_success_doc_key_transform_serialization_ctx_overrides_global + mock_request(:camel) + result = with_config(key_transform: :camel_lower) do + serializer = PostSerializer.new(@post) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + adapter.serializable_hash(@options) + end + assert_equal({ + Data: { + Id: '1337', + Type: 'posts', + Attributes: { + Title: 'Title 1', + Body: 'Body 1', + PublishAt: @publish_at + }, + Relationships: { + Author: { + Data: { Id: '1', Type: 'authors' } + }, + Comments: { + Data: [ + { Id: '7', Type: 'comments' }, + { Id: '12', Type: 'comments' } + ] } + }, + Links: { + Self: 'http://example.com/posts/1337', + PostAuthors: 'http://example.com/posts/1337/authors', + SubscriberComments: 'http://example.com/posts/1337/comments' + }, + Meta: { Rating: 5, FavoriteCount: 10 } + } + }, result) + end + + def test_success_document_key_transform_dashed + mock_request(:dashed) + serializer = PostSerializer.new(@post) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + assert_equal({ + data: { + id: '1337', + type: 'posts', + attributes: { + title: 'Title 1', + body: 'Body 1', + :"publish-at" => @publish_at + }, + relationships: { + author: { + data: { id: '1', type: 'authors' } + }, + comments: { + data: [ + { id: '7', type: 'comments' }, + { id: '12', type: 'comments' } + ] } + }, + links: { + self: 'http://example.com/posts/1337', + :"post-authors" => 'http://example.com/posts/1337/authors', + :"subscriber-comments" => 'http://example.com/posts/1337/comments' + }, + meta: { rating: 5, :"favorite-count" => 10 } + } + }, result) + end + + def test_success_document_key_transform_unaltered + mock_request(:unaltered) + serializer = PostSerializer.new(@post) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + assert_equal({ + data: { + id: '1337', + type: 'posts', + attributes: { + title: 'Title 1', + body: 'Body 1', + publish_at: @publish_at + }, + relationships: { + author: { + data: { id: '1', type: 'authors' } + }, + comments: { + data: [ + { id: '7', type: 'comments' }, + { id: '12', type: 'comments' } + ] } + }, + links: { + self: 'http://example.com/posts/1337', + post_authors: 'http://example.com/posts/1337/authors', + subscriber_comments: 'http://example.com/posts/1337/comments' + }, + meta: { rating: 5, favorite_count: 10 } + } + }, result) + end + + def test_success_document_key_transform_undefined + mock_request(:zoot) + serializer = PostSerializer.new(@post) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + assert_raises NoMethodError do + adapter.serializable_hash(@options) + end + end + + def test_success_document_key_transform_camel + mock_request(:camel) + serializer = PostSerializer.new(@post) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + assert_equal({ + Data: { + Id: '1337', + Type: 'posts', + Attributes: { + Title: 'Title 1', + Body: 'Body 1', + PublishAt: @publish_at + }, + Relationships: { + Author: { + Data: { Id: '1', Type: 'authors' } + }, + Comments: { + Data: [ + { Id: '7', Type: 'comments' }, + { Id: '12', Type: 'comments' } + ] } + }, + Links: { + Self: 'http://example.com/posts/1337', + PostAuthors: 'http://example.com/posts/1337/authors', + SubscriberComments: 'http://example.com/posts/1337/comments' + }, + Meta: { Rating: 5, FavoriteCount: 10 } + } + }, result) + end + + def test_success_document_key_transform_camel_lower + mock_request(:camel_lower) + serializer = PostSerializer.new(@post) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + assert_equal({ + data: { + id: '1337', + type: 'posts', + attributes: { + title: 'Title 1', + body: 'Body 1', + publishAt: @publish_at + }, + relationships: { + author: { + data: { id: '1', type: 'authors' } + }, + comments: { + data: [ + { id: '7', type: 'comments' }, + { id: '12', type: 'comments' } + ] } + }, + links: { + self: 'http://example.com/posts/1337', + postAuthors: 'http://example.com/posts/1337/authors', + subscriberComments: 'http://example.com/posts/1337/comments' + }, + meta: { rating: 5, favoriteCount: 10 } + } + }, result) + end + + def test_error_document_key_transform_default + mock_request + resource = ModelWithErrors.new + resource.errors.add(:published_at, 'must be in the future') + resource.errors.add(:title, 'must be longer') + serializer = ActiveModel::Serializer::ErrorSerializer.new(resource) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + expected_errors_object = + { :errors => + [ + { + :source => { :pointer => '/data/attributes/published_at' }, + :detail => 'must be in the future' }, + { + :source => { :pointer => '/data/attributes/title' }, + :detail => 'must be longer' + } + ] + } + assert_equal expected_errors_object, result + end + + def test_error_document_key_transform_global_config + mock_request + result = with_config(key_transform: :camel) do + resource = ModelWithErrors.new + resource.errors.add(:published_at, 'must be in the future') + resource.errors.add(:title, 'must be longer') + serializer = ActiveModel::Serializer::ErrorSerializer.new(resource) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + adapter.serializable_hash(@options) + end + expected_errors_object = + { :Errors => + [ + { + :Source => { :Pointer => '/data/attributes/published_at' }, + :Detail => 'must be in the future' + }, + { + :Source => { :Pointer => '/data/attributes/title' }, + :Detail => 'must be longer' + } + ] + } + assert_equal expected_errors_object, result + end + + def test_error_document_key_transform_serialization_ctx_overrides_global + mock_request(:camel) + result = with_config(key_transform: :camel_lower) do + resource = ModelWithErrors.new + resource.errors.add(:published_at, 'must be in the future') + resource.errors.add(:title, 'must be longer') + serializer = ActiveModel::Serializer::ErrorSerializer.new(resource) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + adapter.serializable_hash(@options) + end + expected_errors_object = + { :Errors => + [ + { + :Source => { :Pointer => '/data/attributes/published_at' }, + :Detail => 'must be in the future' + }, + { + :Source => { :Pointer => '/data/attributes/title' }, + :Detail => 'must be longer' + } + ] + } + assert_equal expected_errors_object, result + end + + def test_error_document_key_transform_dashed + mock_request(:dashed) + + resource = ModelWithErrors.new + resource.errors.add(:published_at, 'must be in the future') + resource.errors.add(:title, 'must be longer') + + serializer = ActiveModel::Serializer::ErrorSerializer.new(resource) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + + expected_errors_object = + { :errors => + [ + { + :source => { :pointer => '/data/attributes/published_at' }, + :detail => 'must be in the future' + }, + { + :source => { :pointer => '/data/attributes/title' }, + :detail => 'must be longer' + } + ] + } + assert_equal expected_errors_object, result + end + + def test_error_document_key_transform_unaltered + mock_request(:unaltered) + + resource = ModelWithErrors.new + resource.errors.add(:published_at, 'must be in the future') + resource.errors.add(:title, 'must be longer') + + serializer = ActiveModel::Serializer::ErrorSerializer.new(resource) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + + expected_errors_object = + { :errors => + [ + { :source => { :pointer => '/data/attributes/published_at' }, :detail => 'must be in the future' }, + { :source => { :pointer => '/data/attributes/title' }, :detail => 'must be longer' } + ] + } + assert_equal expected_errors_object, result + end + + def test_error_document_key_transform_undefined + mock_request(:krazy) + + resource = ModelWithErrors.new + resource.errors.add(:published_at, 'must be in the future') + resource.errors.add(:title, 'must be longer') + + serializer = ActiveModel::Serializer::ErrorSerializer.new(resource) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + + assert_raises NoMethodError do + adapter.serializable_hash(@options) + end + end + + def test_error_document_key_transform_camel + mock_request(:camel) + + resource = ModelWithErrors.new + resource.errors.add(:published_at, 'must be in the future') + resource.errors.add(:title, 'must be longer') + + serializer = ActiveModel::Serializer::ErrorSerializer.new(resource) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + + expected_errors_object = + { :Errors => + [ + { :Source => { :Pointer => '/data/attributes/published_at' }, :Detail => 'must be in the future' }, + { :Source => { :Pointer => '/data/attributes/title' }, :Detail => 'must be longer' } + ] + } + assert_equal expected_errors_object, result + end + + def test_error_document_key_transform_camel_lower + mock_request(:camel_lower) + + resource = ModelWithErrors.new + resource.errors.add(:published_at, 'must be in the future') + resource.errors.add(:title, 'must be longer') + + serializer = ActiveModel::Serializer::ErrorSerializer.new(resource) + adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) + result = adapter.serializable_hash(@options) + + expected_errors_object = + { :errors => + [ + { :source => { :pointer => '/data/attributes/published_at' }, :detail => 'must be in the future' }, + { :source => { :pointer => '/data/attributes/title' }, :detail => 'must be longer' } + ] + } + assert_equal expected_errors_object, result + end + end + end + end +end diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index 37b0cbe69..2990d5d39 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -25,6 +25,7 @@ def mock_request(query_parameters = {}, original_url = URI) context = Minitest::Mock.new context.expect(:request_url, original_url) context.expect(:query_parameters, query_parameters) + context.expect(:key_transform, nil) @options = {} @options[:serialization_context] = context end