Skip to content

Commit df4f8c1

Browse files
authoredJul 7, 2024
docs: add doc_fields guide (#11)
1 parent df5b17e commit df4f8c1

File tree

4 files changed

+220
-8
lines changed

4 files changed

+220
-8
lines changed
 

‎guides/plugins/doc_fields.md

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Add fields docs to the `@typedoc`
2+
3+
## Implement
4+
```elixir
5+
defmodule Guides.Plugins.DocFields do
6+
@moduledoc """
7+
The `DocFields` plugin generates documentation for fields and parameters.
8+
Simply add the `:doc` option to the `field` and `parameter` macros to document them.
9+
10+
## Example
11+
12+
use TypedStructor
13+
14+
typed_structor do
15+
@typedoc \"""
16+
This is a user struct.
17+
\"""
18+
plugin Guides.Plugins.DocFields
19+
20+
parameter :age, doc: "The age parameter."
21+
22+
field :name, String.t(), doc: "The name of the user."
23+
field :age, age, doc: "The age of the user."
24+
end
25+
26+
This will generate the following documentation for you:
27+
28+
@typedoc \"""
29+
This is a user struct.
30+
31+
32+
## Parameters
33+
34+
| Name | Description |
35+
|------|-------------|
36+
|`:age` | The age parameter.|
37+
38+
39+
## Fields
40+
41+
| Name | Type | Description |
42+
|------|------|-------------|
43+
|`:name` | `String.t() \| nil` | The name of the user.|
44+
|`:age` | `age \| nil` | The age of the user.|
45+
\"""
46+
47+
@type t(age) :: %User{age: age | nil, name: String.t() | nil}
48+
"""
49+
50+
use TypedStructor.Plugin
51+
52+
@impl TypedStructor.Plugin
53+
defmacro before_definition(definition, _opts) do
54+
quote do
55+
@typedoc unquote(__MODULE__).__generate_doc__(unquote(definition), @typedoc)
56+
57+
unquote(definition)
58+
end
59+
end
60+
61+
def __generate_doc__(_definition, false), do: nil
62+
63+
def __generate_doc__(definition, typedoc) do
64+
parameters =
65+
Enum.map(definition.parameters, fn parameter ->
66+
name = Keyword.fetch!(parameter, :name)
67+
doc = Keyword.get(parameter, :doc, "*not documented*")
68+
69+
["`#{inspect(name)}`", doc]
70+
end)
71+
72+
parameters_docs =
73+
if length(parameters) > 0 do
74+
"""
75+
## Parameters
76+
77+
| Name | Description |
78+
|------|-------------|
79+
#{join_rows(parameters)}
80+
"""
81+
end
82+
83+
fields =
84+
Enum.map(definition.fields, fn field ->
85+
name = Keyword.fetch!(field, :name)
86+
87+
type = Keyword.fetch!(field, :type)
88+
89+
type =
90+
if Keyword.get(field, :enforce, false) or Keyword.has_key?(field, :default) do
91+
Macro.to_string(type)
92+
else
93+
# escape `|`
94+
"#{Macro.to_string(type)} \\| nil"
95+
end
96+
97+
doc = Keyword.get(field, :doc, "*not documented*")
98+
99+
["`#{inspect(name)}`", "`#{type}`", doc]
100+
end)
101+
102+
fields_docs =
103+
if length(fields) > 0 do
104+
"""
105+
## Fields
106+
107+
| Name | Type | Description |
108+
|------|------|-------------|
109+
#{join_rows(fields)}
110+
"""
111+
end
112+
113+
[parameters_docs, fields_docs]
114+
|> Enum.reject(&is_nil/1)
115+
|> case do
116+
[] ->
117+
typedoc
118+
119+
docs ->
120+
"""
121+
#{typedoc}
122+
123+
#{Enum.join(docs, "\n\n")}
124+
"""
125+
end
126+
end
127+
128+
defp join_rows(rows) do
129+
Enum.map_join(rows, "\n", fn row -> "|" <> Enum.join(row, " | ") <> "|" end)
130+
end
131+
end
132+
```
133+
134+
## Usage
135+
```elixir
136+
defmodule User do
137+
@moduledoc false
138+
139+
use TypedStructor
140+
141+
typed_structor do
142+
@typedoc """
143+
This is a user struct.
144+
"""
145+
plugin Guides.Plugins.DocFields
146+
147+
parameter :age, doc: "The age parameter."
148+
149+
field :name, String.t(), doc: "The name of the user."
150+
field :age, age, doc: "The age of the user."
151+
end
152+
end
153+
```
154+
155+
```elixir
156+
iex> t User.t
157+
@type t(age) :: %User{age: age | nil, name: String.t() | nil}
158+
159+
This is a user struct.
160+
161+
## Parameters
162+
163+
Name | Description
164+
:age | The age parameter.
165+
166+
## Fields
167+
168+
Name | Type | Description
169+
:name | String.t() | nil | The name of the user.
170+
:age | age | nil | The age of the user.
171+
```

‎mix.exs

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ defmodule TypedStructor.MixProject do
3636
"guides/plugins/registering_plugins_globally.md",
3737
"guides/plugins/accessible.md",
3838
"guides/plugins/reflection.md",
39+
"guides/plugins/doc_fields.md",
3940
"guides/plugins/type_only_on_ecto_schema.md",
4041
"guides/plugins/primary_key_and_timestamps.md",
4142
"guides/plugins/derive_jason.md",
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
defmodule Guides.Plugins.DocFieldsTest do
2+
use TypedStructor.TestCase
3+
4+
@tag :tmp_dir
5+
test "works", ctx do
6+
doc =
7+
with_tmpmodule TestModule, ctx do
8+
unquote(
9+
"doc_fields.md"
10+
|> TypedStructor.GuideCase.extract_code()
11+
|> Code.string_to_quoted!()
12+
)
13+
after
14+
fetch_doc!(TestModule.User, {:type, :t, 1})
15+
end
16+
17+
expected = """
18+
This is a user struct.
19+
20+
21+
## Parameters
22+
23+
| Name | Description |
24+
|------|-------------|
25+
|`:age` | The age parameter.|
26+
27+
28+
## Fields
29+
30+
| Name | Type | Description |
31+
|------|------|-------------|
32+
|`:name` | `String.t() | nil` | The name of the user.|
33+
|`:age` | `age | nil` | The age of the user.|
34+
35+
"""
36+
37+
assert expected === doc
38+
end
39+
end

‎test/support/guide_case.ex

+9-8
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,33 @@ defmodule TypedStructor.GuideCase do
66
using opts do
77
guide = Keyword.fetch!(opts, :guide)
88

9-
file = Path.expand([__DIR__, "../../../", "guides/plugins/", guide])
10-
11-
ast = Code.string_to_quoted!(extract_code(file))
9+
ast = Code.string_to_quoted!(extract_code(guide))
1210

1311
quote do
14-
Code.compiler_options(debug_info: true)
12+
Code.compiler_options(debug_info: true, docs: true)
1513

1614
{:module, _module_name, bytecode, _submodule} = unquote(ast)
1715

1816
ExUnit.Case.register_module_attribute(__MODULE__, :bytecode)
1917
@bytecode bytecode
2018

21-
import unquote(__MODULE__), only: [types: 1]
19+
import unquote(__MODULE__)
2220
end
2321
end
2422

23+
@spec types(binary()) :: binary()
2524
def types(bytecode) when is_binary(bytecode) do
2625
bytecode
2726
|> TypedStructor.TestCase.fetch_types!()
2827
|> TypedStructor.TestCase.format_types()
2928
|> Kernel.<>("\n")
3029
end
3130

32-
defp extract_code(file) do
33-
content =
34-
File.read!(file)
31+
@spec extract_code(String.t()) :: String.t()
32+
def extract_code(filename) do
33+
file = Path.expand([__DIR__, "../../../", "guides/plugins/", filename])
34+
35+
content = File.read!(file)
3536

3637
content
3738
|> String.split("\n")

0 commit comments

Comments
 (0)