storj/private/apigen/docgen.go
Moby von Briesen 8acb1ee5bf private/apigen: Support basic doc generation
Add some code to generate a basic markdown file documenting a generated
API. Generate this document for the API in
satellite/console/consoleweb/consoleapi/gen.

The documentation is not completely correct, as it may include some
values in the request body that are not actually usable by the
requester. This can be fixed by making sure all types used within the
generated API are properly annotated with `json` tags.

Issue: https://github.com/storj/storj-private/issues/244

Change-Id: I57b259967fb0db8f548b6598a10c825da15ba723
2023-06-13 08:48:06 +00:00

158 lines
4.4 KiB
Go

// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package apigen
import (
"fmt"
"os"
"reflect"
"strings"
"time"
"github.com/zeebo/errs"
"storj.io/common/memory"
"storj.io/common/uuid"
)
// MustWriteDocs generates API documentation and writes it to the specified file path.
// If an error occurs, it panics.
func (api *API) MustWriteDocs(path string) {
docs := api.generateDocumentation()
err := os.WriteFile(path, []byte(docs), 0644)
if err != nil {
panic(errs.Wrap(err))
}
}
// generateDocumentation generates a string containing the API documentation.
func (api *API) generateDocumentation() string {
var doc strings.Builder
wf := func(format string, args ...any) { _, _ = fmt.Fprintf(&doc, format, args...) }
wf("# API Docs\n\n")
wf("**Description:** %s\n\n", api.Description)
wf("**Version:** `%s`\n\n", api.Version)
for _, group := range api.EndpointGroups {
for _, endpoint := range group.endpoints {
wf("## %s\n\n", endpoint.Name)
wf("%s\n\n", endpoint.Description)
wf("`%s /%s%s`\n\n", endpoint.Method, group.Prefix, endpoint.Path)
if len(endpoint.QueryParams) > 0 {
wf("**Query Params:**\n\n")
wf("| name | type | elaboration |\n")
wf("|---|---|---|\n")
for _, param := range endpoint.QueryParams {
typeStr, elaboration := getDocType(param.Type)
wf("| `%s` | `%s` | %s |\n", param.Name, typeStr, elaboration)
}
wf("\n")
}
if len(endpoint.PathParams) > 0 {
wf("**Path Params:**\n\n")
wf("| name | type | elaboration |\n")
wf("|---|---|---|\n")
for _, param := range endpoint.PathParams {
typeStr, elaboration := getDocType(param.Type)
wf("| `%s` | `%s` | %s |\n", param.Name, typeStr, elaboration)
}
wf("\n")
}
requestType := reflect.TypeOf(endpoint.Request)
if requestType != nil {
wf("**Request body:**\n\n")
wf("```json\n%s\n```\n\n", getTypeNameRecursively(requestType, 0))
}
responseType := reflect.TypeOf(endpoint.Response)
if responseType != nil {
wf("**Response body:**\n\n")
wf("```json\n%s\n```\n\n", getTypeNameRecursively(responseType, 0))
}
}
}
return doc.String()
}
// getDocType returns the "basic" type to use in JSON, as well as examples for specific types that may require elaboration.
func getDocType(t reflect.Type) (typeStr, elaboration string) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
switch t {
case reflect.TypeOf(uuid.UUID{}):
return "string", "UUID formatted as `00000000-0000-0000-0000-000000000000`"
case reflect.TypeOf(time.Time{}):
return "string", "Date timestamp formatted as `2006-01-02T15:00:00Z`"
case reflect.TypeOf(memory.Size(0)):
return "string", "Amount of memory formatted as `15 GB`"
default:
switch t.Kind() {
case reflect.String:
return "string", ""
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "number", ""
case reflect.Float32, reflect.Float64:
return "number", ""
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return "number", ""
case reflect.Bool:
return "boolean", ""
}
}
return "unknown", ""
}
// getTypeNameRecursively gets a "full type" to document a struct or slice, including proper indentation of child properties.
func getTypeNameRecursively(t reflect.Type, level int) string {
prefix := ""
for i := 0; i < level; i++ {
prefix += "\t"
}
switch t.Kind() {
case reflect.Slice:
elemType := t.Elem()
if elemType.Kind() == reflect.Uint8 { // treat []byte as string in docs
return prefix + "string"
}
return fmt.Sprintf("%s[\n%s\n%s]\n", prefix, getTypeNameRecursively(elemType, level+1), prefix)
case reflect.Struct:
// some struct types may actually be documented as elementary types; check first
typeName, elaboration := getDocType(t)
if typeName != "unknown" {
toReturn := typeName
if len(elaboration) > 0 {
toReturn += " (" + elaboration + ")"
}
return toReturn
}
var fields []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
fields = append(fields, prefix+"\t"+jsonTag+": "+getTypeNameRecursively(field.Type, level+1))
}
}
return fmt.Sprintf("%s{\n%s\n%s}\n", prefix, strings.Join(fields, "\n"), prefix)
default:
typeName, elaboration := getDocType(t)
toReturn := typeName
if len(elaboration) > 0 {
toReturn += " (" + elaboration + ")"
}
return toReturn
}
}