defmodule CozyCase do @moduledoc ~S""" Converts data between common multiple-word identifier formats, such as snake case, kebab case, camel case and pascal case. Currently, this module provides these main functions: + `snake_case/1` + `kebab_case/1` + `camel_case/1` + `pascal_case/1` ## Multiple-word identifier formats | name | example | | ----------- | ----------------- | | snake case | `welcome_message` | | kebab case | `welcome-message` | | camel case | `welcomeMessage` | | pascal case | `WelcomeMessage` | > Read [Examples of multiple-word identifier formats](https://en.wikipedia.org/w/index.php?title=Naming_convention_(programming)&oldid=1126175049#Examples_of_multiple-word_identifier_formats) for more unsupported formats. ## Examples All these functions have the same intefaces. For strings or atoms, these functions convert them directly: iex> CozyCase.snake_case("HelloWorld") "hello_world" iex> CozyCase.snake_case(HelloWorld) "hello_world" For maps, these functions convert the keys of maps recursively, without touching the values of maps: iex> CozyCase.snake_case(%{ ...> "FamilyMembers" => [ ...> %{ ...> "Name" => "Lily", ...> "Age" => 50, ...> "Hobbies" => ["Dreaming", "Singing"] ...> }, ...> %{ ...> "Name" => "Charlie", ...> "Age" => 55, ...> "Hobbies" => ["Dreaming", "Singing"] ...> } ...> ] ...> }) %{ "family_members" => [ %{"name" => "Lily", "age" => 50, "hobbies" => ["Dreaming", "Singing"]}, %{"name" => "Charlie", "age" => 55, "hobbies" => ["Dreaming", "Singing"]} ] } For lists, these functions convert the keys of maps in lists recursively: iex> CozyCase.snake_case([ ...> %{ ...> "Name" => "Lily", ...> "Age" => 50, ...> "Hobbies" => ["Dreaming", "Singing"] ...> }, ...> %{ ...> "Name" => "Charlie", ...> "Age" => 55, ...> "Hobbies" => ["Dreaming", "Singing"] ...> } ...> ]) [ %{"name" => "Lily", "age" => 50, "hobbies" => ["Dreaming", "Singing"]}, %{"name" => "Charlie", "age" => 55, "hobbies" => ["Dreaming", "Singing"]} ] ## Integrate with other packages `CozyCase` doesn't provide higher-level wrappers. Because everything is a function, it's very easy to integrate `CozyCase` with other packages. ### With [`plug`](https://hexdocs.pm/plug) Converts params in `%Plug.Conn{}` to snake case: defmodule DemoWeb.Plug.SnakeCaseParams do @behaviour Plug @impl true def init(opts), do: opts @impl true def call(%{params: params} = conn, _opts) do %{conn | params: CozyCase.snake_case(params)} end end ### With [`jason`](https://hexdocs.pm/jason) Decode a JSON string: iex> json = "{\"Age\":23,\"FamilyMembers\":[],\"Name\":\"Lenna\"}" iex> Jason.decode!(json, keys: &CozyCase.snake_case/1) %{"name" => "Lenna", "age" => 23, "family_members" => []} Encode a map as a JSON string: iex> map = %{"name" => "Lenna", "age" => 23, "family_members" => []} iex> map |> CozyCase.camel_case() |> Jason.encode!() "{\"age\":23,\"familyMembers\":[],\"name\":\"Lenna\"}" ## Note `CozyCase` is created for the convertion between common multiple-word identifier formats. Because of that, `CozyCase` doesn't handle spaces which are not used as part of indentifier. As the user of `CozyCase`, you should NOT pass string containing spaces, or the result will be surprising. iex> CozyCase.snake_case("welCome Message") "wel_come message" iex> CozyCase.kebab_case("wel_Come Me_ssage") "wel-come me-ssage" """ alias CozyCase.SnakeCase alias CozyCase.KebabCase alias CozyCase.CamelCase alias CozyCase.PascalCase @doc """ Converts other supported cases to snake case. """ @spec snake_case(String.t() | atom()) :: String.t() @spec snake_case(t) :: t when t: map() | list() def snake_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, SnakeCase) def snake_case(term) when is_map(term) or is_list(term), do: convert_nest(term, SnakeCase) @doc """ Converts other supported cases to kebab case. """ @spec kebab_case(String.t() | atom()) :: String.t() @spec kebab_case(t) :: t when t: map() | list() def kebab_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, KebabCase) def kebab_case(term) when is_map(term) or is_list(term), do: convert_nest(term, KebabCase) @doc """ Converts other supported cases to camel case. """ @spec camel_case(String.t() | atom()) :: String.t() @spec camel_case(t) :: t when t: map() | list() def camel_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, CamelCase) def camel_case(term) when is_map(term) or is_list(term), do: convert_nest(term, CamelCase) @doc """ Converts other supported cases to pascal case. """ @spec pascal_case(String.t() | atom()) :: String.t() @spec pascal_case(t) :: t when t: map() | list() def pascal_case(term) when is_binary(term) or is_atom(term), do: convert_plain(term, PascalCase) def pascal_case(term) when is_map(term) or is_list(term), do: convert_nest(term, PascalCase) defp convert_plain(string, module) when is_binary(string), do: module.convert(string) defp convert_plain(atom, module) when is_atom(atom) do Atom.to_string(atom) |> case do "Elixir." <> rest -> rest string -> string end |> module.convert() end defp convert_plain(any, _module), do: any defp convert_nest(map, module) when is_map(map) do try do for {k, v} <- map, into: %{}, do: {convert_plain(k, module), convert_nest(v, module)} rescue # not Enumerable Protocol.UndefinedError -> map end end defp convert_nest(list, module) when is_list(list), do: Enum.map(list, &convert_nest(&1, module)) defp convert_nest(any, _module), do: any end