From 2d8f396eeb85353df92dff8cd87e15907f7ec04e Mon Sep 17 00:00:00 2001 From: Ivan Fraixedes Date: Thu, 24 Aug 2023 18:55:59 +0200 Subject: [PATCH] private/apigen: Make API base path configurable Previously the base path for the API was hardcoded to `/api` and the specified version. This was not obvious that the generated code was setting that base path and it was not flexible for serving the API under a different path than `/api`. We will likely need to set a different base path if we pretend to serve the new back office API that we are going to implement alongside the current admin API until the new back office is fully implemented and verified that works properly. This commit also fix add the base path of the endpoints to the documentation because it was even more confusing for somebody that wants to use the API having to find out them through looking to the generated code. Change-Id: I6efab6b6f3d295129d6f42f7fbba8c2dc19725f4 --- private/apigen/common.go | 24 ++++++++-- private/apigen/common_test.go | 47 +++++++++++++++++++ private/apigen/docgen.go | 3 +- private/apigen/example/apidocs.gen.md | 9 ++-- private/apigen/example/gen.go | 2 +- private/apigen/gogen.go | 23 +++++++-- private/apigen/tsgen.go | 2 +- .../consoleweb/consoleapi/apidocs.gen.md | 20 ++++---- .../console/consoleweb/consoleapi/gen/main.go | 1 + 9 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 private/apigen/common_test.go diff --git a/private/apigen/common.go b/private/apigen/common.go index a27770028..ad294ced0 100644 --- a/private/apigen/common.go +++ b/private/apigen/common.go @@ -5,6 +5,7 @@ package apigen import ( "fmt" + "path" "reflect" "strings" @@ -13,9 +14,18 @@ import ( // API represents specific API's configuration. type API struct { - Version string - Description string - PackageName string + // Version is the corresponding version of the API. + // It's concatenated to the BasePath, so assuming the base path is "/api" and the version is "v1" + // the API paths will begin with `/api/v1`. + // When empty, the version doesn't appear in the API paths. If it starts or ends with one or more + // "/", they are stripped from the API endpoint paths. + Version string + Description string + // The package name to use for the Go generated code. + PackageName string + // BasePath is the base path for the API endpoints. E.g. "/api". + // It doesn't require to begin with "/". When empty, "/" is used. + BasePath string Auth api.Auth EndpointGroups []*EndpointGroup } @@ -32,6 +42,14 @@ func (a *API) Group(name, prefix string) *EndpointGroup { return group } +func (a *API) endpointBasePath() string { + if strings.HasPrefix(a.BasePath, "/") { + return path.Join(a.BasePath, a.Version) + } + + return "/" + path.Join(a.BasePath, a.Version) +} + // StringBuilder is an extension of strings.Builder that allows for writing formatted lines. type StringBuilder struct{ strings.Builder } diff --git a/private/apigen/common_test.go b/private/apigen/common_test.go new file mode 100644 index 000000000..00b2f39f6 --- /dev/null +++ b/private/apigen/common_test.go @@ -0,0 +1,47 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + +package apigen + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAPI_endpointBasePath(t *testing.T) { + cases := []struct { + version string + basePath string + expected string + }{ + {version: "", basePath: "", expected: "/"}, + {version: "v1", basePath: "", expected: "/v1"}, + {version: "v0", basePath: "/", expected: "/v0"}, + {version: "", basePath: "api", expected: "/api"}, + {version: "v2", basePath: "api", expected: "/api/v2"}, + {version: "v2", basePath: "/api", expected: "/api/v2"}, + {version: "v2", basePath: "api/", expected: "/api/v2"}, + {version: "v2", basePath: "/api/", expected: "/api/v2"}, + {version: "/v3", basePath: "api", expected: "/api/v3"}, + {version: "/v3/", basePath: "api", expected: "/api/v3"}, + {version: "v3/", basePath: "api", expected: "/api/v3"}, + {version: "//v3/", basePath: "api", expected: "/api/v3"}, + {version: "v3///", basePath: "api", expected: "/api/v3"}, + {version: "/v3///", basePath: "/api/test/", expected: "/api/test/v3"}, + {version: "/v4.2", basePath: "api/test", expected: "/api/test/v4.2"}, + {version: "/v4/2", basePath: "/api/test", expected: "/api/test/v4/2"}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("version:%s basePath: %s", c.version, c.basePath), func(t *testing.T) { + a := API{ + Version: c.version, + BasePath: c.basePath, + } + + assert.Equal(t, c.expected, a.endpointBasePath()) + }) + } +} diff --git a/private/apigen/docgen.go b/private/apigen/docgen.go index 575c54b74..9f8b8f613 100644 --- a/private/apigen/docgen.go +++ b/private/apigen/docgen.go @@ -58,7 +58,7 @@ func (api *API) generateDocumentation() string { 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`\n\n", endpoint.Method, group.Prefix, endpoint.Path) + 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") @@ -140,7 +140,6 @@ func getTypeNameRecursively(t reflect.Type, level int) string { 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: diff --git a/private/apigen/example/apidocs.gen.md b/private/apigen/example/apidocs.gen.md index 631db3ea8..52ff036bb 100644 --- a/private/apigen/example/apidocs.gen.md +++ b/private/apigen/example/apidocs.gen.md @@ -4,15 +4,16 @@ **Version:** `v0` -**List of endpoints:** +

List of Endpoints

+ * TestAPI - * [](#e-31104e2390954bdc113e2444e69a0667) + * [](#testapi-) -

+

(go to full list)

-`POST /testapi/{path}` +`POST /api/v0/testapi/{path}` **Query Params:** diff --git a/private/apigen/example/gen.go b/private/apigen/example/gen.go index 7b44d7f92..9267fb456 100644 --- a/private/apigen/example/gen.go +++ b/private/apigen/example/gen.go @@ -14,7 +14,7 @@ import ( ) func main() { - a := &apigen.API{PackageName: "example", Version: "v0"} + a := &apigen.API{PackageName: "example", Version: "v0", BasePath: "/api"} g := a.Group("TestAPI", "testapi") diff --git a/private/apigen/gogen.go b/private/apigen/gogen.go index ed272ccdf..160c13c50 100644 --- a/private/apigen/gogen.go +++ b/private/apigen/gogen.go @@ -101,7 +101,12 @@ func (a *API) generateGo() ([]byte, error) { for _, group := range a.EndpointGroups { i("github.com/zeebo/errs") - pf("var Err%sAPI = errs.Class(\"%s %s api\")", cases.Title(language.Und).String(group.Prefix), a.PackageName, group.Prefix) + pf( + "var Err%sAPI = errs.Class(\"%s %s api\")", + cases.Title(language.Und).String(group.Prefix), + a.PackageName, + group.Prefix, + ) } pf("") @@ -168,10 +173,16 @@ func (a *API) generateGo() ([]byte, error) { pf("auth: auth,") pf("}") pf("") - pf("%sRouter := router.PathPrefix(\"/api/v0/%s\").Subrouter()", group.Prefix, group.Prefix) + pf("%sRouter := router.PathPrefix(\"%s/%s\").Subrouter()", group.Prefix, a.endpointBasePath(), group.Prefix) for _, endpoint := range group.endpoints { handlerName := "handle" + endpoint.MethodName - pf("%sRouter.HandleFunc(\"%s\", handler.%s).Methods(\"%s\")", group.Prefix, endpoint.Path, handlerName, endpoint.Method) + pf( + "%sRouter.HandleFunc(\"%s\", handler.%s).Methods(\"%s\")", + group.Prefix, + endpoint.Path, + handlerName, + endpoint.Method, + ) } pf("") pf("return handler") @@ -243,7 +254,11 @@ func (a *API) generateGo() ([]byte, error) { pf("") pf("err = json.NewEncoder(w).Encode(retVal)") pf("if err != nil {") - pf("h.log.Debug(\"failed to write json %s response\", zap.Error(Err%sAPI.Wrap(err)))", endpoint.MethodName, cases.Title(language.Und).String(group.Prefix)) + pf( + "h.log.Debug(\"failed to write json %s response\", zap.Error(Err%sAPI.Wrap(err)))", + endpoint.MethodName, + cases.Title(language.Und).String(group.Prefix), + ) pf("}") pf("}") } diff --git a/private/apigen/tsgen.go b/private/apigen/tsgen.go index 1a7d86f65..a7e86e1ad 100644 --- a/private/apigen/tsgen.go +++ b/private/apigen/tsgen.go @@ -86,7 +86,7 @@ func (f *tsGenFile) registerTypes() { func (f *tsGenFile) createAPIClient(group *EndpointGroup) { f.pf("\nexport class %sHttpApi%s {", group.Prefix, strings.ToUpper(f.api.Version)) f.pf("\tprivate readonly http: HttpClient = new HttpClient();") - f.pf("\tprivate readonly ROOT_PATH: string = '/api/%s/%s';", f.api.Version, group.Prefix) + f.pf("\tprivate readonly ROOT_PATH: string = '%s/%s';", f.api.endpointBasePath(), group.Prefix) for _, method := range group.endpoints { f.pf("") diff --git a/satellite/console/consoleweb/consoleapi/apidocs.gen.md b/satellite/console/consoleweb/consoleapi/apidocs.gen.md index a81aa5b6e..8a1a193d4 100644 --- a/satellite/console/consoleweb/consoleapi/apidocs.gen.md +++ b/satellite/console/consoleweb/consoleapi/apidocs.gen.md @@ -24,7 +24,7 @@ Creates new Project with given info -`POST /projects/create` +`POST /api/v0/projects/create` **Request body:** @@ -49,7 +49,7 @@ unknown Updates project with given info -`PATCH /projects/update/{id}` +`PATCH /api/v0/projects/update/{id}` **Path Params:** @@ -99,7 +99,7 @@ Updates project with given info Deletes project by id -`DELETE /projects/delete/{id}` +`DELETE /api/v0/projects/delete/{id}` **Path Params:** @@ -111,7 +111,7 @@ Deletes project by id Gets all projects user has -`GET /projects/` +`GET /api/v0/projects/` **Response body:** @@ -145,7 +145,7 @@ Gets all projects user has Gets project's single bucket usage by bucket ID -`GET /projects/bucket-rollup` +`GET /api/v0/projects/bucket-rollup` **Query Params:** @@ -179,7 +179,7 @@ Gets project's single bucket usage by bucket ID Gets project's all buckets usage -`GET /projects/bucket-rollups` +`GET /api/v0/projects/bucket-rollups` **Query Params:** @@ -215,7 +215,7 @@ Gets project's all buckets usage Gets API keys by project ID -`GET /projects/apikeys/{projectID}` +`GET /api/v0/projects/apikeys/{projectID}` **Query Params:** @@ -265,7 +265,7 @@ Gets API keys by project ID Creates new macaroon API key with given info -`POST /apikeys/create` +`POST /api/v0/apikeys/create` **Request body:** @@ -291,7 +291,7 @@ Creates new macaroon API key with given info Deletes macaroon API key by id -`DELETE /apikeys/delete/{id}` +`DELETE /api/v0/apikeys/delete/{id}` **Path Params:** @@ -303,7 +303,7 @@ Deletes macaroon API key by id Gets User by request context -`GET /users/` +`GET /api/v0/users/` **Response body:** diff --git a/satellite/console/consoleweb/consoleapi/gen/main.go b/satellite/console/consoleweb/consoleapi/gen/main.go index 0ba3a458f..d43b153d3 100644 --- a/satellite/console/consoleweb/consoleapi/gen/main.go +++ b/satellite/console/consoleweb/consoleapi/gen/main.go @@ -22,6 +22,7 @@ func main() { // definition for REST API a := &apigen.API{ Version: "v0", + BasePath: "/api", Description: "Interacts with projects", PackageName: "consoleapi", }