Struct

Library for dealing with data structures
Installation
- Add
struct
to your list of dependencies inmix.exs
:
def deps do
[{:struct, "~> 1.0.0"}]
end
- Ensure
struct
is started before your application:
def application do
[applications: [:struct]]
end
Usage
Suppose you have some user input from several sources (DB, HTTP request, WebSocket), and you will need to process that data into something type-validated, like User entity. With this library you can define a type-validated struct for this entity:
defmodule User do
use Struct
structure do
field :name
field :age, :integer
end
end
And use it to cast your data into something identical, to prevent type coercion in different places of your code. Like this:
iex> User.make(%{"name" => "John Doe", "age" => "37"})
{:ok, %User{age: 37, name: "John Doe"}}
Pretty neat, yeah? But what if you need more complex type? We have a solution!
defmodule Answer do
@behaviour Struct.Type
def cast("yes"), do: {:ok, true}
def cast("no"), do: {:ok, false}
def cast(_), do: {:error, :invalid_answer}
end
And use it in your struct like this:
defmodule Quiz do
use Struct
structure do
field :user_id, :integer
field :answers, {:array, Answer}
end
end
iex> Quiz.make(%{user_id: 42, answers: ["yes", "no", "no", "yes"]})
{:ok, %Quiz{answers: [true, false, false, true], user_id: 42}}
What if we need to parse ‘optimized’ query string from URL, like list of user ids separated by a comma? Do we need to create a custom type for each boxed type? No! Just use type composition feature:
defmodule CommaList do
@behaviour Struct.Type
def cast(""), do: {:ok, []}
def cast(v) when is_binary(v), do: {:ok, String.split(v, ",")}
def cast(v) when is_list(v), do: {:ok, v}
def cast(_), do: :error
end
defmodule SearchFilterRequest do
use Struct
structure do
field :user_ids, [CommaList, {:array, :integer}], default: []
end
end
iex> SearchFilterRequest.make(%{"user_ids" => "1,2,42"})
{:ok, %SearchFilterRequest{user_ids: [1, 2, 42]}}
Also we have default
option in our user_ids
field:
iex> SearchFilterRequest.make(%{})
{:ok, %SearchFilterRequest{user_ids: []}}
What if I have a lot of identical code?
You can use already defined structures as types:
defmodule Comment do
use Struct
structure do
field :text
end
end
defmodule Post do
use Struct
structure do
field :title
field :comments, {:array, Comment}
end
end
iex> Post.make(%{title: "Some article", comments: [%{"text" => "cool!"}, %{text: "awesome!!!"}]})
{:ok, %Post{comments: [%Comment{text: "cool!"}, %Comment{text: "awesome!!!"}], title: "Some article"}}
And include repeated fields in structs:
defmodule PK do
use Struct
structure do
field :primary_key, :integer
end
end
defmodule Timestamps do
use Struct
structure do
field :inserted_at, :utc_datetime
field :updated_at, :utc_datetime, default: nil
end
end
defmodule User do
use Struct
structure do
include PK
include Timestamps
field :name
end
end
iex> User.make(%{name: "John Doe", inserted_at: "2015-01-23 23:50:07", primary_key: 42})
{:ok,
%User{inserted_at: #DateTime<2015-01-23 23:50:07Z>, name: "John Doe",
primary_key: 42, updated_at: nil}}
Types
Primitive types
t()
:- integer
- float
- boolean
- string
- binary
- decimal
- utc_datetime
- naive_datetime
- date
- time
- any
{:array, t()}
{:map, t()}
[t()]
Complex (custom) types
You can use Ecto custom types like Ecto.UUID or implement by yourself:
defmodule CustomType do
@behaviour Struct.Type
@spec cast(term) :: {:ok, term} | {:error, term} | :error
def cast(value) do
{:ok, value}
end
end
Notice that cast/1
can return error with reason, this behaviour is supported only by Struct and you can’t use types defined using Struct in Ecto schemas.
Struct definition
defmodule User do
use Struct, struct_opts
structure do
include module_name
field name
field name, type
field name, type, field_opts
end
end
Where:
use Struct, struct_opts
where:struct_opts
— options passed to everymake/2
andmake!/2
calls as default options;
include module_name
where:module_name
— is struct module, that validates for existence in compile time;
field name, type, field_opts
where:name
— atom;type
— primitive or custom type, that validates for existence in compile time;field_opts
.
Errors while making structs
When you provide invalid data to your structs you can get tuple with errors:
iex> Post.make
{:error, %{comments: :missing, title: :missing}}
iex> Post.make(%{comments: %{}, title: :test})
{:error, %{comments: :invalid, title: :invalid}}
iex> Post.make(%{comments: [%{}], title: "what the title?"})
{:error, %{comments: %{text: :missing}}}
Or receive an exception with invalid data:
iex> Post.make!
** (Struct.MakeError) %{comments: {:missing, nil}, title: {:missing, nil}}
iex:10: Post.make!/2
iex> Post.make!(%{comments: %{}, title: :test})
** (Struct.MakeError) %{comments: {:invalid, %{}}, title: {:invalid, :test}}
iex:10: Post.make!/2
iex> Post.make!(%{comments: [%{}], title: "what the title?"})
** (Struct.MakeError) %{comments: %{text: {:missing, [nil]}}}
iex:10: Post.make!/2
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request