View Source TypedStructNimbleOptions
TypedStructNimbleOptions is a plugin for TypedStruct that allows to easily type, validate & document Elixir structs, all in one place and with little boilerplate.
It leverages NimbleOptions for validation & documentation.
Each field
of TypedStruct is a key in the generated NimbleOptions schema.
Many options on field
are passed directly to NimbleOptions, but in most cases they're automatically derived from the type (and other TypedStruct options).
For example
field :attrs, %{optional(atom()) => String.t()}, enforce: true, doc: "User attributes."
will generate and validate with the following NimbleOptions schema:
attrs: [
type: {:map, :atom, :string}, # automatically derived from type
required: true, # due to enforce: true
doc: "User attributes.",
type_spec: quote(do: %{optional(atom()) => String.t()})
]
Example
defmodule Person do
@moduledoc "A struct representing a person."
@moduledoc since: "0.1.0"
use TypedStruct
typedstruct do
plugin TypedStructNimbleOptions
field :name, String.t(), enforce: true, doc: "The name."
field :age, non_neg_integer(), doc: "The age."
field :happy?, boolean(), default: true
field :attrs, %{optional(atom()) => String.t()}
end
end
# `new/1` returns {:ok, value} | {:error, reason}
iex> Person.new(name: "George", age: 31)
{:ok, %Person{name: "George", age: 31, happy?: true, attrs: nil}}
# `new!/1` raises on error
iex> Person.new!(name: "George", age: 31, attrs: %{phone: 123})
** (NimbleOptions.ValidationError) invalid map in :attrs option: invalid value for map key :phone: expected string, got: 123
# `field_docs/0-1` returns the fields' documentation
iex> Person.field_docs()
"""
* `:name` (`t:String.t/0`) - Required. The name.\n
* `:age` (`t:non_neg_integer/0`) - The age.\n
* `:happy?` (`t:boolean/0`) - The default value is `true`.\n
* `:attrs` (map of `t:atom/0` keys and `t:String.t/0` values)\n
"""
Generated @moduledoc
s
The available options will append themselves to the @moduledoc
.
You can modify this behavior with append_moduledoc_header
option.
For example, the ExDoc page for the Person
module above would start with:
# Person
A struct representing a person.
## Fields
- `:phone` (`String.t/0`)
- `:happy?` (`boolean/0`) - The default value is `true`.
- `:age` (`non_neg_integer/0`) - The age.
- `:name` (`String.t/0`) - Required. The name.
Installation
The package can be installed by adding typed_struct_nimble_options
to your list of dependencies in mix.exs
:
# mix.exs
def deps do
[
{:typed_struct_nimble_options, "~> 0.1.0"}
]
end
Global settings
Settings can be specified in application config to apply to all of application's TypedStructNimbleOptions structs by default.
For example, to disable defining the documentation function by default, you can add the following setting:
# config/config.exs
config :my_otp_app, TypedStructNimbleOptions, docs: nil
Individually, the same settings can be set directly on plugin TypedStructNimbleOptions
and they will override the default settings if given.
For example, the below docs setting will override the config.exs
setting above:
typedstruct do
plugin TypedStructNimbleOptions, docs: :field_docs
end
Supported settings
:ctor
- Name of the non-bang constructor function.nil
disables constructor generation. The default value is:new
.:ctor!
- Name of the bang constructor function.nil
disables bang constructor generation. The default value is:new!
.:docs
- Name of the functions that return the NimbleOptions docs for the struct.nil
disables docs functions generation. The default value is:field_docs
.:otp_app
(atom/0) - Name of the OTP application using TypedStructNimbleOptions. This is used to fetch global options for the TypedStructNimbleOptions. Defaults toApplication.get_application(__CALLER__.module)
.:append_moduledoc_header
- Whether to append the generated docs to the@moduledoc
. The docs are appended with the header specified in this option. Docs are appended only if the header is not nil. The default value is"\n## Fields"
.:warn_unknown_types?
(boolean/0) - Whether to warn when an unknown type is encountered. The default value istrue
.
Field options
All of the following options are passed to NimbleOptions as-is, with the exception of validation_type
which is renamed to type
as it's being passed down.
See https://hexdocs.pm/nimble_options/NimbleOptions.html#module-schema-options for more information on the supported options.
Automatically derived
These options are derived from information given to TypedStruct. Users can override the settings by specifying them manually.
:required
- set to true if the field isenforced
and has nodefault
.:type_spec
- set to the field's type:validation_type
(type
) - many basic types supported by NimbleOptions are automatically derived from the field's type, for exampleatom()
type will settype: :atom
andString.t()
will settype: :string
. Some more complex types like maps or lists are also supported.If TypedStructNimbleOptions encounters a type it cannot derive, it will fall back to
:any
and generate a compilation warning. The warnings can be disabled on a struct or global level withwarn_unknown_types?: false
.
Passed as-is if they're given
:default
:keys
:deprecated
:doc
:type_doc
subsection
Nested structs
Nested structs in particular are not automagically validated.
You should either specify validation_type: {:struct, MyNestedStruct}
manually for the struct field, or run a custom validator with validation_type: {:custom, M, :f, []}
.
If the nested struct also uses TypedStructNimbleOptions, you can use a special validation_type: {:nested_struct, MyNestedStruct, :new}
that will internally run MyNestedStruct.new/1
with proper error handling:
defmodule Profile do
use TypedStruct
typedstruct enforce: true do
plugin TypedStructNimbleOptions
field :name, String.t()
end
end
defmodule User do
use TypedStruct
typedstruct enforce: true do
plugin TypedStructNimbleOptions
field :id, pos_integer()
field :profile, Profile.t(), validation_type: {:nested_struct, Profile, :new}
end
end
iex> User.new!(id: 1, profile: [name: "UserName"])
%User{id: 1, profile: %Profile{name: "UserName"}}
iex> User.new!(id: 2, profile: %Profile{name: "UserName"})
%User{id: 2, profile: %Profile{name: "UserName"}}
iex> User.new(id: 3, profile: [name: 404])
{:error,
%NimbleOptions.ValidationError{
key: :profile, value: [name: 404], keys_path: [],
message: "invalid value for :profile option: invalid value for :name option: expected string, got: 404"}}
Limitations
Compilation-time schema
The NimbleOptions schema is prepared at the compilation time, so it cannot contain runtime elements such as fn
definitions.
Automatically derived types
Non-parametrized types can be given with or without parentheses, e.g. map()
is equivalent to map
.
Elixir type | NimbleOption type |
---|---|
map() , %{} | {:map, :any, :any} |
%{optional(key) => value} , %{key => value} | {:map, derive(key), derive(value)} |
list() | {:list, :any} |
list(subtype) , [subtype] | {:list, derive(subtype)} |
{typea, ...} | {:tuple, [derive(typea), ...]} |
atom() | :atom |
String.t() | :string |
boolean() | :boolean |
integer() | :integer |
non_neg_integer() | :non_neg_integer |
pos_integer() | :pos_integer |
float() | :float |
timeout() | :timeout |
pid() | :pid |
reference() | :reference |
nil | nil |
mfa() | :mfa |
any() , term() | :any |