defmodule BACnet.Macro do @module_doc """ This module also provides typed structs, that will generate a struct with type `t`, based on the given arguments. Such as this module ```elixir defmodule Report do @type report_type :: :refactoring | :warning | :consistency @type t :: %__MODULE__{ type: report_type, description: String.t, message: String.t | nil } @enforce_keys [:type, :description] defstruct [:description, :message, type: :refactoring] end ``` can be rewritten to ```elixir defmodule Report do @type report_type :: :refactoring | :warning | :consistency # required? is by default true typedstruct do field :type, report_type, default: :refactoring field :description, String.t field :message, String.t, required: false end end ``` """ # Do not generate module docs meanwhile (and do not generate a warning) @moduledoc !@module_doc # The typedstruct macro has been copied and slightly adjusted # Original author: https://dorgan.ar/posts/2021/04/the_elixir_ast_typedstruct/ @doc """ Generate a typed struct. It defines `defstruct`, `@enforce_keys` and `@type t`. Example: ```elixir defmodule Report do @type report_type :: :refactoring | :warning | :consistency # required? is by default true typedstruct do field :type, report_type, default: :refactoring field :description, String.t field :message, String.t, required: false end end ``` """ defmacro typedstruct(do_block) defmacro typedstruct(do: ast) do fields_ast = case ast do {:__block__, _meta, fields} -> fields field -> [field] end fields_data = Enum.map(fields_ast, &get_field_data/1) enforced_fields = for field <- fields_data, field.required do field.name end typespecs = Enum.map(fields_data, fn %{name: name, typespec: typespec, required: true} -> {name, typespec} %{name: name, typespec: typespec} -> { name, {:|, [], [typespec, nil]} } end) fields = for %{name: name, default: default} <- fields_data do {name, default} end quote generated: true, location: :keep do @type t :: %__MODULE__{unquote_splicing(typespecs)} @enforce_keys unquote(enforced_fields) defstruct unquote(fields) end end @doc """ Same as `typedstruct/1`, but `@opaque` gets generated. """ defmacro opaquedstruct(do_block) defmacro opaquedstruct(do: ast) do fields_ast = case ast do {:__block__, _meta, fields} -> fields field -> [field] end fields_data = Enum.map(fields_ast, &get_field_data/1) enforced_fields = for field <- fields_data, field.required do field.name end typespecs = Enum.map(fields_data, fn %{name: name, typespec: typespec, required: true} -> {name, typespec} %{name: name, typespec: typespec} -> { name, {:|, [], [typespec, nil]} } end) fields = for %{name: name, default: default} <- fields_data do {name, default} end quote generated: true, location: :keep do @opaque t :: %__MODULE__{unquote_splicing(typespecs)} @enforce_keys unquote(enforced_fields) defstruct unquote(fields) end end defp get_field_data({:field, _meta, [name, typespec]}) do get_field_data({:field, [], [name, typespec, []]}) end defp get_field_data({:field, _meta, [name, typespec, opts]}) do default = Keyword.get(opts, :default) required = Keyword.get(opts, :required, false) %{ name: name, typespec: typespec, default: default, required: required } end end