From daadf98d6172ed90d7677f011f34b70c5491c10e 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 --- lib/action_controller/serialization.rb | 4 +- lib/active_model_serializers/adapter/base.rb | 12 + lib/active_model_serializers/adapter/json.rb | 3 +- .../adapter/json_api.rb | 3 +- .../serialization_context.rb | 3 +- test/adapter/json/key_case_test.rb | 51 ++++ test/adapter/json_api/key_case_test.rb | 234 ++++++++++++++++++ test/adapter/json_api/links_test.rb | 8 +- .../adapter/json_api/pagination_links_test.rb | 1 + 9 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 test/adapter/json/key_case_test.rb create mode 100644 test/adapter/json_api/key_case_test.rb diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index fb5a03a36..192b6ca61 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) { + options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request, options) + } serializable_resource = get_serializer(resource, options) super(serializable_resource, options) end diff --git a/lib/active_model_serializers/adapter/base.rb b/lib/active_model_serializers/adapter/base.rb index 9b31cffcf..9c473b59d 100644 --- a/lib/active_model_serializers/adapter/base.rb +++ b/lib/active_model_serializers/adapter/base.rb @@ -51,6 +51,18 @@ def include_meta(json) json[meta_key] = meta if meta json end + + def translate_key_casing!(value, serialization_context) + return value unless serialization_context + case serialization_context.key_case + when :camel + value.deep_transform_keys! { |key| key.to_s.camelize.to_sym } + when :camel_lower + value.deep_transform_keys! { |key| key.to_s.camelize(:lower).to_sym } + else + value + end + end end end end diff --git a/lib/active_model_serializers/adapter/json.rb b/lib/active_model_serializers/adapter/json.rb index 9652a04f0..8d5ce1919 100644 --- a/lib/active_model_serializers/adapter/json.rb +++ b/lib/active_model_serializers/adapter/json.rb @@ -6,7 +6,8 @@ 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) } + translate_key_casing!(serialized_hash, options[:serialization_context]) end private diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb index f06a69e92..b42df7f0c 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -22,11 +22,12 @@ def initialize(serializer, options = {}) # {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? + document = if serializer.success? success_document(options) else failure_document end + translate_key_casing!(document, options[:serialization_context]) end # {http://jsonapi.org/format/#document-top-level Primary data} diff --git a/lib/active_model_serializers/serialization_context.rb b/lib/active_model_serializers/serialization_context.rb index d7f8aba9e..4ccf4b67a 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_case 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_case = options.delete(:key_case) || :default 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..8b99b9156 --- /dev/null +++ b/test/adapter/json/key_case_test.rb @@ -0,0 +1,51 @@ +require 'test_helper' + +module ActiveModelSerializers + module Adapter + class Json + class KeyCaseTest < ActiveSupport::TestCase + def mock_request(key_case) + context = Minitest::Mock.new + context.expect(:request_url, URI) + context.expect(:query_parameters, {}) + context.expect(:key_case, key_case) + @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_case_default + mock_request(:default) + assert_equal({ + blog: { id: 1, special_attribute: "neat", articles: nil } + }, @adapter.serializable_hash(@options)) + end + + def test_key_case_camel + mock_request(:camel) + assert_equal({ + Blog: { Id: 1, SpecialAttribute: "neat", Articles: nil } + }, @adapter.serializable_hash(@options)) + end + + def test_key_case_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..f450aefcc --- /dev/null +++ b/test/adapter/json_api/key_case_test.rb @@ -0,0 +1,234 @@ +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_case = :default) + context = Minitest::Mock.new + context.expect(:request_url, URI) + context.expect(:query_parameters, {}) + context.expect(:key_case, key_case) + 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_key_case_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_key_case_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_key_case_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_case_default + mock_request(:default) + + 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_case_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_case_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/links_test.rb b/test/adapter/json_api/links_test.rb index 87f22644d..b7bce14a0 100644 --- a/test/adapter/json_api/links_test.rb +++ b/test/adapter/json_api/links_test.rb @@ -40,7 +40,7 @@ def test_toplevel_links stuff: 'value' } } - }).serializable_hash + }).serializable_hash(@options) expected = { self: { href: 'http://example.com/posts', @@ -57,7 +57,7 @@ def test_nil_toplevel_links @post, adapter: :json_api, links: nil - ).serializable_hash + ).serializable_hash(@options) refute hash.key?(:links), 'No links key to be output' end @@ -66,12 +66,12 @@ def test_nil_toplevel_links_json_adapter @post, adapter: :json, links: nil - ).serializable_hash + ).serializable_hash(@options) refute hash.key?(:links), 'No links key to be output' end def test_resource_links - hash = serializable(@author, adapter: :json_api).serializable_hash + hash = serializable(@author, adapter: :json_api).serializable_hash(@options) expected = { self: { href: 'http://example.com/link_author/1337', diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index d728f7001..e2633652a 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -23,6 +23,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_case, :default) @options = {} @options[:serialization_context] = context end