2023-05-17 22:47:58 +01:00
|
|
|
// Copyright (C) 2022 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package apigen
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"reflect"
|
2023-08-24 16:12:27 +01:00
|
|
|
"regexp"
|
2023-05-17 22:47:58 +01:00
|
|
|
"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)
|
|
|
|
|
2023-08-24 16:12:27 +01:00
|
|
|
wf("<h2 id='list-of-endpoints'>List of Endpoints</h2>\n\n")
|
2023-08-23 18:21:54 +01:00
|
|
|
getEndpointLink := func(group, endpoint string) string {
|
2023-08-24 16:12:27 +01:00
|
|
|
fullName := group + "-" + endpoint
|
|
|
|
fullName = strings.ReplaceAll(fullName, " ", "-")
|
|
|
|
var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
|
|
|
|
fullName = nonAlphanumericRegex.ReplaceAllString(fullName, "")
|
|
|
|
return strings.ToLower(fullName)
|
2023-08-23 18:21:54 +01:00
|
|
|
}
|
|
|
|
for _, group := range api.EndpointGroups {
|
|
|
|
wf("* %s\n", group.Name)
|
|
|
|
for _, endpoint := range group.endpoints {
|
|
|
|
wf(" * [%s](#%s)\n", endpoint.Name, getEndpointLink(group.Name, endpoint.Name))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
wf("\n")
|
|
|
|
|
2023-05-17 22:47:58 +01:00
|
|
|
for _, group := range api.EndpointGroups {
|
|
|
|
for _, endpoint := range group.endpoints {
|
2023-08-24 16:12:27 +01:00
|
|
|
wf("<h3 id='%s'>%s (<a href='#list-of-endpoints'>go to full list</a>)</h3>\n\n", getEndpointLink(group.Name, endpoint.Name), endpoint.Name)
|
2023-05-17 22:47:58 +01:00
|
|
|
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")
|
2023-08-17 13:26:13 +01:00
|
|
|
wf("```typescript\n%s\n```\n\n", getTypeNameRecursively(requestType, 0))
|
2023-05-17 22:47:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
responseType := reflect.TypeOf(endpoint.Response)
|
|
|
|
if responseType != nil {
|
|
|
|
wf("**Response body:**\n\n")
|
2023-08-17 13:26:13 +01:00
|
|
|
wf("```typescript\n%s\n```\n\n", getTypeNameRecursively(responseType, 0))
|
2023-05-17 22:47:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-08-17 13:26:13 +01:00
|
|
|
toReturn += " // " + elaboration
|
2023-05-17 22:47:58 +01:00
|
|
|
}
|
|
|
|
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 {
|
2023-08-17 13:26:13 +01:00
|
|
|
toReturn += " // " + elaboration
|
2023-05-17 22:47:58 +01:00
|
|
|
}
|
|
|
|
return toReturn
|
|
|
|
}
|
|
|
|
}
|