diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a46a83f..061d55bb 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 f6ca8635..b9b7802d 100644 --- a/docs/general/configuration_options.md +++ b/docs/general/configuration_options.md @@ -2,26 +2,95 @@ # 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 +- `nil` - use the adapter default + +Each adapter has a default key transform configured: + +- `Json` - `:unaltered` +- `JsonApi` - `:dashed` + +`config.key_transform` is a global override of the adapter default. Adapters +still prefer the render option `:key_transform` over this setting. + ## 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 + +Sets whether the [type](http://jsonapi.org/format/#document-resource-identifier-objects) +of the resource should be `singularized` or `pluralized` when it is not +[explicitly specified by the serializer](https://github.com/rails-api/active_model_serializers/blob/master/docs/general/serializers.md#type) + +Possible values: + +- `:singular` +- `:plural` (default) + +##### jsonapi_include_toplevel_object + +Include a [top level jsonapi 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 00000000..022b7688 --- /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 +- `nil` - use the adapter default + +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 28fdaa36..7b073e00 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 fb5a03a3..cc7b8ba7 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -56,7 +56,9 @@ module ActionController [:_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 b347958c..1553e632 100644 --- a/lib/active_model/serializer/configuration.rb +++ b/lib/active_model/serializer/configuration.rb @@ -26,6 +26,7 @@ module ActiveModel # 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 10701eef..cc6092d6 100644 --- a/lib/active_model_serializers/adapter/base.rb +++ b/lib/active_model_serializers/adapter/base.rb @@ -1,3 +1,5 @@ +require 'active_model_serializers/key_transform' + module ActiveModelSerializers module Adapter class Base @@ -51,6 +53,27 @@ module ActiveModelSerializers 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 3cc36436..7046d782 100644 --- a/lib/active_model_serializers/adapter/json.rb +++ b/lib/active_model_serializers/adapter/json.rb @@ -3,7 +3,8 @@ module ActiveModelSerializers 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 cf0c0c6b..3233121c 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -37,15 +37,20 @@ module ActiveModelSerializers @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 00000000..03cf9cef --- /dev/null +++ b/lib/active_model_serializers/key_transform.rb @@ -0,0 +1,40 @@ +require 'active_support/core_ext/hash/keys' + +module ActiveModelSerializers + module KeyTransform + module_function + + # 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 + end +end diff --git a/lib/active_model_serializers/serialization_context.rb b/lib/active_model_serializers/serialization_context.rb index d7f8aba9..3c521f51 100644 --- a/lib/active_model_serializers/serialization_context.rb +++ b/lib/active_model_serializers/serialization_context.rb @@ -4,13 +4,14 @@ module ActiveModelSerializers 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/action_controller/json_api/key_transform_test.rb b/test/action_controller/json_api/key_transform_test.rb new file mode 100644 index 00000000..63d9a897 --- /dev/null +++ b/test/action_controller/json_api/key_transform_test.rb @@ -0,0 +1,180 @@ +require 'test_helper' + +module ActionController + module Serialization + class JsonApi + class KeyTransformTest < ActionController::TestCase + class KeyTransformTestController < ActionController::Base + Post = Class.new(::Model) + class PostSerializer < ActiveModel::Serializer + type 'posts' + attributes :title, :body, :publish_at + belongs_to :author + has_many :comments + + link(:post_authors) { 'https://example.com/post_authors' } + + 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 setup_post + ActionController::Base.cache_store.clear + @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: '2020-03-16T03:55:25.291Z') + @comment1.post = @post + @comment2.post = @post + end + + def render_resource_with_key_transform + setup_post + render json: @post, serializer: PostSerializer, adapter: :json_api, + key_transform: :camel + end + + def render_resource_with_key_transform_nil + setup_post + render json: @post, serializer: PostSerializer, adapter: :json_api, + key_transform: nil + end + + def render_resource_with_key_transform_with_global_config + setup_post + old_transform = ActiveModelSerializers.config.key_transform + ActiveModelSerializers.config.key_transform = :camel_lower + render json: @post, serializer: PostSerializer, adapter: :json_api + ActiveModelSerializers.config.key_transform = old_transform + end + end + + tests KeyTransformTestController + + def test_render_resource_with_key_transform + get :render_resource_with_key_transform + response = JSON.parse(@response.body) + expected = { + 'Data' => { + 'Id' => '1337', + 'Type' => 'posts', + 'Attributes' => { + 'Title' => 'Title 1', + 'Body' => 'Body 1', + 'PublishAt' => '2020-03-16T03:55:25.291Z' + }, + 'Relationships' => { + 'Author' => { + 'Data' => { + 'Id' => '1', + 'Type' => 'authors' + } + }, + 'Comments' => { + 'Data' => [ + { 'Id' => '7', 'Type' => 'comments' }, + { 'Id' => '12', 'Type' => 'comments' } + ] + } + }, + 'Links' => { + 'PostAuthors' => 'https://example.com/post_authors' + }, + 'Meta' => { 'Rating' => 5, 'FavoriteCount' => 10 } + } + } + assert_equal expected, response + end + + def test_render_resource_with_key_transform_nil + get :render_resource_with_key_transform_nil + response = JSON.parse(@response.body) + expected = { + 'data' => { + 'id' => '1337', + 'type' => 'posts', + 'attributes' => { + 'title' => 'Title 1', + 'body' => 'Body 1', + 'publish-at' => '2020-03-16T03:55:25.291Z' + }, + 'relationships' => { + 'author' => { + 'data' => { + 'id' => '1', + 'type' => 'authors' + } + }, + 'comments' => { + 'data' => [ + { 'id' => '7', 'type' => 'comments' }, + { 'id' => '12', 'type' => 'comments' } + ] + } + }, + 'links' => { + 'post-authors' => 'https://example.com/post_authors' + }, + 'meta' => { 'rating' => 5, 'favorite-count' => 10 } + } + } + assert_equal expected, response + end + + def test_render_resource_with_key_transform_with_global_config + get :render_resource_with_key_transform_with_global_config + response = JSON.parse(@response.body) + expected = { + 'data' => { + 'id' => '1337', + 'type' => 'posts', + 'attributes' => { + 'title' => 'Title 1', + 'body' => 'Body 1', + 'publishAt' => '2020-03-16T03:55:25.291Z' + }, + 'relationships' => { + 'author' => { + 'data' => { + 'id' => '1', + 'type' => 'authors' + } + }, + 'comments' => { + 'data' => [ + { 'id' => '7', 'type' => 'comments' }, + { 'id' => '12', 'type' => 'comments' } + ] + } + }, + 'links' => { + 'postAuthors' => 'https://example.com/post_authors' + }, + 'meta' => { 'rating' => 5, 'favoriteCount' => 10 } + } + } + assert_equal expected, response + end + end + end + end +end diff --git a/test/adapter/json/key_case_test.rb b/test/adapter/json/key_case_test.rb new file mode 100644 index 00000000..17219f3c --- /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 00000000..91076960 --- /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 37b0cbe6..2990d5d3 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -25,6 +25,7 @@ module ActiveModelSerializers 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