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
This commit is contained in:
Ivan Fraixedes 2023-08-24 18:55:59 +02:00 committed by Storj Robot
parent ec42fdae6d
commit 2d8f396eeb
9 changed files with 106 additions and 25 deletions

View File

@ -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 }

View File

@ -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())
})
}
}

View File

@ -58,7 +58,7 @@ func (api *API) generateDocumentation() string {
for _, endpoint := range group.endpoints {
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)
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:

View File

@ -4,15 +4,16 @@
**Version:** `v0`
**List of endpoints:**
<h2 id='list-of-endpoints'>List of Endpoints</h2>
* TestAPI
* [](#e-31104e2390954bdc113e2444e69a0667)
* [](#testapi-)
<h2 id='e-31104e2390954bdc113e2444e69a0667'></h2>
<h3 id='testapi-'> (<a href='#list-of-endpoints'>go to full list</a>)</h3>
`POST /testapi/{path}`
`POST /api/v0/testapi/{path}`
**Query Params:**

View File

@ -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")

View File

@ -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("}")
}

View File

@ -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("")

View File

@ -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:**

View File

@ -22,6 +22,7 @@ func main() {
// definition for REST API
a := &apigen.API{
Version: "v0",
BasePath: "/api",
Description: "Interacts with projects",
PackageName: "consoleapi",
}