An open API service indexing awesome lists of open source software.

https://github.com/elixir-typed-structor/typed_structor

TypedStructor is a library for defining typed structs, exceptions and record macros with effortlessly.
https://github.com/elixir-typed-structor/typed_structor

Last synced: 7 days ago
JSON representation

TypedStructor is a library for defining typed structs, exceptions and record macros with effortlessly.

Awesome Lists containing this project

README

          

# TypedStructor

[![Build Status](https://github.com/elixir-typed-structor/typed_structor/actions/workflows/elixir.yml/badge.svg)](https://github.com/elixir-typed-structor/typed_structor/actions/workflows/elixir.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/typed_structor)](https://hex.pm/packages/typed_structor)
[![HexDocs](https://img.shields.io/badge/HexDocs-gray)](https://hexdocs.pm/typed_structor)
[![Plugin guides](https://img.shields.io/badge/plugin_guides-indianred?label=%F0%9F%94%A5&labelColor=snow)](https://hexdocs.pm/typed_structor/introduction.html)

TypedStructor eliminates the boilerplate of defining Elixir structs, type specs, and enforced keys separately. Define them once, keep them in sync automatically.

**Before** -- three declarations that must stay in sync manually:

```elixir
defmodule User do
@enforce_keys [:id]
defstruct [:id, :name, :age]

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

**After** -- a single source of truth:

```elixir
defmodule User do
use TypedStructor

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

## Feature Highlights

- **Single definition** -- struct, type spec, and `@enforce_keys` generated from one block
- **Nullable by default** -- unenforced fields without defaults automatically include `| nil`
- **Fine-grained null control** -- override nullability per-field or per-block with the `:null` option
- **Opaque and custom types** -- generate `@opaque`, `@typep`, or rename the type from `t()`
- **Type parameters** -- define generic/parametric types
- **Multiple definers** -- supports structs, exceptions, and Erlang records
- **Plugin system** -- extend behavior at compile time with composable plugins
- **Nested modules** -- define structs in submodules with the `:module` option

## Installation

Add `:typed_structor` to your dependencies in `mix.exs`:

```elixir
def deps do
[
{:typed_structor, "~> 0.6"}
]
end
```

> #### Formatter Setup {: .tip}
>
> Add `:typed_structor` to your `.formatter.exs` for proper indentation:
>
> ```elixir
> [
> import_deps: [..., :typed_structor],
> inputs: [...]
> ]
> ```

## Getting Started

Use `typed_structor` blocks to define fields with their types:

```elixir
defmodule User do
use TypedStructor

typed_structor do
field :id, pos_integer(), enforce: true # Required, never nil
field :name, String.t() # Optional, nullable
field :role, String.t(), default: "user" # Has default, not nullable
end
end
```

### Nullability Rules

The interaction between `:enforce`, `:default`, and `:null` determines whether a field's type includes `nil`:

| `:default` | `:enforce` | `:null` | Type includes `nil`? |
|------------|------------|---------|----------------------|
| `unset` | `false` | `true` | yes |
| `unset` | `false` | `false` | no |
| `set` | - | - | no |
| - | `true` | - | no |

You can set `:null` at the block level to change the default for all fields:

```elixir
typed_structor null: false do
field :id, integer() # Not nullable
field :email, String.t() # Not nullable
field :phone, String.t(), null: true # Override: nullable
end
```

## Options

### Opaque Types

Use `type_kind: :opaque` to hide implementation details:

```elixir
typed_structor type_kind: :opaque do
field :secret, String.t()
end
# Generates: @opaque t() :: %__MODULE__{...}
```

### Custom Type Names

Override the default `t()` type name:

```elixir
typed_structor type_name: :user_data do
field :id, pos_integer()
end
# Generates: @type user_data() :: %__MODULE__{...}
```

### Type Parameters

Create generic types with `parameter/1`:

```elixir
typed_structor do
parameter :value_type
parameter :error_type

field :value, value_type
field :error, error_type
end
# Generates: @type t(value_type, error_type) :: %__MODULE__{...}
```

### Nested Modules

Define structs in submodules:

```elixir
defmodule User do
use TypedStructor

typed_structor module: Profile do
field :email, String.t(), enforce: true
field :bio, String.t()
end
end
# Creates User.Profile with its own struct and type
```

## Plugins

Extend TypedStructor's behavior with plugins that run at compile time:

```elixir
typed_structor do
plugin Guides.Plugins.Accessible

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

See the [Plugin Guides](https://hexdocs.pm/typed_structor/introduction.html) for examples and instructions on writing your own.

## Documentation

Add `@typedoc` inside the block, and `@moduledoc` at the module level as usual:

```elixir
defmodule User do
@moduledoc "User account data"
use TypedStructor

typed_structor do
@typedoc "A user with authentication details"

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

## Advanced Usage

### Exceptions

Define typed exceptions with automatic `__exception__` handling:

```elixir
defmodule HTTPException do
use TypedStructor

typed_structor definer: :defexception, enforce: true do
field :status, non_neg_integer()
field :message, String.t()
end

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

### Records

Create Erlang-compatible records:

```elixir
defmodule UserRecord do
use TypedStructor

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

### Integration with Other Libraries

Use `define_struct: false` to skip struct generation when another library defines the struct:

```elixir
defmodule User do
use TypedStructor

typed_structor define_struct: false do
field :email, String.t(), enforce: true

use Ecto.Schema
@primary_key false

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

This generates only the type spec while letting the other library handle the struct definition.

For full Ecto integration with typed fields, see [EctoTypedSchema](https://github.com/elixir-typed-structor/ecto_typed_schema) -- a companion library built on TypedStructor.

## Learn More

- [HexDocs](https://hexdocs.pm/typed_structor) -- full API reference and guides
- [Plugin Guides](https://hexdocs.pm/typed_structor/introduction.html) -- build and use plugins
- [Changelog](https://hexdocs.pm/typed_structor/changelog.html) -- release history