Skip to content

Commit

Permalink
Provide key case translation
Browse files Browse the repository at this point in the history
  • Loading branch information
remear committed Mar 14, 2016
1 parent daabb89 commit 7c397eb
Show file tree
Hide file tree
Showing 14 changed files with 787 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 72 additions & 14 deletions docs/general/configuration_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
34 changes: 34 additions & 0 deletions docs/general/key_transform.md
Original file line number Diff line number Diff line change
@@ -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`
6 changes: 6 additions & 0 deletions docs/general/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lib/action_controller/serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/active_model/serializer/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions lib/active_model_serializers/adapter/base.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion lib/active_model_serializers/adapter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions lib/active_model_serializers/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
38 changes: 38 additions & 0 deletions lib/active_model_serializers/key_transform.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion lib/active_model_serializers/serialization_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 93 additions & 0 deletions test/adapter/json/key_case_test.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 7c397eb

Please sign in to comment.