Skip to content

Commit 535ecfb

Browse files
committed
Refine API doc rendering and accordion styling
1 parent e356d4f commit 535ecfb

File tree

4 files changed

+52
-43
lines changed

4 files changed

+52
-43
lines changed

internal/apidoc/spec.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,10 @@ type Schema struct {
5656
Ref string `json:"$ref,omitempty"`
5757
Type string `json:"type,omitempty"`
5858
Format string `json:"format,omitempty"`
59-
Pattern string `json:"pattern,omitempty"`
60-
Enum []string `json:"enum,omitempty"`
6159
Default any `json:"default,omitempty"`
6260
Minimum *int `json:"minimum,omitempty"`
6361
Maximum *int `json:"maximum,omitempty"`
6462
MaxLength *int `json:"maxLength,omitempty"`
65-
MinItems *int `json:"minItems,omitempty"`
66-
MaxItems *int `json:"maxItems,omitempty"`
6763
Nullable bool `json:"nullable,omitempty"`
6864
Required []string `json:"required,omitempty"`
6965
Properties map[string]*Schema `json:"properties,omitempty"`

internal/ui/apidoc/templates.templ

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,23 @@ templ APIDocContent(spec *apidoc.OpenAPISpec) {
1616
The <a href="/getting-started">pkgstats CLI</a> uses this API to search and compare packages from the terminal.
1717
Please be considerate with request rates to keep the service available for everyone.
1818
</p>
19-
<div class="accordion accordion-flush">
20-
for _, path := range sortedPaths(spec.Paths) {
21-
@renderPath(path, spec.Paths[path], spec.Components)
22-
}
23-
</div>
19+
for _, tag := range spec.Tags {
20+
<h2 class="h5 mt-4 mb-3 text-capitalize">{ tag.Name }</h2>
21+
<div class="accordion accordion-flush mb-3">
22+
for _, path := range sortedPaths(spec.Paths) {
23+
if pathHasTag(spec.Paths[path], tag.Name) {
24+
@renderPath(path, spec.Paths[path], spec.Components)
25+
}
26+
}
27+
</div>
28+
}
29+
}
30+
31+
func pathHasTag(item apidoc.PathItem, tag string) bool {
32+
if item.Get != nil {
33+
return slices.Contains(item.Get.Tags, tag)
34+
}
35+
return false
2436
}
2537

2638
func sortedPaths(paths map[string]apidoc.PathItem) []string {
@@ -73,7 +85,7 @@ func schemaTypeString(s *apidoc.Schema) string {
7385
t += " (" + s.Format + ")"
7486
}
7587
if s.Type == "array" && s.Items != nil {
76-
t = "array of " + schemaTypeString(s.Items)
88+
t = schemaTypeString(s.Items) + "[]"
7789
}
7890
if s.Nullable {
7991
t += ", nullable"
@@ -92,18 +104,6 @@ func schemaConstraints(s *apidoc.Schema) string {
92104
if s.MaxLength != nil {
93105
parts = append(parts, fmt.Sprintf("maxLength: %d", *s.MaxLength))
94106
}
95-
if s.MinItems != nil {
96-
parts = append(parts, fmt.Sprintf("minItems: %d", *s.MinItems))
97-
}
98-
if s.MaxItems != nil {
99-
parts = append(parts, fmt.Sprintf("maxItems: %d", *s.MaxItems))
100-
}
101-
if s.Pattern != "" {
102-
parts = append(parts, "pattern: "+s.Pattern)
103-
}
104-
if len(s.Enum) > 0 {
105-
parts = append(parts, "enum: "+strings.Join(s.Enum, ", "))
106-
}
107107
if s.Default != nil {
108108
parts = append(parts, fmt.Sprintf("default: %v", s.Default))
109109
}
@@ -151,6 +151,9 @@ templ renderOperation(path string, op *apidoc.Operation, components apidoc.SpecC
151151
}
152152
<h3 class="h6 mt-3">Responses</h3>
153153
@renderResponses(op.Responses, components)
154+
<div class="text-end">
155+
@requiredLegend()
156+
</div>
154157
</div>
155158
</div>
156159
</div>
@@ -164,14 +167,18 @@ templ renderParameters(params []apidoc.Parameter) {
164167
<th>Name</th>
165168
<th>In</th>
166169
<th>Type</th>
167-
<th>Required</th>
168170
<th>Description</th>
169171
</tr>
170172
</thead>
171173
<tbody>
172174
for _, p := range params {
173175
<tr>
174-
<td><code>{ p.Name }</code></td>
176+
<td>
177+
<code>{ p.Name }</code>
178+
if p.Required {
179+
@requiredMarker()
180+
}
181+
</td>
175182
<td>{ p.In }</td>
176183
<td>
177184
if p.Schema != nil {
@@ -182,11 +189,6 @@ templ renderParameters(params []apidoc.Parameter) {
182189
}
183190
}
184191
</td>
185-
<td>
186-
if p.Required {
187-
yes
188-
}
189-
</td>
190192
<td>{ p.Description }</td>
191193
</tr>
192194
}
@@ -202,23 +204,26 @@ templ renderResponses(responses map[string]apidoc.Response, components apidoc.Sp
202204
<tr>
203205
<th>Status</th>
204206
<th>Description</th>
207+
<th>Content Type</th>
205208
</tr>
206209
</thead>
207210
<tbody>
208211
for _, code := range sortedResponseCodes(responses) {
209212
<tr>
210213
<td><code>{ code }</code></td>
211214
<td>{ responses[code].Description }</td>
215+
<td>
216+
for contentType := range responses[code].Content {
217+
<code>{ contentType }</code>
218+
}
219+
</td>
212220
</tr>
213221
}
214222
</tbody>
215223
</table>
216224
</div>
217225
for _, code := range sortedResponseCodes(responses) {
218-
for contentType, media := range responses[code].Content {
219-
<p class="text-body-secondary mb-2">
220-
<code>{ code }</code> response (<code>{ contentType }</code>):
221-
</p>
226+
for _, media := range responses[code].Content {
222227
@renderSchemaDetail(media.Schema, components)
223228
}
224229
}
@@ -244,28 +249,29 @@ templ renderSchemaProperties(s *apidoc.Schema, components apidoc.SpecComponents)
244249
<tr>
245250
<th>Field</th>
246251
<th>Type</th>
247-
<th>Required</th>
248-
<th>Constraints</th>
249252
</tr>
250253
</thead>
251254
<tbody>
252255
for _, name := range sortedProperties(s.Properties) {
253256
<tr>
254-
<td><code>{ name }</code></td>
255-
<td><code>{ schemaTypeString(s.Properties[name]) }</code></td>
256257
<td>
258+
<code>{ name }</code>
257259
if slices.Contains(s.Required, name) {
258-
yes
259-
}
260-
</td>
261-
<td>
262-
if constraints := schemaConstraints(s.Properties[name]); constraints != "" {
263-
<small>{ constraints }</small>
260+
@requiredMarker()
264261
}
265262
</td>
263+
<td><code>{ schemaTypeString(s.Properties[name]) }</code></td>
266264
</tr>
267265
}
268266
</tbody>
269267
</table>
270268
</div>
271269
}
270+
271+
templ requiredMarker() {
272+
<span class="text-danger" title="required">*</span>
273+
}
274+
275+
templ requiredLegend() {
276+
<small class="text-body-secondary"><span class="text-danger">*</span> required</small>
277+
}

src/_bootstrap.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ $min-contrast-ratio: 3;
1212
$link-decoration: none;
1313
$link-hover-decoration: underline;
1414
$color-mode-type: media-query;
15+
$accordion-button-active-bg: var(--bs-secondary-bg);
16+
$accordion-button-active-color: var(--bs-body-color);
17+
$accordion-button-focus-box-shadow: none;

src/_overrides.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ pre:has(> code) {
1717
padding: map.get($gutters, 2);
1818
}
1919

20+
.accordion-button:hover {
21+
background-color: rgba(var(--bs-emphasis-color-rgb), 0.05);
22+
}
23+
2024
.code-link {
2125
color: inherit;
2226

0 commit comments

Comments
 (0)