Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to transform field keys non-recursively (#132) #310

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Transforming fields requires two steps:

```elixir
config :jsonapi,
field_transformation: :camelize # or dasherize
field_transformation: :camelize # or :dasherize, :camelize_shallow, or :dasherize_shallow
```

2. Underscoring _incoming_ params (both query and body) requires you add the
Expand Down Expand Up @@ -215,7 +215,8 @@ config :jsonapi,
`:camelize`. JSON:API v1.0 recommended using a dash (e.g.
`"favorite-color": blue`). If your API uses dashed fields, set this value to
`:dasherize`. If your API uses underscores (e.g. `"favorite_color": "red"`)
set to `:underscore`.
set to `:underscore`. To transform only the top-level field keys, use
`:camelize_shallow` or `:dasherize_shallow`.
- **remove_links**. `links` data can optionally be removed from the payload via
setting the configuration above to `true`. Defaults to `false`.
- **json_library**. Defaults to [Jason](https://hex.pm/packages/jason).
Expand Down
6 changes: 3 additions & 3 deletions lib/jsonapi/plugs/query_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,15 @@ defmodule JSONAPI.QueryParser do
@spec get_view_for_type(module(), String.t()) :: module() | no_return()
def get_view_for_type(view, type) do
case Enum.find(view.relationships(), fn relationship ->
is_field_valid_for_relationship(relationship, type)
field_valid_for_relationship?(relationship, type)
end) do
{_, view} -> view
nil -> raise_invalid_field_names(type, view.type())
end
end

@spec is_field_valid_for_relationship({atom(), module()}, String.t()) :: boolean()
defp is_field_valid_for_relationship({key, view}, expected_type) do
@spec field_valid_for_relationship?({atom(), module()}, String.t()) :: boolean()
defp field_valid_for_relationship?({key, view}, expected_type) do
cond do
view.type == expected_type ->
true
Expand Down
2 changes: 2 additions & 0 deletions lib/jsonapi/serializer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ defmodule JSONAPI.Serializer do
case Utils.String.field_transformation() do
:camelize -> Utils.String.expand_fields(fields, &Utils.String.camelize/1)
:dasherize -> Utils.String.expand_fields(fields, &Utils.String.dasherize/1)
:camelize_shallow -> Utils.String.expand_root_keys(fields, &Utils.String.camelize/1)
:dasherize_shallow -> Utils.String.expand_root_keys(fields, &Utils.String.dasherize/1)
_ -> fields
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/jsonapi/utils/data_to_params.ex
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ defmodule JSONAPI.Utils.DataToParams do
defp transform_fields(fields) do
case JString.field_transformation() do
:camelize -> JString.expand_fields(fields, &JString.camelize/1)
:camelize_shallow -> JString.expand_fields(fields, &JString.camelize/1)
:dasherize -> JString.expand_fields(fields, &JString.dasherize/1)
:dasherize_shallow -> JString.expand_fields(fields, &JString.dasherize/1)
_ -> fields
end
end
Expand Down
40 changes: 38 additions & 2 deletions lib/jsonapi/utils/string.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ defmodule JSONAPI.Utils.String do
String manipulation helpers.
"""

@allowed_transformations [:camelize, :dasherize, :underscore]
@allowed_transformations [
:camelize,
:dasherize,
:underscore,
:camelize_shallow,
:dasherize_shallow
]

@doc """
Replace dashes between words in `value` with underscores
Expand Down Expand Up @@ -236,6 +242,27 @@ defmodule JSONAPI.Utils.String do
value
end

@doc """
Like `JSONAPI.Utils.String.expand_fields/2`, but only uses the given function to transform the
keys of a top-level map. Other values are transformed with `to_string/1`.

## Examples

iex> expand_root_keys(%{"foo-bar" => %{"bar-baz" => "x"}}, &underscore/1)
%{"foo_bar" => %{"bar-baz" => "x"}}

iex> expand_root_keys(%{"foo-bar" => [:x, %{"bar-baz" => "y"}]}, &underscore/1)
%{"foo_bar" => ["x", %{"bar-baz" => "y"}]}

"""
def expand_root_keys(map, fun) when is_map(map) do
Enum.into(map, %{}, fn {key, value} ->
{fun.(key), expand_fields(value, &to_string/1)}
end)
end

def expand_root_keys(value, _fun), do: expand_fields(value, &to_string/1)

defp maybe_expand_fields(values, fun) when is_list(values) do
Enum.map(values, fn
string when is_binary(string) -> string
Expand All @@ -248,7 +275,8 @@ defmodule JSONAPI.Utils.String do
using camlized fields (e.g. "goodDog", versus "good_dog"). However, we don't hold a strong
opinion, so feel free to customize it how you would like (e.g. "good-dog", versus "good_dog").

This library currently supports camelized, dashed and underscored fields.
This library currently supports camelized, dashed and underscored fields. Shallow variants
exist that only transform top-level field keys.

## Configuration examples

Expand All @@ -258,12 +286,20 @@ defmodule JSONAPI.Utils.String do
config :jsonapi, field_transformation: :camelize
```

```
config :jsonapi, field_transformation: :camelize_shallow
```

Dashed fields:

```
config :jsonapi, field_transformation: :dasherize
```

```
config :jsonapi, field_transformation: :dasherize_shallow
```

Underscored fields:

```
Expand Down
1 change: 0 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ defmodule JSONAPI.Mixfile do
end

# Use Phoenix compiler depending on environment.
defp compilers(:test), do: [:phoenix] ++ Mix.compilers()
defp compilers(_), do: Mix.compilers()

# Specifies which paths to compile per environment.
Expand Down
73 changes: 65 additions & 8 deletions test/jsonapi/serializer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,14 @@ defmodule JSONAPI.SerializerTest do
id: 1,
text: "Hello",
inserted_at: NaiveDateTime.utc_now(),
body: "Hello world",
body: %{data: "Hello world", data_attr: "foo"},
full_description: "This_is_my_description",
author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"},
author: %{
id: 2,
username: "jbonds",
first_name: %{data: "jerry", data_attr: "foo"},
last_name: "bonds"
},
best_comments: [
%{
id: 5,
Expand All @@ -607,15 +612,17 @@ defmodule JSONAPI.SerializerTest do
included = encoded[:included]

assert attributes["full-description"] == data[:full_description]
assert attributes["body"]["data-attr"] == "foo"
assert attributes["inserted-at"] == data[:inserted_at]

assert Enum.find(included, fn i -> i[:type] == "user" && i[:id] == "2" end)[:attributes][
"last-name"
] == "bonds"
author2 = Enum.find(included, fn i -> i[:type] == "user" && i[:id] == "2" end)
assert author2 != nil
assert author2[:attributes]["first-name"]["data-attr"] == "foo"
assert author2[:attributes]["last-name"] == "bonds"

assert Enum.find(included, fn i -> i[:type] == "user" && i[:id] == "4" end)[:attributes][
"last-name"
] == "bronds"
author4 = Enum.find(included, fn i -> i[:type] == "user" && i[:id] == "4" end)
assert author4 != nil
assert author4[:attributes]["last-name"] == "bronds"

assert List.first(relationships["best-comments"][:data])[:id] == "5"

Expand All @@ -624,6 +631,56 @@ defmodule JSONAPI.SerializerTest do
end
end

describe "when configured to dasherize fields non-recursively" do
setup do
Application.put_env(:jsonapi, :field_transformation, :dasherize_shallow)

on_exit(fn ->
Application.delete_env(:jsonapi, :field_transformation)
end)

{:ok, []}
end

test "serialize properly dasherizes attribute and relationship keys only" do
data = %{
id: 1,
text: "Hello",
inserted_at: NaiveDateTime.utc_now(),
body: %{data: "Some data", data_attr: "foo"},
full_description: "This_is_my_description",
author: %{
id: 2,
username: "jbonds",
first_name: %{data: "jerry", data_attr: "foo"},
last_name: "bonds"
},
best_comments: [
%{
id: 5,
text: %{data: "greatest comment ever", data_attr: "foo"},
user: %{id: 4, username: "jack", last_name: "bronds"}
}
]
}

encoded = Serializer.serialize(PostView, data, nil)

attributes = encoded[:data][:attributes]
included = encoded[:included]

assert attributes["full-description"] == data[:full_description]
assert attributes["body"]["data_attr"] == "foo"
assert attributes["inserted-at"] == data[:inserted_at]

author = Enum.find(included, &(&1[:type] == "user" && &1[:id] == "2"))
assert author != nil
assert author[:attributes]["last-name"] == "bonds"
assert author[:attributes]["first-name"]["data"] == "jerry"
assert author[:attributes]["first-name"]["data_attr"] == "foo"
end
end

test "serialize does not merge `included` if not configured" do
data = %{
id: 1,
Expand Down
Loading