diff --git a/README.md b/README.md index f2d26f0..c53f755 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ defmodule Person do field :name, String.t(), enforce: true field :age, non_neg_integer() field :happy?, boolean(), default: true - field :phone, String.t() + field :phone, String.t(), doc: "strictly the home number" end end ``` @@ -126,6 +126,9 @@ defmodule MyStruct do # You can enforce a field. field :enforced_field, integer(), enforce: true + + # Define each field with the field macro. + field :a_field_thats_purpose_is_not_obvious, String.t(), doc: "some explanation about this field" end end ``` @@ -196,6 +199,38 @@ typedstruct do end ``` +You can also add documentation to individual fields: + +```elixir +typedstruct do + @typedoc "A typed struct" + + field :a_string, String.t(), doc: "this is just a series of letters" + field :an_int, integer(), doc: "some explanation" +end +``` + +As an added benefit, the above will generate a typedoc as below: + +```elixir +@typedoc """ +A typed struct + +- `a_string`: this is just a series of letters +- `an_int`: some explanation +""" +``` + +This works without the `@typedoc ...` declaration, +with the following result: + +```elixir +@typedoc """ +- `a_string`: this is just a series of letters +- `an_int`: some explanation +""" +``` + You can also document submodules this way: ```elixir diff --git a/lib/typed_struct.ex b/lib/typed_struct.ex index 371b64a..58b94ed 100644 --- a/lib/typed_struct.ex +++ b/lib/typed_struct.ex @@ -10,7 +10,8 @@ defmodule TypedStruct do :ts_plugin_fields, :ts_fields, :ts_types, - :ts_enforce_keys + :ts_enforce_keys, + :ts_comments ] @attrs_to_delete [:ts_enforce? | @accumulating_attrs] @@ -110,6 +111,11 @@ defmodule TypedStruct do @enforce_keys @ts_enforce_keys defstruct @ts_fields + td = + if Module.has_attribute?(__MODULE__, :typedoc), do: @typedoc, else: nil + + TypedStruct.__typedoc_generator__(td, @ts_comments) + TypedStruct.__type__(@ts_types, unquote(opts)) end end @@ -127,6 +133,28 @@ defmodule TypedStruct do end end + @doc false + defmacro __typedoc_generator__(typedoc, comments) do + quote bind_quoted: [typedoc: typedoc, comments: comments] do + # If there are no comments we just leave it as it is + if comments != [] do + comments = + comments + |> Enum.reverse() + |> Enum.map(fn {x, c} -> "- `#{x}`: #{c}" end) + + comments = + if typedoc == nil, do: comments, else: [typedoc, "" | comments] + + Module.delete_attribute(__MODULE__, :typedoc) + + @typedoc """ + #{Enum.join(comments, "\n")} + """ + end + end + end + @doc """ Registers a plugin for the currently defined struct. @@ -168,6 +196,8 @@ defmodule TypedStruct do * `default` - sets the default value for the field * `enforce` - if set to true, enforces the field and makes its type non-nullable + * `doc` - short description for the field, + that will be appended to the typedoc as well """ defmacro field(name, type, opts \\ []) do quote bind_quoted: [name: name, type: Macro.escape(type), opts: opts] do @@ -192,6 +222,11 @@ defmodule TypedStruct do nullable? = !has_default? && !enforce? + comment = opts[:doc] + + if comment != nil, + do: Module.put_attribute(mod, :ts_comments, {name, comment}) + Module.put_attribute(mod, :ts_fields, {name, opts[:default]}) Module.put_attribute(mod, :ts_plugin_fields, {name, type, opts, env}) Module.put_attribute(mod, :ts_types, {name, type_for(type, nullable?)})