Skip to content

Commit

Permalink
feat: implement
Browse files Browse the repository at this point in the history
  • Loading branch information
fahchen committed Jun 28, 2024
1 parent db2abd5 commit 8bb3531
Show file tree
Hide file tree
Showing 14 changed files with 1,666 additions and 11 deletions.
9 changes: 8 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
locals_without_parens = [field: 2, field: 3, parameter: 1, plugin: 1, plugin: 2]

[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
import_deps: [:ecto],
locals_without_parens: locals_without_parens,
export: [
locals_without_parens: locals_without_parens
]
]
1 change: 0 additions & 1 deletion .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ jobs:
runs-on: ubuntu-latest
env:
FORCE_COLOR: 1
MIX_ENV: test
strategy:
fail-fast: false
matrix:
Expand Down
222 changes: 216 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# TypedStructor

**TODO: Add description**
[![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.svg)](https://hex.pm/packages/typed_structor)
[![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/typed_structor/)

`TypedStructor` is a library for defining structs with types effortlessly.
(This library is a rewritten version of [TypedStruct](https://github.com/ejpcmac/typed_struct) because it is no longer actively maintained.)

<!-- MODULEDOC -->

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `typed_structor` to your list of dependencies in `mix.exs`:
Add `:typed_structor` to the list of dependencies in `mix.exs`:

```elixir
def deps do
Expand All @@ -15,7 +21,211 @@ def deps do
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/typed_structor>.
Add `:typed_structor` to your `.formatter.exs` file

```elixir
[
# 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.

```elixir
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:
```elixir
defmodule User do
defstruct [:id, :name, :age]

@type t() :: %__MODULE__{
id: pos_integer() | nil,
# Note: The 'name' can be nil, even though it has a default value.
name: String.t() | nil,
age: non_neg_integer()
}
end
```
Check `TypedStructor.typed_structor/2` and `TypedStructor.field/3` for more information.

> #### `:enforce` and `:default` option {: .warning}
> Note that the `default` option does not affect the `enforce` option.
> If you want to enforce a field, you should explicitly set the `enforce` option to `true`.
>
> Consider the following example, `nil` is a valid value for the `:foo` field.
>
> ```elixir
> defmodule Settings do
> @enforce_keys [:foo]
> defstruct [foo: :bar]
> end
>
> %Settings{} # => ** (ArgumentError) the following keys must also be given when building struct Settings: [:foo]
> # `nil` is a valid value for the `:foo` field
> %Settings{foo: nil} # => %Settings{foo: nil}
> ```
### Options
You can also generate an `opaque` type for the struct,
even changing the type name:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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, pass the `module` option.
```elixir
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
```

You can define the type only without defining the struct,
it is useful when the struct is defined by another library(like `Ecto.Schema`).
```elixir
defmodule User do
use Ecto.Schema
use TypedStructor

typed_structor define_struct: false do
field :id, pos_integer()
field :name, String.t()
field :age, non_neg_integer(), default: 0 # default value is useless in this case
end

schema "users" do
field :name, :string
field :age, :integer, default: 0
end
end
```

## Documentation

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

```elixir
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:

```elixir
typedstruct 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 `TypedStructor.Plugins.Accessible` plugin to define `Access` behavior for the struct.
```elixir
defmodule User do
use TypedStructor

typed_structor do
plugin TypedStructor.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"
```
Loading

0 comments on commit 8bb3531

Please sign in to comment.