// Copyright (C) 2022 Storj Labs, Inc. // See LICENSE for copying information. package apigen import ( "fmt" "os" "reflect" "regexp" "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) wf("

List of Endpoints

\n\n") getEndpointLink := func(group, endpoint string) string { fullName := group + "-" + endpoint fullName = strings.ReplaceAll(fullName, " ", "-") var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9-]+`) fullName = nonAlphanumericRegex.ReplaceAllString(fullName, "") return strings.ToLower(fullName) } 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") for _, group := range api.EndpointGroups { for _, endpoint := range group.endpoints { wf("

%s (go to full list)

\n\n", getEndpointLink(group.Name, endpoint.Name), endpoint.Name) wf("%s\n\n", endpoint.Description) wf("`%s %s/%s%s`\n\n", endpoint.Method, api.endpointBasePath(), 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("```typescript\n%s\n```\n\n", getTypeNameRecursively(requestType, 0)) } responseType := reflect.TypeOf(endpoint.Response) if responseType != nil { wf("**Response body:**\n\n") wf("```typescript\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 } }