Skip to content

Commit 4302428

Browse files
committed
Replace swagger-ui with server-rendered API docs
1 parent 08f3e7a commit 4302428

File tree

14 files changed

+394
-1948
lines changed

14 files changed

+394
-1948
lines changed

internal/apidoc/handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type Handler struct {
1010
}
1111

1212
func NewHandler(includeInternal bool) *Handler {
13-
data, err := json.MarshalIndent(buildSpec(includeInternal), "", " ")
13+
data, err := json.MarshalIndent(BuildSpec(includeInternal), "", " ")
1414
if err != nil {
1515
panic("apidoc: failed to marshal OpenAPI spec: " + err.Error())
1616
}

internal/apidoc/spec.go

Lines changed: 87 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,61 +5,61 @@ import (
55
"pkgstatsd/internal/web"
66
)
77

8-
type openAPISpec struct {
8+
type OpenAPISpec struct {
99
OpenAPI string `json:"openapi"`
10-
Info specInfo `json:"info"`
11-
Tags []specTag `json:"tags,omitempty"`
12-
Paths map[string]pathItem `json:"paths"`
13-
Components specComponents `json:"components"`
10+
Info SpecInfo `json:"info"`
11+
Tags []SpecTag `json:"tags,omitempty"`
12+
Paths map[string]PathItem `json:"paths"`
13+
Components SpecComponents `json:"components"`
1414
}
1515

16-
type specInfo struct {
16+
type SpecInfo struct {
1717
Title string `json:"title"`
1818
Version string `json:"version"`
1919
}
2020

21-
type specTag struct {
21+
type SpecTag struct {
2222
Name string `json:"name"`
2323
Description string `json:"description,omitempty"`
2424
}
2525

26-
type pathItem struct {
27-
Get *operation `json:"get,omitempty"`
28-
Post *operation `json:"post,omitempty"`
26+
type PathItem struct {
27+
Get *Operation `json:"get,omitempty"`
28+
Post *Operation `json:"post,omitempty"`
2929
}
3030

31-
type operation struct {
31+
type Operation struct {
3232
Tags []string `json:"tags,omitempty"`
3333
Summary string `json:"summary,omitempty"`
3434
OperationID string `json:"operationId,omitempty"`
35-
Parameters []parameter `json:"parameters,omitempty"`
36-
RequestBody *requestBody `json:"requestBody,omitempty"`
37-
Responses map[string]response `json:"responses"`
35+
Parameters []Parameter `json:"parameters,omitempty"`
36+
RequestBody *RequestBody `json:"requestBody,omitempty"`
37+
Responses map[string]Response `json:"responses"`
3838
}
3939

40-
type parameter struct {
40+
type Parameter struct {
4141
Name string `json:"name"`
4242
In string `json:"in"`
4343
Description string `json:"description,omitempty"`
4444
Required bool `json:"required,omitempty"`
45-
Schema *schema `json:"schema,omitempty"`
45+
Schema *Schema `json:"schema,omitempty"`
4646
}
4747

48-
type requestBody struct {
48+
type RequestBody struct {
4949
Required bool `json:"required,omitempty"`
50-
Content map[string]mediaType `json:"content"`
50+
Content map[string]MediaType `json:"content"`
5151
}
5252

53-
type mediaType struct {
54-
Schema *schema `json:"schema,omitempty"`
53+
type MediaType struct {
54+
Schema *Schema `json:"schema,omitempty"`
5555
}
5656

57-
type response struct {
57+
type Response struct {
5858
Description string `json:"description"`
59-
Content map[string]mediaType `json:"content,omitempty"`
59+
Content map[string]MediaType `json:"content,omitempty"`
6060
}
6161

62-
type schema struct {
62+
type Schema struct {
6363
Ref string `json:"$ref,omitempty"`
6464
Type string `json:"type,omitempty"`
6565
Format string `json:"format,omitempty"`
@@ -73,24 +73,24 @@ type schema struct {
7373
MaxItems *int `json:"maxItems,omitempty"`
7474
Nullable bool `json:"nullable,omitempty"`
7575
Required []string `json:"required,omitempty"`
76-
Properties map[string]*schema `json:"properties,omitempty"`
77-
Items *schema `json:"items,omitempty"`
76+
Properties map[string]*Schema `json:"properties,omitempty"`
77+
Items *Schema `json:"items,omitempty"`
7878
}
7979

80-
type specComponents struct {
81-
Schemas map[string]*schema `json:"schemas"`
80+
type SpecComponents struct {
81+
Schemas map[string]*Schema `json:"schemas"`
8282
}
8383

8484
type entitySpec struct {
85-
basePath string // e.g. "/api/packages"
86-
pathParam string // e.g. "name"
87-
pathParamDesc string // e.g. "Package name"
88-
tag string // e.g. "packages"
89-
itemSchemaName string // e.g. "PackagePopularity"
90-
listSchemaName string // e.g. "PackagePopularityList"
91-
identifierField string // JSON field for the identifier in the item (e.g. "name")
92-
collectionField string // JSON field for items in the list (e.g. "packagePopularities")
93-
internal bool // omit from the spec in production
85+
basePath string
86+
pathParam string
87+
pathParamDesc string
88+
tag string
89+
itemSchemaName string
90+
listSchemaName string
91+
identifierField string
92+
collectionField string
93+
internal bool
9494
}
9595

9696
var popularityEntities = []entitySpec{
@@ -162,43 +162,43 @@ var popularityEntities = []entitySpec{
162162
}
163163

164164
var (
165-
paramStartMonth = parameter{
165+
paramStartMonth = Parameter{
166166
Name: "startMonth",
167167
In: "query",
168168
Description: "Start month in Ym format (e.g. 202501). Defaults to 12 months ago.",
169-
Schema: &schema{Type: "integer"},
169+
Schema: &Schema{Type: "integer"},
170170
}
171-
paramEndMonth = parameter{
171+
paramEndMonth = Parameter{
172172
Name: "endMonth",
173173
In: "query",
174174
Description: "End month in Ym format (e.g. 202501). Defaults to last month.",
175-
Schema: &schema{Type: "integer"},
175+
Schema: &Schema{Type: "integer"},
176176
}
177-
paramLimit = parameter{
177+
paramLimit = Parameter{
178178
Name: "limit",
179179
In: "query",
180180
Description: "Maximum number of results to return.",
181-
Schema: &schema{Type: "integer", Default: web.DefaultLimit, Minimum: new(1), Maximum: new(web.MaxLimit)},
181+
Schema: &Schema{Type: "integer", Default: web.DefaultLimit, Minimum: new(1), Maximum: new(web.MaxLimit)},
182182
}
183-
paramOffset = parameter{
183+
paramOffset = Parameter{
184184
Name: "offset",
185185
In: "query",
186186
Description: "Number of results to skip.",
187-
Schema: &schema{Type: "integer", Default: 0, Minimum: new(0), Maximum: new(web.MaxOffset)},
187+
Schema: &Schema{Type: "integer", Default: 0, Minimum: new(0), Maximum: new(web.MaxOffset)},
188188
}
189-
paramQuery = parameter{
189+
paramQuery = Parameter{
190190
Name: "query",
191191
In: "query",
192192
Description: "Filter by name.",
193-
Schema: &schema{Type: "string", MaxLength: new(submit.MaxPackageLen)},
193+
Schema: &Schema{Type: "string", MaxLength: new(submit.MaxPackageLen)},
194194
}
195195
)
196196

197-
func popularityItemSchema(identifierField string) *schema {
198-
return &schema{
197+
func popularityItemSchema(identifierField string) *Schema {
198+
return &Schema{
199199
Type: "object",
200200
Required: []string{identifierField, "samples", "count", "popularity", "startMonth", "endMonth"},
201-
Properties: map[string]*schema{
201+
Properties: map[string]*Schema{
202202
identifierField: {Type: "string"},
203203
"samples": {Type: "integer"},
204204
"count": {Type: "integer"},
@@ -209,14 +209,14 @@ func popularityItemSchema(identifierField string) *schema {
209209
}
210210
}
211211

212-
func popularityListSchema(collectionField, itemSchemaRef string) *schema {
213-
return &schema{
212+
func popularityListSchema(collectionField, itemSchemaRef string) *Schema {
213+
return &Schema{
214214
Type: "object",
215215
Required: []string{collectionField, "total", "count", "limit", "offset"},
216-
Properties: map[string]*schema{
216+
Properties: map[string]*Schema{
217217
collectionField: {
218218
Type: "array",
219-
Items: &schema{Ref: itemSchemaRef},
219+
Items: &Schema{Ref: itemSchemaRef},
220220
},
221221
"total": {Type: "integer"},
222222
"count": {Type: "integer"},
@@ -227,126 +227,126 @@ func popularityListSchema(collectionField, itemSchemaRef string) *schema {
227227
}
228228
}
229229

230-
func jsonResponse(schemaName string) map[string]response {
231-
return map[string]response{
230+
func jsonResponse(schemaName string) map[string]Response {
231+
return map[string]Response{
232232
"200": {
233233
Description: "Success",
234-
Content: map[string]mediaType{
235-
"application/json": {Schema: &schema{Ref: "#/components/schemas/" + schemaName}},
234+
Content: map[string]MediaType{
235+
"application/json": {Schema: &Schema{Ref: "#/components/schemas/" + schemaName}},
236236
},
237237
},
238238
"400": {Description: "Invalid request"},
239239
"500": {Description: "Internal server error"},
240240
}
241241
}
242242

243-
func buildSpec(includeInternal bool) *openAPISpec {
244-
spec := &openAPISpec{
243+
func BuildSpec(includeInternal bool) *OpenAPISpec {
244+
spec := &OpenAPISpec{
245245
OpenAPI: "3.0.0",
246-
Info: specInfo{
246+
Info: SpecInfo{
247247
Title: "pkgstats API documentation",
248248
Version: "3.0.0",
249249
},
250-
Paths: make(map[string]pathItem),
251-
Components: specComponents{Schemas: make(map[string]*schema)},
250+
Paths: make(map[string]PathItem),
251+
Components: SpecComponents{Schemas: make(map[string]*Schema)},
252252
}
253253

254254
for _, e := range popularityEntities {
255255
if !includeInternal && e.internal {
256256
continue
257257
}
258-
spec.Tags = append(spec.Tags, specTag{Name: e.tag})
258+
spec.Tags = append(spec.Tags, SpecTag{Name: e.tag})
259259

260260
itemSchemaRef := "#/components/schemas/" + e.itemSchemaName
261261
spec.Components.Schemas[e.itemSchemaName] = popularityItemSchema(e.identifierField)
262262
spec.Components.Schemas[e.listSchemaName] = popularityListSchema(e.collectionField, itemSchemaRef)
263263

264-
pathParam := parameter{
264+
pathParam := Parameter{
265265
Name: e.pathParam,
266266
In: "path",
267267
Description: e.pathParamDesc,
268268
Required: true,
269-
Schema: &schema{Type: "string"},
269+
Schema: &Schema{Type: "string"},
270270
}
271271

272-
spec.Paths[e.basePath] = pathItem{
273-
Get: &operation{
272+
spec.Paths[e.basePath] = PathItem{
273+
Get: &Operation{
274274
Tags: []string{e.tag},
275275
Summary: "List " + e.tag,
276276
OperationID: "list_" + e.tag,
277-
Parameters: []parameter{paramStartMonth, paramEndMonth, paramLimit, paramOffset, paramQuery},
277+
Parameters: []Parameter{paramStartMonth, paramEndMonth, paramLimit, paramOffset, paramQuery},
278278
Responses: jsonResponse(e.listSchemaName),
279279
},
280280
}
281-
spec.Paths[e.basePath+"/{"+e.pathParam+"}"] = pathItem{
282-
Get: &operation{
281+
spec.Paths[e.basePath+"/{"+e.pathParam+"}"] = PathItem{
282+
Get: &Operation{
283283
Tags: []string{e.tag},
284284
Summary: "Get " + e.tag + " by " + e.pathParam,
285285
OperationID: "get_" + e.tag + "_by_" + e.pathParam,
286-
Parameters: []parameter{pathParam, paramStartMonth, paramEndMonth},
286+
Parameters: []Parameter{pathParam, paramStartMonth, paramEndMonth},
287287
Responses: jsonResponse(e.itemSchemaName),
288288
},
289289
}
290-
spec.Paths[e.basePath+"/{"+e.pathParam+"}/series"] = pathItem{
291-
Get: &operation{
290+
spec.Paths[e.basePath+"/{"+e.pathParam+"}/series"] = PathItem{
291+
Get: &Operation{
292292
Tags: []string{e.tag},
293293
Summary: "List " + e.tag + " series by " + e.pathParam,
294294
OperationID: "list_" + e.tag + "_series_by_" + e.pathParam,
295-
Parameters: []parameter{pathParam, paramStartMonth, paramEndMonth, paramLimit, paramOffset},
295+
Parameters: []Parameter{pathParam, paramStartMonth, paramEndMonth, paramLimit, paramOffset},
296296
Responses: jsonResponse(e.listSchemaName),
297297
},
298298
}
299299
}
300300

301301
if includeInternal {
302-
spec.Tags = append(spec.Tags, specTag{Name: "submit"})
303-
spec.Paths["/api/submit"] = pathItem{
304-
Post: &operation{
302+
spec.Tags = append(spec.Tags, SpecTag{Name: "submit"})
303+
spec.Paths["/api/submit"] = PathItem{
304+
Post: &Operation{
305305
Tags: []string{"submit"},
306306
Summary: "Submit package statistics",
307307
OperationID: "submit",
308-
RequestBody: &requestBody{
308+
RequestBody: &RequestBody{
309309
Required: true,
310-
Content: map[string]mediaType{
311-
"application/json": {Schema: &schema{Ref: "#/components/schemas/SubmitRequest"}},
310+
Content: map[string]MediaType{
311+
"application/json": {Schema: &Schema{Ref: "#/components/schemas/SubmitRequest"}},
312312
},
313313
},
314-
Responses: map[string]response{
314+
Responses: map[string]Response{
315315
"204": {Description: "Statistics accepted"},
316316
"400": {Description: "Invalid request"},
317317
"429": {Description: "Rate limit exceeded"},
318318
"500": {Description: "Internal server error"},
319319
},
320320
},
321321
}
322-
spec.Components.Schemas["SubmitRequest"] = &schema{
322+
spec.Components.Schemas["SubmitRequest"] = &Schema{
323323
Type: "object",
324324
Required: []string{"version", "system", "os", "pacman"},
325-
Properties: map[string]*schema{
325+
Properties: map[string]*Schema{
326326
"version": {Type: "string", Enum: []string{"3"}},
327327
"system": {
328328
Type: "object",
329329
Required: []string{"architecture"},
330-
Properties: map[string]*schema{
330+
Properties: map[string]*Schema{
331331
"architecture": {Type: "string"},
332332
},
333333
},
334334
"os": {
335335
Type: "object",
336336
Required: []string{"architecture"},
337-
Properties: map[string]*schema{
337+
Properties: map[string]*Schema{
338338
"architecture": {Type: "string"},
339339
"id": {Type: "string", Pattern: `^[0-9a-z._-]{1,50}$`},
340340
},
341341
},
342342
"pacman": {
343343
Type: "object",
344344
Required: []string{"packages"},
345-
Properties: map[string]*schema{
345+
Properties: map[string]*Schema{
346346
"mirror": {Type: "string"},
347347
"packages": {
348348
Type: "array",
349-
Items: &schema{Type: "string"},
349+
Items: &Schema{Type: "string"},
350350
MinItems: new(1),
351351
MaxItems: new(submit.MaxPackages),
352352
},

internal/ui/apidoc/handler.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@ package apidoc
33
import (
44
"net/http"
55

6+
"pkgstatsd/internal/apidoc"
67
"pkgstatsd/internal/ui/layout"
78
)
89

910
type Handler struct {
1011
manifest *layout.Manifest
12+
spec *apidoc.OpenAPISpec
1113
}
1214

13-
func NewHandler(manifest *layout.Manifest) *Handler {
14-
return &Handler{manifest: manifest}
15+
func NewHandler(manifest *layout.Manifest, spec *apidoc.OpenAPISpec) *Handler {
16+
return &Handler{manifest: manifest, spec: spec}
1517
}
1618

1719
func (h *Handler) HandleAPIDoc(w http.ResponseWriter, r *http.Request) {
1820
layout.Render(w, r,
1921
layout.Page{Title: "API documentation", Path: "/api/doc", Manifest: h.manifest, NoIndex: true},
20-
APIDocContent(),
22+
APIDocContent(h.spec),
2123
)
2224
}
2325

0 commit comments

Comments
 (0)