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