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:
parent
ec42fdae6d
commit
2d8f396eeb
@ -5,6 +5,7 @@ package apigen
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -13,9 +14,18 @@ import (
|
|||||||
|
|
||||||
// API represents specific API's configuration.
|
// API represents specific API's configuration.
|
||||||
type API struct {
|
type API struct {
|
||||||
|
// 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
|
Version string
|
||||||
Description string
|
Description string
|
||||||
|
// The package name to use for the Go generated code.
|
||||||
PackageName string
|
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
|
Auth api.Auth
|
||||||
EndpointGroups []*EndpointGroup
|
EndpointGroups []*EndpointGroup
|
||||||
}
|
}
|
||||||
@ -32,6 +42,14 @@ func (a *API) Group(name, prefix string) *EndpointGroup {
|
|||||||
return group
|
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.
|
// StringBuilder is an extension of strings.Builder that allows for writing formatted lines.
|
||||||
type StringBuilder struct{ strings.Builder }
|
type StringBuilder struct{ strings.Builder }
|
||||||
|
|
||||||
|
47
private/apigen/common_test.go
Normal file
47
private/apigen/common_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -58,7 +58,7 @@ func (api *API) generateDocumentation() string {
|
|||||||
for _, endpoint := range group.endpoints {
|
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("<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\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 {
|
if len(endpoint.QueryParams) > 0 {
|
||||||
wf("**Query Params:**\n\n")
|
wf("**Query Params:**\n\n")
|
||||||
@ -140,7 +140,6 @@ func getTypeNameRecursively(t reflect.Type, level int) string {
|
|||||||
elemType := t.Elem()
|
elemType := t.Elem()
|
||||||
if elemType.Kind() == reflect.Uint8 { // treat []byte as string in docs
|
if elemType.Kind() == reflect.Uint8 { // treat []byte as string in docs
|
||||||
return prefix + "string"
|
return prefix + "string"
|
||||||
|
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s[\n%s\n%s]\n", prefix, getTypeNameRecursively(elemType, level+1), prefix)
|
return fmt.Sprintf("%s[\n%s\n%s]\n", prefix, getTypeNameRecursively(elemType, level+1), prefix)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
|
@ -4,15 +4,16 @@
|
|||||||
|
|
||||||
**Version:** `v0`
|
**Version:** `v0`
|
||||||
|
|
||||||
**List of endpoints:**
|
<h2 id='list-of-endpoints'>List of Endpoints</h2>
|
||||||
|
|
||||||
* TestAPI
|
* 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:**
|
**Query Params:**
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
a := &apigen.API{PackageName: "example", Version: "v0"}
|
a := &apigen.API{PackageName: "example", Version: "v0", BasePath: "/api"}
|
||||||
|
|
||||||
g := a.Group("TestAPI", "testapi")
|
g := a.Group("TestAPI", "testapi")
|
||||||
|
|
||||||
|
@ -101,7 +101,12 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
|
|
||||||
for _, group := range a.EndpointGroups {
|
for _, group := range a.EndpointGroups {
|
||||||
i("github.com/zeebo/errs")
|
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("")
|
pf("")
|
||||||
@ -168,10 +173,16 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
pf("auth: auth,")
|
pf("auth: auth,")
|
||||||
pf("}")
|
pf("}")
|
||||||
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 {
|
for _, endpoint := range group.endpoints {
|
||||||
handlerName := "handle" + endpoint.MethodName
|
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("")
|
||||||
pf("return handler")
|
pf("return handler")
|
||||||
@ -243,7 +254,11 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
pf("")
|
pf("")
|
||||||
pf("err = json.NewEncoder(w).Encode(retVal)")
|
pf("err = json.NewEncoder(w).Encode(retVal)")
|
||||||
pf("if err != nil {")
|
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("}")
|
||||||
pf("}")
|
pf("}")
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ func (f *tsGenFile) registerTypes() {
|
|||||||
func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
||||||
f.pf("\nexport class %sHttpApi%s {", group.Prefix, strings.ToUpper(f.api.Version))
|
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 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 {
|
for _, method := range group.endpoints {
|
||||||
f.pf("")
|
f.pf("")
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
Creates new Project with given info
|
Creates new Project with given info
|
||||||
|
|
||||||
`POST /projects/create`
|
`POST /api/v0/projects/create`
|
||||||
|
|
||||||
**Request body:**
|
**Request body:**
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ unknown
|
|||||||
|
|
||||||
Updates project with given info
|
Updates project with given info
|
||||||
|
|
||||||
`PATCH /projects/update/{id}`
|
`PATCH /api/v0/projects/update/{id}`
|
||||||
|
|
||||||
**Path Params:**
|
**Path Params:**
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ Updates project with given info
|
|||||||
|
|
||||||
Deletes project by id
|
Deletes project by id
|
||||||
|
|
||||||
`DELETE /projects/delete/{id}`
|
`DELETE /api/v0/projects/delete/{id}`
|
||||||
|
|
||||||
**Path Params:**
|
**Path Params:**
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ Deletes project by id
|
|||||||
|
|
||||||
Gets all projects user has
|
Gets all projects user has
|
||||||
|
|
||||||
`GET /projects/`
|
`GET /api/v0/projects/`
|
||||||
|
|
||||||
**Response body:**
|
**Response body:**
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ Gets all projects user has
|
|||||||
|
|
||||||
Gets project's single bucket usage by bucket ID
|
Gets project's single bucket usage by bucket ID
|
||||||
|
|
||||||
`GET /projects/bucket-rollup`
|
`GET /api/v0/projects/bucket-rollup`
|
||||||
|
|
||||||
**Query Params:**
|
**Query Params:**
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ Gets project's single bucket usage by bucket ID
|
|||||||
|
|
||||||
Gets project's all buckets usage
|
Gets project's all buckets usage
|
||||||
|
|
||||||
`GET /projects/bucket-rollups`
|
`GET /api/v0/projects/bucket-rollups`
|
||||||
|
|
||||||
**Query Params:**
|
**Query Params:**
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ Gets project's all buckets usage
|
|||||||
|
|
||||||
Gets API keys by project ID
|
Gets API keys by project ID
|
||||||
|
|
||||||
`GET /projects/apikeys/{projectID}`
|
`GET /api/v0/projects/apikeys/{projectID}`
|
||||||
|
|
||||||
**Query Params:**
|
**Query Params:**
|
||||||
|
|
||||||
@ -265,7 +265,7 @@ Gets API keys by project ID
|
|||||||
|
|
||||||
Creates new macaroon API key with given info
|
Creates new macaroon API key with given info
|
||||||
|
|
||||||
`POST /apikeys/create`
|
`POST /api/v0/apikeys/create`
|
||||||
|
|
||||||
**Request body:**
|
**Request body:**
|
||||||
|
|
||||||
@ -291,7 +291,7 @@ Creates new macaroon API key with given info
|
|||||||
|
|
||||||
Deletes macaroon API key by id
|
Deletes macaroon API key by id
|
||||||
|
|
||||||
`DELETE /apikeys/delete/{id}`
|
`DELETE /api/v0/apikeys/delete/{id}`
|
||||||
|
|
||||||
**Path Params:**
|
**Path Params:**
|
||||||
|
|
||||||
@ -303,7 +303,7 @@ Deletes macaroon API key by id
|
|||||||
|
|
||||||
Gets User by request context
|
Gets User by request context
|
||||||
|
|
||||||
`GET /users/`
|
`GET /api/v0/users/`
|
||||||
|
|
||||||
**Response body:**
|
**Response body:**
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ func main() {
|
|||||||
// definition for REST API
|
// definition for REST API
|
||||||
a := &apigen.API{
|
a := &apigen.API{
|
||||||
Version: "v0",
|
Version: "v0",
|
||||||
|
BasePath: "/api",
|
||||||
Description: "Interacts with projects",
|
Description: "Interacts with projects",
|
||||||
PackageName: "consoleapi",
|
PackageName: "consoleapi",
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user