private/apigen: Don't force casing for API group name/prefix

The API generators rely on the Name and Prefix fields of the
EndpointGroup type to generate code.

Conventional naming code requires using upper or lower case for types,
functions, etc, however requiring the user to set this fields with the
correct casing seems cumbersome for them because they can be adjusted
depending where those values are used on the generated code.

This commit lifts the restriction for the user and adjust the casing of
them according to where they are used.

Change-Id: I700a879d13b4789b4d6ba0519b4d7508061eac73
This commit is contained in:
Ivan Fraixedes 2023-10-03 19:41:59 +02:00 committed by Storj Robot
parent 7b50ece931
commit 3193ff9155
9 changed files with 108 additions and 35 deletions

View File

@ -16,10 +16,9 @@ import (
"storj.io/storj/private/api"
)
var (
groupNameRegExp = regexp.MustCompile(`^([A-Z0-9]\w*)?$`)
groupPrefixRegExp = regexp.MustCompile(`^\w*$`)
)
// groupNameAndPrefixRegExp guarantees that Group name and prefix are empty or have are only formed
// by ASCII letters or digits and not starting with a digit.
var groupNameAndPrefixRegExp = regexp.MustCompile(`^([A-Za-z][0-9A-Za-z]*)?$`)
// API represents specific API's configuration.
type API struct {
@ -43,21 +42,21 @@ type API struct {
// name must be `^([A-Z0-9]\w*)?$“
// prefix must be `^\w*$`.
func (a *API) Group(name, prefix string) *EndpointGroup {
if !groupNameRegExp.MatchString(name) {
if !groupNameAndPrefixRegExp.MatchString(name) {
panic(
fmt.Sprintf(
"invalid name for API Endpoint Group. name must fulfill the regular expression %q, got %q",
groupNameRegExp,
groupNameAndPrefixRegExp,
name,
),
)
}
if !groupPrefixRegExp.MatchString(prefix) {
if !groupNameAndPrefixRegExp.MatchString(prefix) {
panic(
fmt.Sprintf(
"invalid prefix for API Endpoint Group %q. prefix must fulfill the regular expression %q, got %q",
name,
groupPrefixRegExp,
groupNameAndPrefixRegExp,
prefix,
),
)
@ -140,6 +139,15 @@ func capitalize(s string) string {
return string(unicode.ToTitle(r)) + s[size:]
}
func uncapitalize(s string) string {
r, size := utf8.DecodeRuneInString(s)
if size <= 0 {
return s
}
return string(unicode.ToLower(r)) + s[size:]
}
type typeAndName struct {
Type reflect.Type
Name string

View File

@ -8,6 +8,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPI_endpointBasePath(t *testing.T) {
@ -45,3 +46,41 @@ func TestAPI_endpointBasePath(t *testing.T) {
})
}
}
func TestAPI_Group(t *testing.T) {
t.Run("valid name and prefix", func(t *testing.T) {
api := API{}
require.NotPanics(t, func() {
api.Group("testName", "tName")
})
require.NotPanics(t, func() {
api.Group("TestName1", "TName1")
})
})
t.Run("invalid name", func(t *testing.T) {
api := API{}
require.Panics(t, func() {
api.Group("1testName", "tName")
})
require.Panics(t, func() {
api.Group("test-name", "tName")
})
})
t.Run("invalid prefix", func(t *testing.T) {
api := API{}
require.Panics(t, func() {
api.Group("testName", "5tName")
})
require.Panics(t, func() {
api.Group("testname", "t_name")
})
})
}

View File

@ -177,7 +177,24 @@ func (fe fullEndpoint) responseType() reflect.Type {
// You should always create a group using API.Group because it validates the field values to
// guarantee correct code generation.
type EndpointGroup struct {
Name string
// Name is the group name.
//
// Go generator uses it as part of type, functions, interfaces names, and in code comments.
// The casing is adjusted according where it's used.
//
// TypeScript generator uses it as part of types names for the API functionality of this group.
// The casing is adjusted according where it's used.
//
// Document generator uses as it is.
Name string
// Prefix is a prefix used for
//
// Go generator uses it as part of variables names, error messages, and the URL base path for the group.
// The casing is adjusted according where it's used, but for the URL base path, lowercase is used.
//
// TypeScript generator uses it for composing the URL base path (lowercase).
//
// Document generator uses as it is.
Prefix string
endpoints []*fullEndpoint
}

View File

@ -47,7 +47,7 @@ type DocumentsService interface {
}, api.HTTPError)
}
// DocumentsHandler is an api handler that exposes all docs related functionality.
// DocumentsHandler is an api handler that implements all Documents API endpoints functionality.
type DocumentsHandler struct {
log *zap.Logger
mon *monkit.Scope

View File

@ -50,7 +50,7 @@ export type GetResponse = Array<GetResponseItem>
export type GetResponseItemLastRetrievals = Array<GetResponseItemLastRetrievalsItem>
export class DocsHttpApiV0 {
export class DocumentsHttpApiV0 {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/docs';

View File

@ -12,8 +12,6 @@ import (
"time"
"github.com/zeebo/errs"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"storj.io/common/uuid"
)
@ -108,9 +106,9 @@ func (a *API) generateGo() ([]byte, error) {
i("github.com/zeebo/errs")
pf(
"var Err%sAPI = errs.Class(\"%s %s api\")",
cases.Title(language.Und).String(group.Prefix),
capitalize(group.Prefix),
a.PackageName,
group.Prefix,
strings.ToLower(group.Prefix),
)
}
@ -119,7 +117,7 @@ func (a *API) generateGo() ([]byte, error) {
params := make(map[*fullEndpoint][]Param)
for _, group := range a.EndpointGroups {
pf("type %sService interface {", group.Name)
pf("type %sService interface {", capitalize(group.Name))
for _, e := range group.endpoints {
params[e] = append(e.PathParams, e.QueryParams...)
@ -152,38 +150,49 @@ func (a *API) generateGo() ([]byte, error) {
}
for _, group := range a.EndpointGroups {
cname := capitalize(group.Name)
i("go.uber.org/zap", "github.com/spacemonkeygo/monkit/v3")
pf("// %sHandler is an api handler that exposes all %s related functionality.", group.Name, group.Prefix)
pf("type %sHandler struct {", group.Name)
pf(
"// %sHandler is an api handler that implements all %s API endpoints functionality.",
cname,
group.Name,
)
pf("type %sHandler struct {", cname)
pf("log *zap.Logger")
pf("mon *monkit.Scope")
pf("service %sService", group.Name)
pf("service %sService", cname)
pf("auth api.Auth")
pf("}")
pf("")
}
for _, group := range a.EndpointGroups {
cname := capitalize(group.Name)
i("github.com/gorilla/mux")
pf(
"func New%s(log *zap.Logger, mon *monkit.Scope, service %sService, router *mux.Router, auth api.Auth) *%sHandler {",
group.Name,
group.Name,
group.Name,
cname,
cname,
cname,
)
pf("handler := &%sHandler{", group.Name)
pf("handler := &%sHandler{", cname)
pf("log: log,")
pf("mon: mon,")
pf("service: service,")
pf("auth: auth,")
pf("}")
pf("")
pf("%sRouter := router.PathPrefix(\"%s/%s\").Subrouter()", group.Prefix, a.endpointBasePath(), group.Prefix)
pf(
"%sRouter := router.PathPrefix(\"%s/%s\").Subrouter()",
uncapitalize(group.Prefix),
a.endpointBasePath(),
strings.ToLower(group.Prefix),
)
for _, endpoint := range group.endpoints {
handlerName := "handle" + endpoint.GoName
pf(
"%sRouter.HandleFunc(\"%s\", handler.%s).Methods(\"%s\")",
group.Prefix,
uncapitalize(group.Prefix),
endpoint.Path,
handlerName,
endpoint.Method,
@ -200,7 +209,7 @@ func (a *API) generateGo() ([]byte, error) {
i("net/http")
pf("")
handlerName := "handle" + endpoint.GoName
pf("func (h *%sHandler) %s(w http.ResponseWriter, r *http.Request) {", group.Name, handlerName)
pf("func (h *%sHandler) %s(w http.ResponseWriter, r *http.Request) {", capitalize(group.Name), handlerName)
pf("ctx := r.Context()")
pf("var err error")
pf("defer h.mon.Task()(&ctx)(&err)")
@ -262,7 +271,7 @@ func (a *API) generateGo() ([]byte, error) {
pf(
"h.log.Debug(\"failed to write json %s response\", zap.Error(Err%sAPI.Wrap(err)))",
endpoint.GoName,
cases.Title(language.Und).String(group.Prefix),
capitalize(group.Prefix),
)
pf("}")
pf("}")

View File

@ -85,9 +85,9 @@ func (f *tsGenFile) registerTypes() {
}
func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
f.pf("\nexport class %sHttpApi%s {", capitalize(group.Prefix), strings.ToUpper(f.api.Version))
f.pf("\nexport class %sHttpApi%s {", capitalize(group.Name), strings.ToUpper(f.api.Version))
f.pf("\tprivate readonly http: HttpClient = new HttpClient();")
f.pf("\tprivate readonly ROOT_PATH: string = '%s/%s';", f.api.endpointBasePath(), group.Prefix)
f.pf("\tprivate readonly ROOT_PATH: string = '%s/%s';", f.api.endpointBasePath(), strings.ToLower(group.Prefix))
for _, method := range group.endpoints {
f.pf("")

View File

@ -46,7 +46,7 @@ type UserManagementService interface {
GenGetUser(ctx context.Context) (*console.ResponseUser, api.HTTPError)
}
// ProjectManagementHandler is an api handler that exposes all projects related functionality.
// ProjectManagementHandler is an api handler that implements all ProjectManagement API endpoints functionality.
type ProjectManagementHandler struct {
log *zap.Logger
mon *monkit.Scope
@ -54,7 +54,7 @@ type ProjectManagementHandler struct {
auth api.Auth
}
// APIKeyManagementHandler is an api handler that exposes all apikeys related functionality.
// APIKeyManagementHandler is an api handler that implements all APIKeyManagement API endpoints functionality.
type APIKeyManagementHandler struct {
log *zap.Logger
mon *monkit.Scope
@ -62,7 +62,7 @@ type APIKeyManagementHandler struct {
auth api.Auth
}
// UserManagementHandler is an api handler that exposes all users related functionality.
// UserManagementHandler is an api handler that implements all UserManagement API endpoints functionality.
type UserManagementHandler struct {
log *zap.Logger
mon *monkit.Scope

View File

@ -94,7 +94,7 @@ export class UpsertProjectInfo {
createdAt: Time;
}
export class ProjectsHttpApiV0 {
export class ProjectManagementHttpApiV0 {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/projects';
@ -184,7 +184,7 @@ export class ProjectsHttpApiV0 {
}
}
export class ApikeysHttpApiV0 {
export class APIKeyManagementHttpApiV0 {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/apikeys';
@ -209,7 +209,7 @@ export class ApikeysHttpApiV0 {
}
}
export class UsersHttpApiV0 {
export class UserManagementHttpApiV0 {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/users';