Skip to content

Commit

Permalink
feat: add :defexception denfiner to define an exception (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
fahchen authored Sep 25, 2024
1 parent ecaec7d commit 800ca9e
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 17 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,26 @@ iex> User.Profile.changeset(%User.Profile{}, %{"email" => "[email protected]"})
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.

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

## Documentation

To add a `@typedoc` to the struct type, just add the attribute in the typed_structor block:
Expand Down
13 changes: 12 additions & 1 deletion lib/typed_structor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@ defmodule TypedStructor do
* `:type_name` - the name of the type to use for the struct. Defaults to `t`.
## Definer
There are one available definer for now, `:defstruct`, which defines a struct and a type for a given definition.
The available definers are:
- `:defstruct`, which defines a struct and a type for a given definition
- `:defexception`, which defines an exception and a type for a given definition
### `:defstruct` options
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
### `:defexception` options
* `:define_struct` - if `false`, the type will be defined, but the exception struct will not be defined. Defaults to `true`.
### custom definer
defmodule MyStruct do
Expand Down Expand Up @@ -273,6 +279,11 @@ defmodule TypedStructor do
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
TypedStructor.Definer.Defstruct.define(definition)

:defexception ->
require TypedStructor.Definer.Defexception
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
TypedStructor.Definer.Defexception.define(definition)

fun when is_function(fun) ->
then(definition, fun)
end
Expand Down
46 changes: 46 additions & 0 deletions lib/typed_structor/definer/defexception.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule TypedStructor.Definer.Defexception do
@moduledoc """
A definer to define an exception and a type for a given definition.
## Additional options for `typed_structor`
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
## Usage
defmodule MyException do
use TypedStructor
typed_structor definer: :defexception, define_struct: false do
field :message, String.t()
end
end
"""

alias TypedStructor.Definer.Defstruct

@doc """
Defines an exception and a type for a given definition.
"""
defmacro define(definition) do
quote do
unquote(__MODULE__).__exception_ast__(unquote(definition))

require Defstruct
Defstruct.__type_ast__(unquote(definition))
end
end

@doc false
defmacro __exception_ast__(definition) do
quote bind_quoted: [definition: definition] do
if Keyword.get(definition.options, :define_struct, true) do
{fields, enforce_keys} =
Defstruct.__extract_fields_and_enforce_keys__(definition)

@enforce_keys Enum.reverse(enforce_keys)
defexception fields
end
end
end
end
36 changes: 20 additions & 16 deletions lib/typed_structor/definer/defstruct.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,33 @@ defmodule TypedStructor.Definer.Defstruct do

@doc false
defmacro __struct_ast__(definition) do
ast =
quote do
{fields, enforce_keys} =
Enum.map_reduce(unquote(definition).fields, [], fn field, acc ->
name = Keyword.fetch!(field, :name)
default = Keyword.get(field, :default)
alias TypedStructor.Definer.Defstruct

if Keyword.get(field, :enforce, false) and not Keyword.has_key?(field, :default) do
{{name, default}, [name | acc]}
else
{{name, default}, acc}
end
end)
quote bind_quoted: [definition: definition] do
if Keyword.get(definition.options, :define_struct, true) do
{fields, enforce_keys} =
Defstruct.__extract_fields_and_enforce_keys__(definition)

@enforce_keys Enum.reverse(enforce_keys)
defstruct fields
end
end
end

quote do
if Keyword.get(unquote(definition).options, :define_struct, true) do
unquote(ast)
@doc false
@spec __extract_fields_and_enforce_keys__(TypedStructor.Definition.t()) ::
{Keyword.t(), [atom()]}
def __extract_fields_and_enforce_keys__(definition) do
Enum.map_reduce(definition.fields, [], fn field, acc ->
name = Keyword.fetch!(field, :name)
default = Keyword.get(field, :default)

if Keyword.get(field, :enforce, false) and not Keyword.has_key?(field, :default) do
{{name, default}, [name | acc]}
else
{{name, default}, acc}
end
end
end)
end

@doc false
Expand Down
63 changes: 63 additions & 0 deletions test/definer_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule DefinerTest do
@compile {:no_warn_undefined, __MODULE__.Struct}
@compile {:no_warn_undefined, __MODULE__.MyException}

use TypedStructor.TestCase, async: true

Expand Down Expand Up @@ -82,4 +83,66 @@ defmodule DefinerTest do
cleanup_modules([__MODULE__.Struct], ctx.tmp_dir)
end
end

describe "defexception" do
@tag :tmp_dir
test "works", ctx do
expected_types =
with_tmpmodule MyException, ctx do
@type t() :: %__MODULE__{
message: String.t() | nil
}

defexception [:message]
after
fetch_types!(MyException)
end

generated_types =
with_tmpmodule MyException, ctx do
use TypedStructor

typed_structor definer: :defexception do
field :message, String.t()
end

@impl Exception
def exception(arguments) do
%__MODULE__{message: Keyword.fetch!(arguments, :message)}
end

@impl Exception
def message(%__MODULE__{message: message}) do
message
end
after
exception = MyException.exception(message: "this is an error")
assert is_exception(exception)
assert "this is an error" === Exception.message(exception)
fetch_types!(MyException)
end

assert_type expected_types, generated_types
end

@tag :tmp_dir
test "define_struct false", ctx do
deftmpmodule MyException, ctx do
use TypedStructor

typed_structor define_struct: false do
parameter :message

field :message, message
end

defexception message: "error"
end

assert %{__struct__: MyException, __exception__: true, message: "error"} ===
struct(MyException)
after
cleanup_modules([__MODULE__.MyException], ctx.tmp_dir)
end
end
end

0 comments on commit 800ca9e

Please sign in to comment.