Skip to content

Commit

Permalink
private/apigen: Support Go generics (TypeScript)
Browse files Browse the repository at this point in the history
This commit fixes the TypeScript API generator to generate correct names
from Go structs that use generics.

Change-Id: Ic512b8ac3eae19fe2a27f7e9508ad679f86389c3
  • Loading branch information
ifraixedes authored and Storj Robot committed Dec 22, 2023
1 parent 177f7fe commit 1b9e6e4
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 2 deletions.
4 changes: 4 additions & 0 deletions private/apigen/common.go
Expand Up @@ -219,3 +219,7 @@ func parseJSONTag(structType reflect.Type, field reflect.StructField) (_ jsonTag

return info, true, nil
}

func typeNameWithoutGenerics(n string) string {
return strings.SplitN(n, "[", 2)[0]
}
21 changes: 21 additions & 0 deletions private/apigen/example/api.gen.go
Expand Up @@ -35,6 +35,7 @@ type DocumentsService interface {
type UsersService interface {
Get(ctx context.Context) ([]myapi.User, api.HTTPError)
Create(ctx context.Context, request []myapi.User) api.HTTPError
GetAge(ctx context.Context) (*myapi.UserAge[int16], api.HTTPError)
}

// DocumentsHandler is an api handler that implements all Documents API endpoints functionality.
Expand Down Expand Up @@ -80,6 +81,7 @@ func NewUsers(log *zap.Logger, mon *monkit.Scope, service UsersService, router *
usersRouter := router.PathPrefix("/api/v0/users").Subrouter()
usersRouter.HandleFunc("/", handler.handleGet).Methods("GET")
usersRouter.HandleFunc("/", handler.handleCreate).Methods("POST")
usersRouter.HandleFunc("/age", handler.handleGetAge).Methods("GET")

return handler
}
Expand Down Expand Up @@ -304,3 +306,22 @@ func (h *UsersHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
}
}

func (h *UsersHandler) handleGetAge(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")

retVal, httpErr := h.service.GetAge(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 GetAge response", zap.Error(ErrUsersAPI.Wrap(err)))
}
}
18 changes: 18 additions & 0 deletions private/apigen/example/apidocs.gen.md
Expand Up @@ -13,6 +13,7 @@
* Users
* [Get Users](#users-get-users)
* [Create Users](#users-create-users)
* [Get User's age](#users-get-users-age)

<h3 id='documents-get-documents'>Get Documents (<a href='#list-of-endpoints'>go to full list</a>)</h3>

Expand Down Expand Up @@ -226,3 +227,20 @@ Create users

```

<h3 id='users-get-users-age'>Get User's age (<a href='#list-of-endpoints'>go to full list</a>)</h3>

Get the user's age

`GET /api/v0/users/age`

**Response body:**

```typescript
{
day: number
month: number
year: number
}

```

14 changes: 14 additions & 0 deletions private/apigen/example/client-api-mock.gen.ts
Expand Up @@ -28,6 +28,12 @@ export class User {
position: string;
}

export class UserAge {
day: number;
month: number;
year: number;
}

export class Version {
date: Time;
number: number;
Expand Down Expand Up @@ -136,4 +142,12 @@ export class UsersHttpApiV0 {

return;
}

public async getAge(): Promise<UserAge> {
if (this.respStatusCode !== 0) {
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
}

return JSON.parse('{"day":1,"month":1,"year":2000}') as UserAge;
}
}
16 changes: 16 additions & 0 deletions private/apigen/example/client-api.gen.ts
Expand Up @@ -30,6 +30,12 @@ export class User {
position: string;
}

export class UserAge {
day: number;
month: number;
year: number;
}

export class Version {
date: Time;
number: number;
Expand Down Expand Up @@ -125,4 +131,14 @@ export class UsersHttpApiV0 {
const err = await response.json();
throw new APIError(err.error, response.status);
}

public async getAge(): Promise<UserAge> {
const fullPath = `${this.ROOT_PATH}/age`;
const response = await this.http.get(fullPath);
if (response.ok) {
return response.json().then((body) => body as UserAge);
}
const err = await response.json();
throw new APIError(err.error, response.status);
}
}
9 changes: 9 additions & 0 deletions private/apigen/example/gen.go
Expand Up @@ -159,6 +159,15 @@ func main() {
Request: []myapi.User{},
})

g.Get("/age", &apigen.Endpoint{
Name: "Get User's age",
Description: "Get the user's age",
GoName: "GetAge",
TypeScriptName: "getAge",
Response: myapi.UserAge[int16]{},
ResponseMock: myapi.UserAge[int16]{Day: 1, Month: 1, Year: 2000},
})

a.MustWriteGo("api.gen.go")
a.MustWriteTS("client-api.gen.ts")
a.MustWriteTSMock("client-api-mock.gen.ts")
Expand Down
11 changes: 11 additions & 0 deletions private/apigen/example/myapi/types.go
Expand Up @@ -49,3 +49,14 @@ type Professional struct {
Company string `json:"company"`
Position string `json:"position"`
}

// UserAge represents a user's age.
//
// The value is generic for being able to increase the year's size to afford to recompile the code
// when we have users that were born in a too far DC year or allow to register users in a few years
// in the future. JOKING, we need it for testing that the API generator works fine with them.
type UserAge[T ~int16 | int32 | int64] struct {
Day uint8 `json:"day"`
Month uint8 `json:"month"`
Year T `json:"year"`
}
4 changes: 2 additions & 2 deletions private/apigen/tstypes.go
Expand Up @@ -76,7 +76,7 @@ func (types *Types) All() map[reflect.Type]string {
panic(fmt.Sprintf("BUG: found an anonymous 'struct'. Found type=%q", t))
}

all[t] = t.Name()
all[t] = typeNameWithoutGenerics(t.Name())

for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
Expand Down Expand Up @@ -211,7 +211,7 @@ func TypescriptTypeName(t reflect.Type) string {
if t.Name() == "" {
panic(fmt.Sprintf(`anonymous struct aren't accepted because their type doesn't have a name. Type="%+v"`, t))
}
return capitalize(t.Name())
return capitalize(typeNameWithoutGenerics(t.Name()))
default:
panic(fmt.Sprintf(`unhandled type. Type="%+v"`, t))
}
Expand Down

0 comments on commit 1b9e6e4

Please sign in to comment.