Skip to content

Commit

Permalink
Add options to transform field keys non-recursively (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
protestContest committed Feb 15, 2024
1 parent 9bbacd1 commit dcbf608
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 16 deletions.
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

0 comments on commit dcbf608

Please sign in to comment.