diff --git a/lib/jsonapi/plugs/query_parser.ex b/lib/jsonapi/plugs/query_parser.ex index 5e27b8bf..a9153028 100644 --- a/lib/jsonapi/plugs/query_parser.ex +++ b/lib/jsonapi/plugs/query_parser.ex @@ -253,7 +253,7 @@ defmodule JSONAPI.QueryParser do if JString.field_transformation() == :underscore do fields else - JString.underscore(fields) + JString.expand_fields(fields, &JString.underscore/1) end end end diff --git a/lib/jsonapi/plugs/underscore_parameters.ex b/lib/jsonapi/plugs/underscore_parameters.ex index 83c926f5..7e685b2a 100644 --- a/lib/jsonapi/plugs/underscore_parameters.ex +++ b/lib/jsonapi/plugs/underscore_parameters.ex @@ -33,7 +33,7 @@ defmodule JSONAPI.UnderscoreParameters do import Plug.Conn - import JSONAPI.Utils.String, only: [underscore: 1] + alias JSONAPI.Utils.String, as: JString @doc false def init(_opts) do @@ -44,7 +44,7 @@ defmodule JSONAPI.UnderscoreParameters do content_type = get_req_header(conn, "content-type") if JSONAPI.mime_type() in content_type do - new_params = underscore(params) + new_params = JString.expand_fields(params, &JString.underscore/1) Map.put(conn, :params, new_params) else diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index 4e9009dd..4c92d1da 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -231,8 +231,8 @@ defmodule JSONAPI.Serializer do defp transform_fields(fields) do case JString.field_transformation() do - :camelize -> JString.camelize(fields) - :dasherize -> JString.dasherize(fields) + :camelize -> JString.expand_fields(fields, &JString.camelize/1) + :dasherize -> JString.expand_fields(fields, &JString.dasherize/1) _ -> fields end end diff --git a/lib/jsonapi/utils/string.ex b/lib/jsonapi/utils/string.ex index a74636d2..6319b145 100644 --- a/lib/jsonapi/utils/string.ex +++ b/lib/jsonapi/utils/string.ex @@ -17,56 +17,26 @@ defmodule JSONAPI.Utils.String do iex> underscore("top-posts") "top_posts" + iex> underscore(:top_posts) + "top_posts" + iex> underscore("-top-posts") "-top_posts" iex> underscore("-top--posts-") "-top--posts-" - iex> underscore(%{"foo-bar" => "baz"}) - %{"foo_bar" => "baz"} - - iex> underscore({"foo-bar", "dollar-sol"}) - {"foo_bar", "dollar-sol"} - - iex> underscore({"foo-bar", %{"a-d" => "z-8"}}) - {"foo_bar", %{"a_d" => "z-8"}} - - iex> underscore(%{"f-b" => %{"a-d" => "z"}, "c-d" => "e"}) - %{"f_b" => %{"a_d" => "z"}, "c_d" => "e"} - - iex> underscore(:"foo-bar") - :foo_bar - - iex> underscore(%{"f-b" => "a-d"}) - %{"f_b" => "a-d"} - """ + @spec underscore(String.t()) :: String.t() def underscore(value) when is_binary(value) do String.replace(value, ~r/([a-zA-Z0-9])-([a-zA-Z0-9])/, "\\1_\\2") end - def underscore(map) when is_map(map) do - Enum.into(map, %{}, &underscore/1) - end - - def underscore({key, value}) when is_map(value) do - {underscore(key), underscore(value)} - end - - def underscore({key, value}) do - {underscore(key), value} - end - + @spec underscore(atom) :: atom def underscore(value) when is_atom(value) do value |> to_string() |> underscore() - |> String.to_atom() - end - - def underscore(value) do - value end @doc """ @@ -86,32 +56,18 @@ defmodule JSONAPI.Utils.String do "_top__posts_" """ + @spec dasherize(atom) :: atom def dasherize(value) when is_atom(value) do value |> to_string() |> dasherize() end + @spec dasherize(String.t()) :: String.t() def dasherize(value) when is_binary(value) do String.replace(value, ~r/([a-zA-Z0-9])_([a-zA-Z0-9])/, "\\1-\\2") end - def dasherize(%{__struct__: _} = value) when is_map(value) do - value - end - - def dasherize(value) when is_map(value) do - Enum.into(value, %{}, &dasherize/1) - end - - def dasherize({key, value}) do - if is_map(value) do - {dasherize(key), dasherize(value)} - else - {dasherize(key), value} - end - end - @doc """ Replace underscores or dashes between words in `value` with camelCasing @@ -122,6 +78,9 @@ defmodule JSONAPI.Utils.String do iex> camelize("top_posts") "topPosts" + iex> camelize(:top_posts) + "topPosts" + iex> camelize("_top_posts") "_topPosts" @@ -129,12 +88,14 @@ defmodule JSONAPI.Utils.String do "_top__posts_" """ + @spec camelize(atom) :: atom def camelize(value) when is_atom(value) do value |> to_string() |> camelize() end + @spec camelize(String.t()) :: String.t() def camelize(value) when is_binary(value) do with words <- Regex.split( @@ -154,20 +115,68 @@ defmodule JSONAPI.Utils.String do [String.capitalize(h)] ++ camelize_list(t) end - def camelize(%{__struct__: _} = value) when is_map(value) do - value + @doc """ + + ## Examples + + iex> expand_fields(%{"foo-bar" => "baz"}, &underscore/1) + %{"foo_bar" => "baz"} + + iex> expand_fields(%{"foo_bar" => "baz"}, &dasherize/1) + %{"foo-bar" => "baz"} + + iex> expand_fields(%{"foo-bar" => "baz"}, &camelize/1) + %{"fooBar" => "baz"} + + iex> expand_fields({"foo-bar", "dollar-sol"}, &underscore/1) + {"foo_bar", "dollar-sol"} + + iex> expand_fields({"foo-bar", %{"a-d" => "z-8"}}, &underscore/1) + {"foo_bar", %{"a_d" => "z-8"}} + + iex> expand_fields(%{"f-b" => %{"a-d" => "z"}, "c-d" => "e"}, &underscore/1) + %{"f_b" => %{"a_d" => "z"}, "c_d" => "e"} + + iex> expand_fields(%{"f-b" => %{"a-d" => %{"z-w" => "z"}}, "c-d" => "e"}, &underscore/1) + %{"f_b" => %{"a_d" => %{"z_w" => "z"}}, "c_d" => "e"} + + iex> expand_fields(:"foo-bar", &underscore/1) + "foo_bar" + + iex> expand_fields(:foo_bar, &dasherize/1) + "foo-bar" + + iex> expand_fields(:"foo-bar", &camelize/1) + "fooBar" + + iex> expand_fields(%{"f-b" => "a-d"}, &underscore/1) + %{"f_b" => "a-d"} + + iex> expand_fields(%{"inserted-at" => ~N[2019-01-17 03:27:24.776957]}, &underscore/1) + %{"inserted_at" => ~N[2019-01-17 03:27:24.776957]} + + """ + @spec expand_fields(map, function) :: map + def expand_fields(%{__struct__: _} = value, _fun), do: value + + @spec expand_fields(map, function) :: map + def expand_fields(map, fun) when is_map(map) do + Enum.into(map, %{}, &expand_fields(&1, fun)) end - def camelize(value) when is_map(value) do - Enum.into(value, %{}, &camelize/1) + @spec expand_fields(tuple, function) :: tuple + def expand_fields({key, value}, fun) when is_map(value) do + {fun.(key), expand_fields(value, fun)} end - def camelize({key, value}) do - if is_map(value) do - {camelize(key), camelize(value)} - else - {camelize(key), value} - end + @spec expand_fields(tuple, function) :: tuple + def expand_fields({key, value}, fun) do + {fun.(key), value} + end + + @spec expand_fields(any, function) :: any + def expand_fields(value, fun) do + fun.(value) end defp normalized_underscore_to_dash_config(value) when is_boolean(value) do