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:
parent
3193ff9155
commit
99c4359062
@ -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,
|
||||
|
@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user