/
doc_index.ex
137 lines (112 loc) · 3.78 KB
/
doc_index.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
defmodule Ash.DocIndex do
@moduledoc """
A module for configuring how a library is rendered in ash_hq.
There is a small template syntax available in any documentation.
The template syntax will link to the user's currently selected
version for the relevant library.
All templates should be wrapped in double curly braces, e.g `{{}}`.
## Links to other documentation
`{{link:library:item_type:name}}`
The only item_type currently supported is `guide`, so the name would be `category/name`
For example:
`{{link:ash:guide:Attributes}}` -> `<a href="/docs/guides/ash/topics/attributes.md">Attributes</a>`
## Mix dependencies
`{{mix_dep:library}}`
For example:
`{{mix_dep:ash}}` -> `{:ash, "~> 1.5"}`
"""
@type extension :: %{
optional(:module) => module,
optional(:target) => String.t(),
optional(:default_for_target?) => boolean,
:name => String.t(),
:type => String.t()
}
@type guide :: %{
name: String.t(),
text: String.t(),
category: String.t() | nil,
route: String.t() | nil
}
@callback extensions() :: list(extension())
@callback for_library() :: String.t()
@callback guides() :: list(guide())
@callback code_modules() :: [{String.t(), list(module())}]
@callback default_guide() :: String.t()
defmacro __using__(opts) do
quote bind_quoted: [otp_app: opts[:otp_app], guides_from: opts[:guides_from]] do
@behaviour Ash.DocIndex
if guides_from do
@impl Ash.DocIndex
# sobelow_skip ["Traversal.FileModule"]
def guides do
unquote(otp_app)
|> :code.priv_dir()
|> Path.join(unquote(guides_from))
|> Path.wildcard()
|> Enum.map(fn path ->
path
|> Path.split()
|> Enum.reverse()
|> Enum.take(2)
|> Enum.reverse()
|> case do
[category, file] ->
%{
name: Ash.DocIndex.to_name(Path.rootname(file)),
category: Ash.DocIndex.to_name(category),
text: File.read!(path),
route: "#{Ash.DocIndex.to_path(category)}/#{Ash.DocIndex.to_path(file)}"
}
end
end)
end
defoverridable guides: 0
end
end
end
def find_undocumented_items(doc_index) do
Enum.each(doc_index.extensions(), fn extension ->
Enum.each(
extension.module.sections(),
&find_undocumented_in_section(&1, [inspect(extension.module)])
)
end)
end
defp find_undocumented_in_section(section, path) do
find_undocumented_in_schema(section.schema(), [section.name() | path])
Enum.each(section.sections(), &find_undocumented_in_section(&1, [section.name() | path]))
Enum.each(section.entities(), &find_undocumented_in_entity(&1, [section.name() | path]))
end
defp find_undocumented_in_entity(entity, path) do
find_undocumented_in_schema(entity.schema(), [entity.name() | path])
Enum.each(entity.entities(), fn {_key, entities} ->
Enum.each(entities, &find_undocumented_in_entity(&1, [entity.name() | path]))
end)
end
defp find_undocumented_in_schema(schema, path) do
Enum.each(schema, fn {key, opts} ->
if !opts[:link] do
raise "Undocumented item #{Enum.reverse(path) |> Enum.join(".")}.#{key}"
end
end)
end
# sobelow_skip ["Traversal.FileModule"]
def read!(app, path) do
app
|> :code.priv_dir()
|> Path.join(path)
|> File.read!()
end
def to_name(string) do
string
|> String.split(~r/[-_]/, trim: true)
|> Enum.map_join(" ", &String.capitalize/1)
end
def to_path(string) do
string
|> String.split(~r/\s/, trim: true)
|> Enum.join("-")
|> String.downcase()
end
end