private/apigen: Avoid clashes of types names

The TypeScript generator create types of anonymous structs. When those
anonymous structs are in endpoints with the same name, but in different
endpoint groups, the generator panic because it create different types
with the same names.

This commit fixes that problem through using the endpoint group prefix
to create the types with different names.

Change-Id: Ibe87532609ce824b80951326f9777ed5b0cc2f7a
This commit is contained in:
Ivan Fraixedes 2023-10-03 19:27:08 +02:00 committed by Storj Robot
parent 3193ff9155
commit 99c4359062
8 changed files with 202 additions and 41 deletions

View File

@ -62,6 +62,15 @@ func (a *API) Group(name, prefix string) *EndpointGroup {
)
}
for _, g := range a.EndpointGroups {
if strings.EqualFold(g.Name, name) {
panic(fmt.Sprintf("name has to be case-insensitive unique across all the groups. name=%q", name))
}
if strings.EqualFold(g.Prefix, prefix) {
panic(fmt.Sprintf("prefix has to be case-insensitive unique across all the groups. prefix=%q", prefix))
}
}
group := &EndpointGroup{
Name: name,
Prefix: prefix,

View File

@ -83,4 +83,36 @@ func TestAPI_Group(t *testing.T) {
api.Group("testname", "t_name")
})
})
t.Run("group with repeated name", func(t *testing.T) {
api := API{}
require.NotPanics(t, func() {
api.Group("testName", "tName")
})
require.Panics(t, func() {
api.Group("TESTNAME", "tName2")
})
require.Panics(t, func() {
api.Group("testname", "tName3")
})
})
t.Run("group with repeated prefix", func(t *testing.T) {
api := API{}
require.NotPanics(t, func() {
api.Group("testName", "tName")
})
require.Panics(t, func() {
api.Group("testName2", "tname")
})
require.Panics(t, func() {
api.Group("testname3", "tnamE")
})
})
}

View File

@ -122,7 +122,9 @@ type fullEndpoint struct {
}
// requestType guarantees to return a named Go type associated to the Endpoint.Request field.
func (fe fullEndpoint) requestType() reflect.Type {
// g is used to avoid clashes with types defined in different groups that are different, but with
// the same name. It cannot be nil.
func (fe fullEndpoint) requestType(g *EndpointGroup) reflect.Type {
t := reflect.TypeOf(fe.Request)
if t.Name() != "" {
return t
@ -131,10 +133,10 @@ func (fe fullEndpoint) requestType() reflect.Type {
switch k := t.Kind(); k {
case reflect.Array, reflect.Slice:
if t.Elem().Name() == "" {
t = typeCustomName{Type: t, name: compoundTypeName(fe.TypeScriptName, "Request")}
t = typeCustomName{Type: t, name: compoundTypeName(capitalize(g.Prefix), fe.TypeScriptName, "Request")}
}
case reflect.Struct:
t = typeCustomName{Type: t, name: compoundTypeName(fe.TypeScriptName, "Request")}
t = typeCustomName{Type: t, name: compoundTypeName(capitalize(g.Prefix), fe.TypeScriptName, "Request")}
default:
panic(
fmt.Sprintf(
@ -148,7 +150,9 @@ func (fe fullEndpoint) requestType() reflect.Type {
}
// responseType guarantees to return a named Go type associated to the Endpoint.Response field.
func (fe fullEndpoint) responseType() reflect.Type {
// g is used to avoid clashes with types defined in different groups that are different, but with
// the same name. It cannot be nil.
func (fe fullEndpoint) responseType(g *EndpointGroup) reflect.Type {
t := reflect.TypeOf(fe.Response)
if t.Name() != "" {
return t
@ -157,10 +161,10 @@ func (fe fullEndpoint) responseType() reflect.Type {
switch k := t.Kind(); k {
case reflect.Array, reflect.Slice:
if t.Elem().Name() == "" {
t = typeCustomName{Type: t, name: compoundTypeName(fe.TypeScriptName, "Response")}
t = typeCustomName{Type: t, name: compoundTypeName(capitalize(g.Prefix), fe.TypeScriptName, "Response")}
}
case reflect.Struct:
t = typeCustomName{Type: t, name: compoundTypeName(fe.TypeScriptName, "Response")}
t = typeCustomName{Type: t, name: compoundTypeName(capitalize(g.Prefix), fe.TypeScriptName, "Response")}
default:
panic(
fmt.Sprintf(

View File

@ -22,6 +22,7 @@ import (
const dateLayout = "2006-01-02T15:04:05.999Z"
var ErrDocsAPI = errs.Class("example docs api")
var ErrUsersAPI = errs.Class("example users api")
type DocumentsService interface {
Get(ctx context.Context) ([]struct {
@ -47,6 +48,14 @@ type DocumentsService interface {
}, api.HTTPError)
}
type UsersService interface {
Get(ctx context.Context) ([]struct {
Name string "json:\"name\""
Surname string "json:\"surname\""
Email string "json:\"email\""
}, api.HTTPError)
}
// DocumentsHandler is an api handler that implements all Documents API endpoints functionality.
type DocumentsHandler struct {
log *zap.Logger
@ -55,6 +64,14 @@ type DocumentsHandler struct {
auth api.Auth
}
// UsersHandler is an api handler that implements all Users API endpoints functionality.
type UsersHandler struct {
log *zap.Logger
mon *monkit.Scope
service UsersService
auth api.Auth
}
func NewDocuments(log *zap.Logger, mon *monkit.Scope, service DocumentsService, router *mux.Router, auth api.Auth) *DocumentsHandler {
handler := &DocumentsHandler{
log: log,
@ -73,6 +90,20 @@ func NewDocuments(log *zap.Logger, mon *monkit.Scope, service DocumentsService,
return handler
}
func NewUsers(log *zap.Logger, mon *monkit.Scope, service UsersService, router *mux.Router, auth api.Auth) *UsersHandler {
handler := &UsersHandler{
log: log,
mon: mon,
service: service,
auth: auth,
}
usersRouter := router.PathPrefix("/api/v0/users").Subrouter()
usersRouter.HandleFunc("/", handler.handleGet).Methods("GET")
return handler
}
func (h *DocumentsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
@ -264,3 +295,29 @@ func (h *DocumentsHandler) handleUpdateContent(w http.ResponseWriter, r *http.Re
h.log.Debug("failed to write json UpdateContent response", zap.Error(ErrDocsAPI.Wrap(err)))
}
}
func (h *UsersHandler) handleGet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer h.mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
h.auth.RemoveAuthCookie(w)
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
retVal, httpErr := h.service.Get(ctx)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json Get response", zap.Error(ErrUsersAPI.Wrap(err)))
}
}

View File

@ -12,6 +12,8 @@
* [Get a tag](#documents-get-a-tag)
* [Get Version](#documents-get-version)
* [Update Content](#documents-update-content)
* Users
* [Get Users](#users-get-users)
<h3 id='documents-get-documents'>Get Documents (<a href='#list-of-endpoints'>go to full list</a>)</h3>
@ -162,3 +164,23 @@ Update the content of the document with the specified path and ID if the last up
```
<h3 id='users-get-users'>Get Users (<a href='#list-of-endpoints'>go to full list</a>)</h3>
Get the list of registered users
`GET /api/v0/users/`
**Response body:**
```typescript
[
{
name: string
surname: string
email: string
}
]
```

View File

@ -4,6 +4,30 @@
import { HttpClient } from '@/utils/httpClient';
import { Time, UUID } from '@/types/common';
export class DocsGetResponseItem {
id: UUID;
path: string;
date: Time;
metadata: Metadata;
last_retrievals?: DocsGetResponseItemLastRetrievals;
}
export class DocsGetResponseItemLastRetrievalsItem {
user: string;
when: Time;
}
export class DocsUpdateContentRequest {
content: string;
}
export class DocsUpdateContentResponse {
id: UUID;
date: Time;
pathParam: string;
body: string;
}
export class Document {
id: UUID;
date: Time;
@ -17,48 +41,32 @@ export class Metadata {
tags?: string[][];
}
export class UsersGetResponseItem {
name: string;
surname: string;
email: string;
}
export class Version {
date: Time;
number: number;
}
export class GetResponseItem {
id: UUID;
path: string;
date: Time;
metadata: Metadata;
last_retrievals?: GetResponseItemLastRetrievals;
}
export type DocsGetResponse = Array<DocsGetResponseItem>
export class GetResponseItemLastRetrievalsItem {
user: string;
when: Time;
}
export type DocsGetResponseItemLastRetrievals = Array<DocsGetResponseItemLastRetrievalsItem>
export class UpdateContentRequest {
content: string;
}
export class UpdateContentResponse {
id: UUID;
date: Time;
pathParam: string;
body: string;
}
export type GetResponse = Array<GetResponseItem>
export type GetResponseItemLastRetrievals = Array<GetResponseItemLastRetrievalsItem>
export type UsersGetResponse = Array<UsersGetResponseItem>
export class DocumentsHttpApiV0 {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/docs';
public async get(): Promise<GetResponse> {
public async get(): Promise<DocsGetResponse> {
const fullPath = `${this.ROOT_PATH}/`;
const response = await this.http.get(fullPath);
if (response.ok) {
return response.json().then((body) => body as GetResponse);
return response.json().then((body) => body as DocsGetResponse);
}
const err = await response.json();
throw new Error(err.error);
@ -94,14 +102,29 @@ export class DocumentsHttpApiV0 {
throw new Error(err.error);
}
public async updateContent(request: UpdateContentRequest, path: string, id: UUID, date: Time): Promise<UpdateContentResponse> {
public async updateContent(request: DocsUpdateContentRequest, path: string, id: UUID, date: Time): Promise<DocsUpdateContentResponse> {
const u = new URL(`${this.ROOT_PATH}/${path}`);
u.searchParams.set('id', id);
u.searchParams.set('date', date);
const fullPath = u.toString();
const response = await this.http.post(fullPath, JSON.stringify(request));
if (response.ok) {
return response.json().then((body) => body as UpdateContentResponse);
return response.json().then((body) => body as DocsUpdateContentResponse);
}
const err = await response.json();
throw new Error(err.error);
}
}
export class UsersHttpApiV0 {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/users';
public async get(): Promise<UsersGetResponse> {
const fullPath = `${this.ROOT_PATH}/`;
const response = await this.http.get(fullPath);
if (response.ok) {
return response.json().then((body) => body as UsersGetResponse);
}
const err = await response.json();
throw new Error(err.error);

View File

@ -94,6 +94,20 @@ func main() {
},
})
g = a.Group("Users", "users")
g.Get("/", &apigen.Endpoint{
Name: "Get Users",
Description: "Get the list of registered users",
GoName: "Get",
TypeScriptName: "get",
Response: []struct {
Name string `json:"name"`
Surname string `json:"surname"`
Email string `json:"email"`
}{},
})
a.MustWriteGo("api.gen.go")
a.MustWriteTS("client-api.gen.ts")
a.MustWriteDocs("apidocs.gen.md")

View File

@ -68,10 +68,10 @@ func (f *tsGenFile) registerTypes() {
for _, group := range f.api.EndpointGroups {
for _, method := range group.endpoints {
if method.Request != nil {
f.types.Register(method.requestType())
f.types.Register(method.requestType(group))
}
if method.Response != nil {
f.types.Register(method.responseType())
f.types.Register(method.responseType(group))
}
if len(method.QueryParams) > 0 {
for _, p := range method.QueryParams {
@ -91,12 +91,12 @@ func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
for _, method := range group.endpoints {
f.pf("")
funcArgs, path := f.getArgsAndPath(method)
funcArgs, path := f.getArgsAndPath(method, group)
returnStmt := "return"
returnType := "void"
if method.Response != nil {
respType := method.responseType()
respType := method.responseType(group)
returnType = TypescriptTypeName(respType)
returnStmt += fmt.Sprintf(" response.json().then((body) => body as %s)", returnType)
}
@ -129,7 +129,7 @@ func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
f.pf("}")
}
func (f *tsGenFile) getArgsAndPath(method *fullEndpoint) (funcArgs, path string) {
func (f *tsGenFile) getArgsAndPath(method *fullEndpoint, group *EndpointGroup) (funcArgs, path string) {
// remove path parameter placeholders
path = method.Path
i := strings.Index(path, "{")
@ -139,7 +139,7 @@ func (f *tsGenFile) getArgsAndPath(method *fullEndpoint) (funcArgs, path string)
path = "${this.ROOT_PATH}" + path
if method.Request != nil {
funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(method.requestType()))
funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(method.requestType(group)))
}
for _, p := range method.PathParams {