defmodule Dotenvy.Transformer do @moduledoc """ This module provides functionality for converting string values to specific Elixir data types. These conversions were designed to operate on system environment variables, which _always_ store string binaries. """ @typedoc """ The conversion type specifies the target data type to which a string will be converted. For example, `:integer` would indicate a transformation of `"12"` to `12`. The following types are supported: - `:atom` - converts to an atom. An empty string will be the atom `:""` (!). - `:atom?` - converts to an atom. An empty string will be considered `nil` - `:atom!` - converts to an atom. An empty string will raise. - `:boolean` - "false", "0", or an empty string "" will be considered boolean `false`. Any other non-empty value is considered `true`. - `:boolean?` - as above, except an empty string will be considered `nil` - `:boolean!` - as above, except an empty string will raise. - `:charlist` - converts string to charlist. - `:charlist?` - converts string to charlist. Empty string will be considered `nil`. - `:charlist!` - as above, but an empty string will raise. - `:integer` - converts a string to an integer. An empty string will be considered `0`. - `:integer?` - as above, but an empty string will be considered `nil`. - `:integer!` - as above, but an empty string will raise. - `:float` - converts a string to an float. An empty string will be considered `0`. - `:float?` - as above, but an empty string will be considered `nil`. - `:float!` - as above, but an empty string will raise. - `:existing_atom` - converts into an existing atom. Raises error if the atom does not exist. - `:existing_atom?` - as above, but an empty string will be considered `nil`. - `:existing_atom!` - as above, but an empty string will raise. - `:module` - converts a string into an Elixir module name. Raises on error. - `:module?` - as above, but an empty string will be considered `nil`. - `:module!` - as above, but an empty string will raise. - `:string` - no conversion (default) - `:string?` - empty strings will be considered `nil`. - `:string!` - as above, but an empty string will raise. - custom function - see below. ## Custom Callback function When you require more control over the transformation of your value than is possible with the types provided, you can provide an arity 1 function in place of the type. """ @type conversion_type :: :atom | :atom? | :atom! | :boolean | :boolean? | :boolean! | :charlist | :charlist? | :charlist! | :integer | :integer? | :integer! | :float | :float? | :float! | :existing_atom | :existing_atom? | :existing_atom! | :module | :module? | :module! | :string | :string? | :string! | (String.t() -> any()) defmodule Error do @moduledoc false defexception message: "non-empty value required" end @doc """ Converts strings into Elixir data types with support for nil-able values. Raises on error. Each type determines how to interpret the incoming string, e.g. when the `type` is `:integer`, an empty string is considered a `0`; when `:integer?` is the `type`, and empty string is converted to `nil`. Remember: - Use a `?` suffix when an empty string should be considered `nil` (a.k.a. a "nullable" value). - Use a `!` suffix when an empty string is not allowed. Use this when values are required. ## Types See the `t:conversion_types/0` for a description of valid ## Examples iex> to!("debug", :atom) :debug iex> to!("", :boolean) false iex> to!("", :boolean?) nil iex> to!("5432", :integer) 5432 iex> to!("foo", fn val -> val <> "bar" end) "foobar" """ @spec to!(str :: binary(), type :: conversion_type) :: any() def to!(str, :atom) when is_binary(str) do str |> String.trim_leading(":") |> String.to_atom() end def to!("", :atom?), do: nil def to!(str, :atom?), do: to!(str, :atom) def to!("", :atom!), do: raise(Error) def to!(str, :atom!), do: to!(str, :atom) def to!(str, :boolean) when is_binary(str) do str |> String.downcase() |> case do "false" -> false "0" -> false "" -> false _ -> true end end def to!("", :boolean?), do: nil def to!(str, :boolean?), do: to!(str, :boolean) def to!("", :boolean!), do: raise(Error) def to!(str, :boolean!), do: to!(str, :boolean) def to!(str, :charlist) when is_binary(str), do: to_charlist(str) def to!("", :charlist?), do: nil def to!(str, :charlist?), do: to!(str, :charlist) def to!("", :charlist!), do: raise(Error) def to!(str, :charlist!), do: to!(str, :charlist) def to!(str, :existing_atom) when is_binary(str) do str |> String.trim_leading(":") |> String.to_existing_atom() rescue _ -> reraise(Error, "#{inspect(str)}: not an existing atom", __STACKTRACE__) end def to!("", :existing_atom?), do: nil def to!(str, :existing_atom?), do: to!(str, :existing_atom) def to!("", :existing_atom!), do: raise(Error) def to!(str, :existing_atom!), do: to!(str, :existing_atom) def to!("", :float), do: 0 def to!(str, :float) when is_binary(str) do case Float.parse(str) do :error -> raise(Error, "Unparsable") {value, _} -> value end end def to!("", :float?), do: nil def to!(str, :float?), do: to!(str, :float) def to!("", :float!), do: raise(Error) def to!(str, :float!), do: to!(str, :float) def to!("", :integer), do: 0 def to!(str, :integer) when is_binary(str) do case Integer.parse(str) do :error -> raise(Error, "Unparsable") {value, _} -> value end end def to!("", :integer?), do: nil def to!(str, :integer?), do: to!(str, :integer) def to!("", :integer!), do: raise(Error) def to!(str, :integer!), do: to!(str, :integer) def to!(str, :module) when is_binary(str) do "Elixir.#{str}" |> String.to_existing_atom() end def to!("", :module?), do: nil def to!(str, :module?), do: to!(str, :module) def to!("", :module!), do: raise(Error) def to!(str, :module!), do: to!(str, :module) def to!(str, :string) when is_binary(str), do: str def to!("", :string?), do: nil def to!(str, :string?) when is_binary(str), do: str def to!("", :string!), do: raise(Error) def to!(str, :string!) when is_binary(str), do: str def to!(str, callback) when is_function(callback, 1) do callback.(str) end def to!(str, _) when not is_binary(str), do: raise(Error, "Input must be a string.") def to!(_, type), do: raise(Error, "Unknown type #{inspect(type)}") end