Home

Awesome

TypedStructor

Build Status Hex.pm Document Plugin guides

<!-- MODULEDOC -->

TypedStructor is a library for defining structs with types effortlessly. (This library is a rewritten version of TypedStruct because it is no longer actively maintained.)

Installation

Add :typed_structor to the list of dependencies in mix.exs:

def deps do
  [
    {:typed_structor, "~> 0.5"}
  ]
end

Add :typed_structor to your .formatter.exs file

[
  # import the formatter rules from `:typed_structor`
  import_deps: [..., :typed_structor],
  inputs: [...]
]

Usage

General usage

To define a struct with types, use TypedStructor, and then define fields under the TypedStructor.typed_structor/2 macro, using the TypedStructor.field/3 macro to define each field.

defmodule User do
  # use TypedStructor to import the `typed_structor` macro
  use TypedStructor

  typed_structor do
    # Define each field with the `field` macro.
    field :id, pos_integer()

    # set a default value
    field :name, String.t(), default: "Unknown"

    # enforce a field
    field :age, non_neg_integer(), enforce: true
  end
end

This is equivalent to:

defmodule User do
  defstruct [:id, :name, :age]

  @type t() :: %__MODULE__{
    id: pos_integer() | nil,
    # Note: The 'name' can not be nil, for it has a default value.
    name: String.t(),
    age: non_neg_integer()
  }
end

Check TypedStructor.typed_structor/2 and TypedStructor.field/3 for more information.

Options

You can also generate an opaque type for the struct, even changing the type name:

defmodule User do
  use TypedStructor

  typed_structor type_kind: :opaque, type_name: :profile do
    field :id, pos_integer()
    field :name, String.t()
    field :age, non_neg_integer()
  end
end

This is equivalent to:

defmodule User do
  use TypedStructor

  defstruct [:id, :name, :age]

  @opaque profile() :: %__MODULE__{
    id: pos_integer() | nil,
    name: String.t() | nil,
    age: non_neg_integer() | nil
  }
end

Type parameters also can be defined:

defmodule User do
  use TypedStructor

  typed_structor do
    parameter :id
    parameter :name

    field :id, id
    field :name, name
    field :age, non_neg_integer()
  end
end

becomes:

defmodule User do
  @type t(id, name) :: %__MODULE__{
    id: id | nil,
    name: name | nil,
    age: non_neg_integer() | nil
  }

  defstruct [:id, :name, :age]
end

If you prefer to define a struct in a submodule, you can use the module option with TypedStructor. This allows you to encapsulate the struct definition within a specific submodule context.

Consider this example:

defmodule User do
  use TypedStructor

  # `%User.Profile{}` is generated
  typed_structor module: Profile do
    field :id, pos_integer()
    field :name, String.t()
    field :age, non_neg_integer()
  end
end

When defining a struct in a submodule, the typed_structor block functions similarly to a defmodule block. Therefore, the previous example can be alternatively written as:

defmodule User do
  defmodule Profile do
    use TypedStructor

    typed_structor do
      field :id, pos_integer()
      field :name, String.t()
      field :age, non_neg_integer()
    end
  end
end

Furthermore, the typed_structor block allows you to define functions, derive protocols, and more, just as you would within a defmodule block. Here's a example:

defmodule User do
  use TypedStructor

  typed_structor module: Profile, define_struct: false do
    @derive {Jason.Encoder, only: [:email]}
    field :email, String.t()

    use Ecto.Schema
    @primary_key false

    schema "users" do
      Ecto.Schema.field(:email, :string)
    end

    import Ecto.Changeset

    def changeset(%__MODULE__{} = user, attrs) do
      user
      |> cast(attrs, [:email])
      |> validate_required([:email])
    end
  end
end

Now, you can interact with these structures:

iex> User.Profile.__struct__()
%User.Profile{__meta__: #Ecto.Schema.Metadata<:built, "users">, email: nil}
iex> Jason.encode!(%User.Profile{})
"{\"email\":null}"
iex> User.Profile.changeset(%User.Profile{}, %{"email" => "my@email.com"})
#Ecto.Changeset<
  action: nil,
  changes: %{email: "my@email.com"},
  errors: [],
  data: #User.Profile<>,
  valid?: true
>

Define an Exception

In Elixir, an exception is defined as a struct that includes a special field named __exception__. To define an exception, use the defexception definer within the typed_structor block.

defmodule HTTPException do
  use TypedStructor

  typed_structor definer: :defexception, enforce: true do
    field :status, non_neg_integer()
  end

  @impl Exception
  def message(%__MODULE__{status: status}) do
    "HTTP status #{status}"
  end
end

Define records related macros

In Elixir, you can use the Record module to define and work with Erlang records, making interoperability between Elixir and Erlang more seamless.

defmodule TypedStructor.User do
  use TypedStructor

  typed_structor definer: :defrecord, record_name: :user, record_tag: User, enforce: true do
    field :name, String.t()
    field :age, pos_integer()
  end
end

Documentation

To add a @typedoc to the struct type, just add the attribute in the typed_structor block:

typed_structor do
  @typedoc "A typed user"

  field :id, pos_integer()
  field :name, String.t()
  field :age, non_neg_integer()
end

You can also document submodules this way:

typedstructor module: Profile do
  @moduledoc "A user profile struct"
  @typedoc "A typed user profile"

  field :id, pos_integer()
  field :name, String.t()
  field :age, non_neg_integer()
end

Plugins

TypedStructor offers a plugin system to enhance functionality. For details on creating a plugin, refer to the TypedStructor.Plugin module.

Here is a example of Guides.Plugins.Accessible plugin to define Access behavior for the struct.

defmodule User do
  use TypedStructor

  typed_structor do
    plugin Guides.Plugins.Accessible

    field :id, pos_integer()
    field :name, String.t()
    field :age, non_neg_integer()
  end
end

user = %User{id: 1, name: "Phil", age: 20}
get_in(user, [:name]) # => "Phil"

Plugins guides {: .tip}

Here are some Plugin Guides for creating your own plugins. Please check them out and feel free to copy-paste the code.