diff --git a/private/apigen/docgen.go b/private/apigen/docgen.go new file mode 100644 index 000000000..0c98a3ce4 --- /dev/null +++ b/private/apigen/docgen.go @@ -0,0 +1,157 @@ +// 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 + } +} diff --git a/satellite/console/consoleweb/consoleapi/apidocs.gen.md b/satellite/console/consoleweb/consoleapi/apidocs.gen.md new file mode 100644 index 000000000..654acfdec --- /dev/null +++ b/satellite/console/consoleweb/consoleapi/apidocs.gen.md @@ -0,0 +1,313 @@ +# API Docs + +**Description:** Interacts with projects + +**Version:** `v0` + +## Create new Project + +Creates new Project with given info + +`POST /projects/create` + +**Request body:** + +```json +{ + name: string + description: string + storageLimit: string (Amount of memory formatted as `15 GB`) + bandwidthLimit: string (Amount of memory formatted as `15 GB`) + createdAt: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) +} + +``` + +**Response body:** + +```json +unknown +``` + +## Update Project + +Updates project with given info + +`PATCH /projects/update/{id}` + +**Path Params:** + +| name | type | elaboration | +|---|---|---| +| `id` | `string` | UUID formatted as `00000000-0000-0000-0000-000000000000` | + +**Request body:** + +```json +{ + name: string + description: string + storageLimit: string (Amount of memory formatted as `15 GB`) + bandwidthLimit: string (Amount of memory formatted as `15 GB`) + createdAt: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) +} + +``` + +**Response body:** + +```json +{ + id: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + publicId: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + name: string + description: string + userAgent: string + ownerId: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + rateLimit: number + burstLimit: number + maxBuckets: number + createdAt: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) + memberCount: number + storageLimit: string (Amount of memory formatted as `15 GB`) + bandwidthLimit: string (Amount of memory formatted as `15 GB`) + userSpecifiedStorageLimit: string (Amount of memory formatted as `15 GB`) + userSpecifiedBandwidthLimit: string (Amount of memory formatted as `15 GB`) + segmentLimit: number + defaultPlacement: number +} + +``` + +## Delete Project + +Deletes project by id + +`DELETE /projects/delete/{id}` + +**Path Params:** + +| name | type | elaboration | +|---|---|---| +| `id` | `string` | UUID formatted as `00000000-0000-0000-0000-000000000000` | + +## Get Projects + +Gets all projects user has + +`GET /projects/` + +**Response body:** + +```json +[ + { + id: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + publicId: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + name: string + description: string + userAgent: string + ownerId: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + rateLimit: number + burstLimit: number + maxBuckets: number + createdAt: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) + memberCount: number + storageLimit: string (Amount of memory formatted as `15 GB`) + bandwidthLimit: string (Amount of memory formatted as `15 GB`) + userSpecifiedStorageLimit: string (Amount of memory formatted as `15 GB`) + userSpecifiedBandwidthLimit: string (Amount of memory formatted as `15 GB`) + segmentLimit: number + defaultPlacement: number + } + +] + +``` + +## Get Project's Single Bucket Usage + +Gets project's single bucket usage by bucket ID + +`GET /projects/bucket-rollup` + +**Query Params:** + +| name | type | elaboration | +|---|---|---| +| `projectID` | `string` | UUID formatted as `00000000-0000-0000-0000-000000000000` | +| `bucket` | `string` | | +| `since` | `string` | Date timestamp formatted as `2006-01-02T15:00:00Z` | +| `before` | `string` | Date timestamp formatted as `2006-01-02T15:00:00Z` | + +**Response body:** + +```json +{ + projectID: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + bucketName: string + totalStoredData: number + totalSegments: number + objectCount: number + metadataSize: number + repairEgress: number + getEgress: number + auditEgress: number + since: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) + before: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) +} + +``` + +## Get Project's All Buckets Usage + +Gets project's all buckets usage + +`GET /projects/bucket-rollups` + +**Query Params:** + +| name | type | elaboration | +|---|---|---| +| `projectID` | `string` | UUID formatted as `00000000-0000-0000-0000-000000000000` | +| `since` | `string` | Date timestamp formatted as `2006-01-02T15:00:00Z` | +| `before` | `string` | Date timestamp formatted as `2006-01-02T15:00:00Z` | + +**Response body:** + +```json +[ + { + projectID: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + bucketName: string + totalStoredData: number + totalSegments: number + objectCount: number + metadataSize: number + repairEgress: number + getEgress: number + auditEgress: number + since: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) + before: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) + } + +] + +``` + +## Get Project's API Keys + +Gets API keys by project ID + +`GET /projects/apikeys/{projectID}` + +**Query Params:** + +| name | type | elaboration | +|---|---|---| +| `search` | `string` | | +| `limit` | `number` | | +| `page` | `number` | | +| `order` | `number` | | +| `orderDirection` | `number` | | + +**Path Params:** + +| name | type | elaboration | +|---|---|---| +| `projectID` | `string` | UUID formatted as `00000000-0000-0000-0000-000000000000` | + +**Response body:** + +```json +{ + apiKeys: [ + { + id: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + projectId: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + projectPublicId: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + userAgent: string + name: string + createdAt: string (Date timestamp formatted as `2006-01-02T15:00:00Z`) + } + + ] + + search: string + limit: number + order: number + orderDirection: number + offset: number + pageCount: number + currentPage: number + totalCount: number +} + +``` + +## Create new macaroon API key + +Creates new macaroon API key with given info + +`POST /apikeys/create` + +**Request body:** + +```json +{ + projectID: string + name: string +} + +``` + +**Response body:** + +```json +{ + key: string + keyInfo: unknown +} + +``` + +## Delete API Key + +Deletes macaroon API key by id + +`DELETE /apikeys/delete/{id}` + +**Path Params:** + +| name | type | elaboration | +|---|---|---| +| `id` | `string` | UUID formatted as `00000000-0000-0000-0000-000000000000` | + +## Get User + +Gets User by request context + +`GET /users/` + +**Response body:** + +```json +{ + id: string (UUID formatted as `00000000-0000-0000-0000-000000000000`) + fullName: string + shortName: string + email: string + userAgent: string + projectLimit: number + isProfessional: boolean + position: string + companyName: string + employeeCount: string + haveSalesContact: boolean + paidTier: boolean + isMFAEnabled: boolean + mfaRecoveryCodeCount: number +} + +``` + diff --git a/satellite/console/consoleweb/consoleapi/gen/main.go b/satellite/console/consoleweb/consoleapi/gen/main.go index b4307e400..7acb6376a 100644 --- a/satellite/console/consoleweb/consoleapi/gen/main.go +++ b/satellite/console/consoleweb/consoleapi/gen/main.go @@ -22,7 +22,7 @@ func main() { // definition for REST API a := &apigen.API{ Version: "v0", - Description: "", + Description: "Interacts with projects", PackageName: "consoleapi", } @@ -152,6 +152,7 @@ func main() { modroot := findModuleRootDir() a.MustWriteGo(filepath.Join(modroot, "satellite", "console", "consoleweb", "consoleapi", "api.gen.go")) a.MustWriteTS(filepath.Join(modroot, "web", "satellite", "src", "api", a.Version+".gen.ts")) + a.MustWriteDocs(filepath.Join(modroot, "satellite", "console", "consoleweb", "consoleapi", "apidocs.gen.md")) } func findModuleRootDir() string { diff --git a/web/satellite/src/api/v0.gen.ts b/web/satellite/src/api/v0.gen.ts index 92cf1f6c3..a453ba9f6 100644 --- a/web/satellite/src/api/v0.gen.ts +++ b/web/satellite/src/api/v0.gen.ts @@ -66,6 +66,7 @@ export class Project { userSpecifiedStorageLimit: MemorySize; userSpecifiedBandwidthLimit: MemorySize; segmentLimit: number; + defaultPlacement: number; } export class ProjectInfo {