defmodule Blazer do
@moduledoc """
Blazer is a case parser for json keys.
available case options:
* `:camel` example: `camelCase`
* `:pascal` example: `PascalCase`
* `:snake` example: `snake_case`
* `:upper` example: `UPPERCASE`
* `:kebab` example: `kebab-case`
* `:title` example: `Title Case`
"""
alias Blazer.Native
alias Blazer.Structs.Opts
@doc"""
Parses a map or a string to the desired case
```elixir
iex(1)> Blazer.parse(%{"firstKey" => "data", "secondKey" => "data"}, case: :snake, keys: :atoms)
{:ok, %{first_key: "data", second_key: "data"}}
iex(2)> Blazer.parse("john_doe", case: :title)
{:ok, "John Doe"}
```
"""
@type opts :: [ case: :camel|:pascal|:snake|:upper|:kebab|:title, keys: :strings|:atoms|:atoms!]
@spec parse(String.t() | map(), Opts.t()) :: {:ok, String.t() | map()} | {:error, String.t()}
def parse(term, opts), do: transform(term, opts)
@spec parse!(String.t() | map(), Opts.t()) :: String.t() | map()
def parse!(term, opts), do: force!(fn -> parse(term, opts) end)
@spec encode_to_iodata!(map(), opts) :: [...]
def encode_to_iodata!(term, opts \\ []) do
{:ok, opts} = get_out_opts(opts)
term
|> parse!(opts)
|> Jason.encode_to_iodata!(opts)
end
@doc"""
encode a map into JSON after parsing its keys
`opts` is passed to Jason, so all its options can be used
"""
@spec encode(map(), opts) :: {:ok, String.t()} | {:error, String.t()}
def encode(term, opts \\ []) do
with {:ok, opts} <- get_out_opts(opts),
{:ok, parsed} <- transform(term, opts),
{:ok, encoded} <- Jason.encode(parsed, opts) do
{:ok, encoded}
else
{:error, reason} -> {:error, reason}
end
end
@spec encode!(map(), opts) :: String.t()
def encode!(term, opts \\ []), do: force!(fn -> encode(term, opts) end)
@doc"""
Decode a JSON into a map and parse its keys
`opts` is passed to Jason, so all its options can be used
"""
@spec decode(String.t(), opts) :: {:ok, map()} | {:error, String.t()}
def decode(json, opts \\ []) do
with {:ok, opts} <- get_in_opts(opts),
{:ok, decoded} <- Jason.decode(json, opts),
{:ok, parsed} <- transform(decoded, opts) do
{:ok, parsed}
else
{:error, reason} -> {:error, reason}
end
end
@spec decode!(String.t(), opts) :: map()
def decode!(json, opts \\ []) do
force!(fn -> decode(json, opts) end)
end
defp transform(term, opts) when is_binary(term),
do: Native.convert_binary(term, opts)
defp transform(term, opts) when is_map(term), do: Native.convert_map(term, opts)
defp transform(_term, _target_opts), do: raise("only strings and maps are accepted.")
defp force!(fun) do
case fun.() do
{:ok, result} -> result
{:error, reason} -> raise reason
end
end
defp get_in_opts(opts), do: get_opts(opts, :inner_case)
defp get_out_opts(opts), do: get_opts(opts, :outer_case)
defp get_opts(opts, direction) do
cond do
length(opts) > 0 -> {:ok, opts}
Application.get_env(:blazer, direction) ->
{:ok, [keys: (Application.get_env(:blazer, :keys) || :strings), case: Application.get_env(:blazer, direction)]}
true ->
{:error, "Target case not provided, either pass an case in the options or set in the configs."}
end
end
end