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

Allow options for key format #55

Closed
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
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ locals_without_parens = [
includes: 1,
index: 1,
index: 2,
key_transformer: 1,
keys: 1,
log_errors?: 1,
paginate?: 1,
Expand Down
5 changes: 5 additions & 0 deletions lib/ash_json_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ defmodule AshJsonApi do
Extension.get_opt(api, [:json_api], :log_errors?, false, true)
end

def key_transformer(api) do
Extension.get_opt(api, [:json_api], :key_transformer, false, true)
end


defmacro forward(path, api, opts \\ []) do
quote bind_quoted: [path: path, api: api, opts: opts] do
case Code.ensure_compiled(api) do
Expand Down
5 changes: 5 additions & 0 deletions lib/ash_json_api/api/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ defmodule AshJsonApi.Api do
type: :string,
doc: "The route prefix at which you are serving the JSON:API"
],
key_transformer: [
type: :string,
doc: "Transformer to use on attribute names.",
default: "snake_case",
],
serve_schema?: [
type: :boolean,
doc: "Whether or not create a /schema route that serves the JSON schema of your API",
Expand Down
4 changes: 4 additions & 0 deletions lib/ash_json_api/resource/transformers/key_transformer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
defmodule AshJsonApi.Resource.Transformers.KeyTransformer do
@callback convert_from(type :: String.t()) :: String.t()
@callback convert_to(type :: String.t()) :: String.t()
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule AshJsonApi.Resource.Transformers.KeyTransformer.CamelCase do
@behaviour AshJsonApi.Resource.Transformers.KeyTransformer
@doc """
Transforms snake case attribute names to camelCase.

iex> #{__MODULE__}.transform_in("first_name")
"firstName"
"""
def convert_to(attribute), do: camelize(attribute)

@doc """
Transforms snake case attribute names to camelCase.

iex> #{__MODULE__}.transform_out("firstName")
"first_name"
"""
def convert_from(attribute), do: underscore(attribute)

defp camelize(word, option \\ :lower) do
case Regex.split(~r/(?:^|[-_])|(?=[A-Z])/, to_string(word)) do
words ->
words
|> Enum.filter(&(&1 != ""))
|> camelize_list(option)
|> Enum.join()
end
end

defp camelize_list([], _), do: []

defp camelize_list([h | tail], :upper) do
[capitalize(h)] ++ camelize_list(tail, :upper)
end

defp camelize_list([h | tail], :lower) do
[lowercase(h)] ++ camelize_list(tail, :upper)
end

defp capitalize(word), do: String.capitalize(word)
defp lowercase(word), do: String.downcase(word)

defp underscore(word) when is_binary(word) do
word
|> String.replace(~r/([A-Z]+)([A-Z][a-z])/, "\\1_\\2")
|> String.replace(~r/([a-z\d])([A-Z])/, "\\1_\\2")
|> String.replace(~r/-/, "_")
|> String.downcase()
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule AshJsonApi.Resource.Transformers.KeyTransformer.Dasherize do
@behaviour AshJsonApi.Resource.Transformers.KeyTransformer
@doc """
Transforms snake case attribute names to dasherized.

** Example
iex> #{__MODULE__}.transform_in("first_name")
"first-name"
"""
def convert_to(attribute), do: dasherize(attribute)

@doc """
Transforms dasherized attribute names to snake case.

** Example
iex> #{__MODULE__}.transform_out("first-name")
"first_name"
"""
def convert_from(attribute), do: underscore(attribute)

defp dasherize(string, option \\ "-") do
case Regex.split(~r/(?:^|[-_])|(?=[A-Z])/, to_string(string)) do
words ->
words
|> Enum.filter(&(&1 != ""))
|> Enum.join(option)
end
end

defp underscore(word) when is_binary(word) do
word
|> String.replace(~r/([A-Z]+)([A-Z][a-z])/, "\\1_\\2")
|> String.replace(~r/([a-z\d])([A-Z])/, "\\1_\\2")
|> String.replace(~r/-/, "_")
|> String.downcase()
end
end
31 changes: 22 additions & 9 deletions lib/ash_json_api/serializer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,11 @@ defmodule AshJsonApi.Serializer do
end

defp serialize_one_record(request, %resource{} = record) do
serializer_format = AshJsonApi.key_transformer(request.api)

%{
id: AshJsonApi.Resource.encode_primary_key(record),
type: AshJsonApi.Resource.type(resource),
type: transform_field(AshJsonApi.Resource.type(resource), serializer_format),
attributes: serialize_attributes(request, record),
relationships: serialize_relationships(request, record),
links: %{} |> add_one_record_self_link(request, record)
Expand Down Expand Up @@ -429,6 +431,8 @@ defmodule AshJsonApi.Serializer do
defp add_keyset(meta, _), do: meta

defp serialize_relationships(request, %resource{} = record) do
serializer_format = AshJsonApi.key_transformer(request.api)

resource
|> Ash.Resource.Info.public_relationships()
|> Enum.into(%{}, fn relationship ->
Expand All @@ -437,9 +441,9 @@ defmodule AshJsonApi.Serializer do
links: relationship_links(record, request, relationship),
meta: %{}
}
|> add_linkage(record, relationship)
|> add_linkage(record, relationship, serializer_format)

{relationship.name, value}
{transform_field(relationship.name, serializer_format), value}
end)
end

Expand Down Expand Up @@ -491,10 +495,10 @@ defmodule AshJsonApi.Serializer do
end
end

defp add_linkage(payload, record, %{destination: destination, cardinality: :one, name: name}) do
defp add_linkage(payload, record, %{destination: destination, cardinality: :one, name: name}, serializer_format) do
case record do
%{__linkage__: %{^name => [%{id: id}]}} ->
Map.put(payload, :data, %{id: id, type: AshJsonApi.Resource.type(destination)})
Map.put(payload, :data, %{id: id, type: transform_field(AshJsonApi.Resource.type(destination), serializer_format)})

# There could be another case here if a bug in the system gave us a list
# of more than one shouldn't happen though
Expand All @@ -507,11 +511,12 @@ defmodule AshJsonApi.Serializer do
defp add_linkage(
payload,
record,
%{destination: destination, cardinality: :many, name: name} = relationship
%{destination: destination, cardinality: :many, name: name} = relationship,
serializer_format
) do
case record do
%{__linkage__: %{^name => linkage}} ->
type = AshJsonApi.Resource.type(destination)
type = transform_field(AshJsonApi.Resource.type(destination), serializer_format)

Map.put(
payload,
Expand Down Expand Up @@ -575,12 +580,12 @@ defmodule AshJsonApi.Serializer do

defp serialize_attributes(request, %resource{} = record) do
fields = Map.get(request.fields || %{}, resource) || default_attributes(resource)

serializer_format = AshJsonApi.key_transformer(request.api)
Enum.reduce(fields, %{}, fn field, acc ->
if field == :id do
acc
else
Map.put(acc, field, Map.get(record, field))
Map.put(acc, transform_field(field, serializer_format), Map.get(record, field))
end
end)
end
Expand All @@ -600,4 +605,12 @@ defmodule AshJsonApi.Serializer do
# end)
# |> URI.to_string()
end

defp transform_field(field, serializer_format) do
case serializer_format do
"camel_case" -> AshJsonApi.Resource.Transformers.KeyTransformer.CamelCase.convert_to(field)
"dasherized" -> AshJsonApi.Resource.Transformers.KeyTransformer.Dasherize.convert_to(field)
_ -> field
end
end
end
5 changes: 5 additions & 0 deletions test/unit/transformers_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule AshJsonApi.ResourceTest do
use ExUnit.Case
doctest AshJsonApi.Resource.Transformers.KeyTransformer.Dasherize
doctest AshJsonApi.Resource.Transformers.KeyTransformer.CamelCase
end