Packages

ExConstructor generates constructor functions for your structs, handling map-vs-keyword-list, string-vs-atom-keys, and camelCase-vs-under_score issues automatically.

Current section

Files

Jump to
exconstructor lib exconstructor.ex
Raw

lib/exconstructor.ex

defmodule ExConstructor do
@moduledoc ~s"""
ExConstructor is an Elixir library which makes it easier to instantiate
structs from external data, such as that emitted by a JSON parser.
It provides a `define_constructor` macro that can be invoked from struct
modules. This macro defines a constructor, by default called `new`,
that accepts struct values as either a map or a dict, whose keys are
either strings, and whose keys may be formatted in `camelCase` or
`under_score` format (or a literal match on the struct field name).
## Installation
1. Add ExConstructor to your list of dependencies in `mix.exs`:
def deps do
[{:exconstructor, "~> #{ExConstructor.Mixfile.project[:version]}"}]
end
2. Ensure ExConstructor is started before your application:
def application do
[applications: [:exconstructor]]
end
## Usage
Example:
iex(1)> defmodule TestStruct do
...(1)> import ExConstructor
...(1)> defstruct field_one: 1,
...(1)> field_two: 2,
...(1)> field_three: 3,
...(1)> field_four: 4,
...(1)> field_five: 5
...(1)> define_constructor
...(1)> end
iex(2)> TestStruct.new(%{"field_one" => "a", "fieldTwo" => "b", :field_three => "c", :fieldFour => "d"})
%TestStruct{field_one: "a", field_two: "b", field_three: "c", field_four: "d", field_five: 5}
## Authorship and License
ExConstructor is copyright 2016 Appcues, Inc.
ExConstructor is released under the
[MIT License](https://github.com/appcues/exsentry/blob/master/LICENSE.txt).
"""
@doc ~S"""
Defines a constructor for the struct defined in the module in which this
macro was invoked. This constructor accepts a map or dict of keys and values.
Keys may be strings or atoms, in camelCase or under_score format.
"""
defmacro define_constructor(function_name \\ :new) do
quote do
def unquote(function_name)(map_or_dict, opts \\ []) do
default = %__MODULE__{}
map = Map.new(map_or_dict)
keys = default |> Map.from_struct |> Map.keys
Enum.reduce keys, default, fn (atom, acc) ->
str = to_string(k)
under_str = Mix.Utils.underscore(str)
camel_str = Mix.Utils.camelize(str) |> ExConstructor.Utils.lcfirst
under_atom = String.to_atom(under_str)
camel_atom = String.to_atom(camel_str)
value = cond do
Map.has_key?(map, str) -> Map.get(map, str)
Map.has_key?(map, atom) -> Map.get(map, atom)
Map.has_key?(map, under_str) -> Map.get(map, under_str)
Map.has_key?(map, under_atom) -> Map.get(map, under_atom)
Map.has_key?(map, camel_str) -> Map.get(map, camel_str)
Map.has_key?(map, camel_atom) -> Map.get(map, camel_atom)
true -> Map.get(default, atom)
end
Map.put(acc, atom, value)
end
end
end
end
defmodule Utils do
@doc ~s"""
Returns a copy of `str` with the first character lowercased.
iex> ExConstructor.Utils.lcfirst("OmgThisIsCool")
"omgThisIsCool"
"""
def lcfirst(str) do
first = String.slice(str, 0..0) |> String.downcase
first <> String.slice(str, 1..-1)
end
end
end