private/apigen: Fix code generation for slices & arrays
Fix the API generator to generate valid TypeScript code when using slices an arrays of any type (base types, struct types, anonymous struct types, etc.). Closes https://github.com/storj/storj/issues/6323 Change-Id: I580ae5305c58f65c2e4f4a35d14ca4ee509a9250
This commit is contained in:
parent
bde3e48842
commit
9d7ef17a26
@ -8,6 +8,7 @@ import (
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
@ -109,17 +110,6 @@ func getElementaryType(t reflect.Type) reflect.Type {
|
||||
}
|
||||
}
|
||||
|
||||
// filter returns a new slice of reflect.Type values that satisfy the given keep function.
|
||||
func filter(types []reflect.Type, keep func(reflect.Type) bool) []reflect.Type {
|
||||
filtered := make([]reflect.Type, 0, len(types))
|
||||
for _, t := range types {
|
||||
if keep(t) {
|
||||
filtered = append(filtered, t)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// isNillableType returns whether instances of the given type can be nil.
|
||||
func isNillableType(t reflect.Type) bool {
|
||||
switch t.Kind() {
|
||||
@ -140,3 +130,32 @@ func compoundTypeName(base string, parts ...string) string {
|
||||
|
||||
return base + strings.Join(titled, "")
|
||||
}
|
||||
|
||||
type typeAndName struct {
|
||||
Type reflect.Type
|
||||
Name string
|
||||
}
|
||||
|
||||
func mapToSlice(typesAndNames map[reflect.Type]string) []typeAndName {
|
||||
list := make([]typeAndName, 0, len(typesAndNames))
|
||||
for t, n := range typesAndNames {
|
||||
list = append(list, typeAndName{Type: t, Name: n})
|
||||
}
|
||||
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return list[i].Name < list[j].Name
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// filter returns a new slice of typeAndName values that satisfy the given keep function.
|
||||
func filter(types []typeAndName, keep func(typeAndName) bool) []typeAndName {
|
||||
filtered := make([]typeAndName, 0, len(types))
|
||||
for _, t := range types {
|
||||
if keep(t) {
|
||||
filtered = append(filtered, t)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
@ -60,13 +60,29 @@ type fullEndpoint struct {
|
||||
// requestType guarantees to return a named Go type associated to the Endpoint.Request field.
|
||||
func (fe fullEndpoint) requestType() reflect.Type {
|
||||
t := reflect.TypeOf(fe.Request)
|
||||
if t.Name() == "" {
|
||||
name := fe.RequestName
|
||||
if name == "" {
|
||||
name = fe.MethodName
|
||||
}
|
||||
if t.Name() != "" {
|
||||
return t
|
||||
}
|
||||
|
||||
name := fe.RequestName
|
||||
if name == "" {
|
||||
name = fe.MethodName
|
||||
}
|
||||
|
||||
switch k := t.Kind(); k {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if t.Elem().Name() == "" {
|
||||
t = typeCustomName{Type: t, name: compoundTypeName(name, "Request")}
|
||||
}
|
||||
case reflect.Struct:
|
||||
t = typeCustomName{Type: t, name: compoundTypeName(name, "Request")}
|
||||
default:
|
||||
panic(
|
||||
fmt.Sprintf(
|
||||
"BUG: Unsupported Request type. Endpoint.Method=%q, Endpoint.Path=%q, found type=%q",
|
||||
fe.Method, fe.Path, k,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return t
|
||||
@ -75,8 +91,24 @@ 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 {
|
||||
t := reflect.TypeOf(fe.Response)
|
||||
if t.Name() == "" {
|
||||
if t.Name() != "" {
|
||||
return t
|
||||
}
|
||||
|
||||
switch k := t.Kind(); k {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if t.Elem().Name() == "" {
|
||||
t = typeCustomName{Type: t, name: compoundTypeName(fe.MethodName, "Response")}
|
||||
}
|
||||
case reflect.Struct:
|
||||
t = typeCustomName{Type: t, name: compoundTypeName(fe.MethodName, "Response")}
|
||||
default:
|
||||
panic(
|
||||
fmt.Sprintf(
|
||||
"BUG: Unsupported Response type. Endpoint.Method=%q, Endpoint.Path=%q, found type=%q",
|
||||
fe.Method, fe.Path, k,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return t
|
||||
|
@ -24,7 +24,19 @@ const dateLayout = "2006-01-02T15:04:05.999Z"
|
||||
var ErrDocsAPI = errs.Class("example docs api")
|
||||
|
||||
type DocumentsService interface {
|
||||
Get(ctx context.Context) ([]struct {
|
||||
ID uuid.UUID "json:\"id\""
|
||||
Path string "json:\"path\""
|
||||
Date time.Time "json:\"date\""
|
||||
Metadata myapi.Metadata "json:\"metadata\""
|
||||
LastRetrievals []struct {
|
||||
User string "json:\"user\""
|
||||
When time.Time "json:\"when\""
|
||||
} "json:\"last_retrievals\""
|
||||
}, api.HTTPError)
|
||||
GetOne(ctx context.Context, path string) (*myapi.Document, api.HTTPError)
|
||||
GetTag(ctx context.Context, path, tagName string) (*[2]string, api.HTTPError)
|
||||
GetVersions(ctx context.Context, path string) ([]myapi.Version, api.HTTPError)
|
||||
UpdateContent(ctx context.Context, path string, id uuid.UUID, date time.Time, request struct {
|
||||
Content string "json:\"content\""
|
||||
}) (*struct {
|
||||
@ -52,12 +64,41 @@ func NewDocuments(log *zap.Logger, mon *monkit.Scope, service DocumentsService,
|
||||
}
|
||||
|
||||
docsRouter := router.PathPrefix("/api/v0/docs").Subrouter()
|
||||
docsRouter.HandleFunc("/", handler.handleGet).Methods("GET")
|
||||
docsRouter.HandleFunc("/{path}", handler.handleGetOne).Methods("GET")
|
||||
docsRouter.HandleFunc("/{path}/tag/{tagName}", handler.handleGetTag).Methods("GET")
|
||||
docsRouter.HandleFunc("/{path}/versions", handler.handleGetVersions).Methods("GET")
|
||||
docsRouter.HandleFunc("/{path}", handler.handleUpdateContent).Methods("POST")
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (h *DocumentsHandler) 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(ErrDocsAPI.Wrap(err)))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DocumentsHandler) handleGetOne(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
@ -90,6 +131,76 @@ func (h *DocumentsHandler) handleGetOne(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DocumentsHandler) handleGetTag(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")
|
||||
|
||||
path, ok := mux.Vars(r)["path"]
|
||||
if !ok {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing path route param"))
|
||||
return
|
||||
}
|
||||
|
||||
tagName, ok := mux.Vars(r)["tagName"]
|
||||
if !ok {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing tagName route param"))
|
||||
return
|
||||
}
|
||||
|
||||
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.GetTag(ctx, path, tagName)
|
||||
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 GetTag response", zap.Error(ErrDocsAPI.Wrap(err)))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DocumentsHandler) handleGetVersions(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")
|
||||
|
||||
path, ok := mux.Vars(r)["path"]
|
||||
if !ok {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing path route param"))
|
||||
return
|
||||
}
|
||||
|
||||
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.GetVersions(ctx, path)
|
||||
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 GetVersions response", zap.Error(ErrDocsAPI.Wrap(err)))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DocumentsHandler) handleUpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
|
@ -7,12 +7,51 @@
|
||||
<h2 id='list-of-endpoints'>List of Endpoints</h2>
|
||||
|
||||
* Documents
|
||||
* [Get Documents](#documents-get-documents)
|
||||
* [Get One](#documents-get-one)
|
||||
* [Get a tag](#documents-get-a-tag)
|
||||
* [Get Version](#documents-get-version)
|
||||
* [Update Content](#documents-update-content)
|
||||
|
||||
<h3 id='documents-get-documents'>Get Documents (<a href='#list-of-endpoints'>go to full list</a>)</h3>
|
||||
|
||||
Get the paths to all the documents under the specified paths
|
||||
|
||||
`GET /api/v0/docs/`
|
||||
|
||||
**Response body:**
|
||||
|
||||
```typescript
|
||||
[
|
||||
{
|
||||
id: string // UUID formatted as `00000000-0000-0000-0000-000000000000`
|
||||
path: string
|
||||
date: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
|
||||
metadata: {
|
||||
owner: string
|
||||
tags: [
|
||||
unknown
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
last_retrievals: [
|
||||
{
|
||||
user: string
|
||||
when: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
<h3 id='documents-get-one'>Get One (<a href='#list-of-endpoints'>go to full list</a>)</h3>
|
||||
|
||||
Get one document with the specified version
|
||||
Get the document in the specified path
|
||||
|
||||
`GET /api/v0/docs/{path}`
|
||||
|
||||
@ -30,11 +69,59 @@ Get one document with the specified version
|
||||
date: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
|
||||
pathParam: string
|
||||
body: string
|
||||
version: number
|
||||
version: {
|
||||
date: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
|
||||
number: number
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<h3 id='documents-get-a-tag'>Get a tag (<a href='#list-of-endpoints'>go to full list</a>)</h3>
|
||||
|
||||
Get the tag of the document in the specified path and tag label
|
||||
|
||||
`GET /api/v0/docs/{path}/tag/{tagName}`
|
||||
|
||||
**Path Params:**
|
||||
|
||||
| name | type | elaboration |
|
||||
|---|---|---|
|
||||
| `path` | `string` | |
|
||||
| `tagName` | `string` | |
|
||||
|
||||
**Response body:**
|
||||
|
||||
```typescript
|
||||
unknown
|
||||
```
|
||||
|
||||
<h3 id='documents-get-version'>Get Version (<a href='#list-of-endpoints'>go to full list</a>)</h3>
|
||||
|
||||
Get all the version of the document in the specified path
|
||||
|
||||
`GET /api/v0/docs/{path}/versions`
|
||||
|
||||
**Path Params:**
|
||||
|
||||
| name | type | elaboration |
|
||||
|---|---|---|
|
||||
| `path` | `string` | |
|
||||
|
||||
**Response body:**
|
||||
|
||||
```typescript
|
||||
[
|
||||
{
|
||||
date: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
|
||||
number: number
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
<h3 id='documents-update-content'>Update Content (<a href='#list-of-endpoints'>go to full list</a>)</h3>
|
||||
|
||||
Update the content of the document with the specified path and ID if the last update is before the indicated date
|
||||
|
@ -9,7 +9,25 @@ export class Document {
|
||||
date: Time;
|
||||
pathParam: string;
|
||||
body: string;
|
||||
version: number;
|
||||
version: Version;
|
||||
}
|
||||
|
||||
export class GetResponseItem {
|
||||
id: UUID;
|
||||
path: string;
|
||||
date: Time;
|
||||
metadata: Metadata;
|
||||
last_retrievals: GetResponseItemLastretrievals;
|
||||
}
|
||||
|
||||
export class GetResponseItemLastretrievalsItem {
|
||||
user: string;
|
||||
when: Time;
|
||||
}
|
||||
|
||||
export class Metadata {
|
||||
owner: string;
|
||||
tags: string[][];
|
||||
}
|
||||
|
||||
export class UpdateContentRequest {
|
||||
@ -23,10 +41,29 @@ export class UpdateContentResponse {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export class Version {
|
||||
date: Time;
|
||||
number: number;
|
||||
}
|
||||
|
||||
export type GetResponse = Array<GetResponseItem>
|
||||
|
||||
export type GetResponseItemLastretrievals = Array<GetResponseItemLastretrievalsItem>
|
||||
|
||||
export class docsHttpApiV0 {
|
||||
private readonly http: HttpClient = new HttpClient();
|
||||
private readonly ROOT_PATH: string = '/api/v0/docs';
|
||||
|
||||
public async Get(): Promise<GetResponse> {
|
||||
const fullPath = `${this.ROOT_PATH}/`;
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as GetResponse);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new Error(err.error);
|
||||
}
|
||||
|
||||
public async GetOne(path: string): Promise<Document> {
|
||||
const fullPath = `${this.ROOT_PATH}/${path}`;
|
||||
const response = await this.http.get(fullPath);
|
||||
@ -37,6 +74,26 @@ export class docsHttpApiV0 {
|
||||
throw new Error(err.error);
|
||||
}
|
||||
|
||||
public async GetTag(path: string, tagName: string): Promise<string[]> {
|
||||
const fullPath = `${this.ROOT_PATH}/${path}/${tagName}`;
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as string[]);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new Error(err.error);
|
||||
}
|
||||
|
||||
public async GetVersions(path: string): Promise<Version[]> {
|
||||
const fullPath = `${this.ROOT_PATH}/${path}`;
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as Version[]);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new Error(err.error);
|
||||
}
|
||||
|
||||
public async UpdateContent(request: UpdateContentRequest, path: string, id: UUID, date: Time): Promise<UpdateContentResponse> {
|
||||
const u = new URL(`${this.ROOT_PATH}/${path}`);
|
||||
u.searchParams.set('id', id);
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
|
||||
"storj.io/storj/private/apigen"
|
||||
"storj.io/storj/private/apigen/example/myapi"
|
||||
)
|
||||
@ -19,9 +20,25 @@ func main() {
|
||||
|
||||
g := a.Group("Documents", "docs")
|
||||
|
||||
g.Get("/", &apigen.Endpoint{
|
||||
Name: "Get Documents",
|
||||
Description: "Get the paths to all the documents under the specified paths",
|
||||
MethodName: "Get",
|
||||
Response: []struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Date time.Time `json:"date"`
|
||||
Metadata myapi.Metadata `json:"metadata"`
|
||||
LastRetrievals []struct {
|
||||
User string `json:"user"`
|
||||
When time.Time `json:"when"`
|
||||
} `json:"last_retrievals"`
|
||||
}{},
|
||||
})
|
||||
|
||||
g.Get("/{path}", &apigen.Endpoint{
|
||||
Name: "Get One",
|
||||
Description: "Get one document with the specified version",
|
||||
Description: "Get the document in the specified path",
|
||||
MethodName: "GetOne",
|
||||
Response: myapi.Document{},
|
||||
PathParams: []apigen.Param{
|
||||
@ -29,6 +46,27 @@ func main() {
|
||||
},
|
||||
})
|
||||
|
||||
g.Get("/{path}/tag/{tagName}", &apigen.Endpoint{
|
||||
Name: "Get a tag",
|
||||
Description: "Get the tag of the document in the specified path and tag label ",
|
||||
MethodName: "GetTag",
|
||||
Response: [2]string{},
|
||||
PathParams: []apigen.Param{
|
||||
apigen.NewParam("path", ""),
|
||||
apigen.NewParam("tagName", ""),
|
||||
},
|
||||
})
|
||||
|
||||
g.Get("/{path}/versions", &apigen.Endpoint{
|
||||
Name: "Get Version",
|
||||
Description: "Get all the version of the document in the specified path",
|
||||
MethodName: "GetVersions",
|
||||
Response: []myapi.Version{},
|
||||
PathParams: []apigen.Param{
|
||||
apigen.NewParam("path", ""),
|
||||
},
|
||||
})
|
||||
|
||||
g.Post("/{path}", &apigen.Endpoint{
|
||||
Name: "Update Content",
|
||||
Description: "Update the content of the document with the specified path and ID if the last update is before the indicated date",
|
||||
|
@ -15,5 +15,17 @@ type Document struct {
|
||||
Date time.Time `json:"date"`
|
||||
PathParam string `json:"pathParam"`
|
||||
Body string `json:"body"`
|
||||
Version uint `json:"version"`
|
||||
Version Version `json:"version"`
|
||||
}
|
||||
|
||||
// Version is document version.
|
||||
type Version struct {
|
||||
Date time.Time `json:"date"`
|
||||
Number uint `json:"number"`
|
||||
}
|
||||
|
||||
// Metadata is metadata associated to a document.
|
||||
type Metadata struct {
|
||||
Owner string `json:"owner"`
|
||||
Tags [][2]string `json:"tags"`
|
||||
}
|
||||
|
@ -29,9 +29,19 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
auth struct{}
|
||||
service struct{}
|
||||
response = struct {
|
||||
auth struct{}
|
||||
service struct{}
|
||||
responseGet = struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Date time.Time `json:"date"`
|
||||
Metadata myapi.Metadata `json:"metadata"`
|
||||
LastRetrievals []struct {
|
||||
User string `json:"user"`
|
||||
When time.Time `json:"when"`
|
||||
} `json:"last_retrievals"`
|
||||
}
|
||||
responseUpdateContent = struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Date time.Time `json:"date"`
|
||||
PathParam string `json:"pathParam"`
|
||||
@ -45,6 +55,12 @@ func (a auth) IsAuthenticated(ctx context.Context, r *http.Request, isCookieAuth
|
||||
|
||||
func (a auth) RemoveAuthCookie(w http.ResponseWriter) {}
|
||||
|
||||
func (s service) Get(
|
||||
ctx context.Context,
|
||||
) ([]responseGet, api.HTTPError) {
|
||||
return []responseGet{}, api.HTTPError{}
|
||||
}
|
||||
|
||||
func (s service) GetOne(
|
||||
ctx context.Context,
|
||||
pathParam string,
|
||||
@ -52,6 +68,21 @@ func (s service) GetOne(
|
||||
return &myapi.Document{}, api.HTTPError{}
|
||||
}
|
||||
|
||||
func (s service) GetTag(
|
||||
ctx context.Context,
|
||||
pathParam string,
|
||||
tagName string,
|
||||
) (*[2]string, api.HTTPError) {
|
||||
return &[2]string{}, api.HTTPError{}
|
||||
}
|
||||
|
||||
func (s service) GetVersions(
|
||||
ctx context.Context,
|
||||
pathParam string,
|
||||
) ([]myapi.Version, api.HTTPError) {
|
||||
return []myapi.Version{}, api.HTTPError{}
|
||||
}
|
||||
|
||||
func (s service) UpdateContent(
|
||||
ctx context.Context,
|
||||
pathParam string,
|
||||
@ -60,8 +91,8 @@ func (s service) UpdateContent(
|
||||
body struct {
|
||||
Content string `json:"content"`
|
||||
},
|
||||
) (*response, api.HTTPError) {
|
||||
return &response{
|
||||
) (*responseUpdateContent, api.HTTPError) {
|
||||
return &responseUpdateContent{
|
||||
ID: id,
|
||||
Date: date,
|
||||
PathParam: pathParam,
|
||||
@ -114,7 +145,7 @@ func TestAPIServer(t *testing.T) {
|
||||
id, err := uuid.New()
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := response{
|
||||
expected := responseUpdateContent{
|
||||
ID: id,
|
||||
Date: time.Now(),
|
||||
PathParam: "foo",
|
||||
|
@ -6,7 +6,6 @@ package apigen
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
@ -98,11 +97,7 @@ func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
||||
returnType := "void"
|
||||
if method.Response != nil {
|
||||
respType := method.responseType()
|
||||
returnType = TypescriptTypeName(getElementaryType(respType))
|
||||
// TODO: see if this is needed after we are creating types for array and slices
|
||||
if respType.Kind() == reflect.Array || respType.Kind() == reflect.Slice {
|
||||
returnType = fmt.Sprintf("Array<%s>", returnType)
|
||||
}
|
||||
returnType = TypescriptTypeName(respType)
|
||||
returnStmt += fmt.Sprintf(" response.json().then((body) => body as %s)", returnType)
|
||||
}
|
||||
returnStmt += ";"
|
||||
@ -149,9 +144,7 @@ func (f *tsGenFile) getArgsAndPath(method *fullEndpoint) (funcArgs, path string)
|
||||
path = "${this.ROOT_PATH}" + path
|
||||
|
||||
if method.Request != nil {
|
||||
// TODO: This should map slices and arrays because a request could be one of them.
|
||||
t := getElementaryType(method.requestType())
|
||||
funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(t))
|
||||
funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(method.requestType()))
|
||||
}
|
||||
|
||||
for _, p := range method.PathParams {
|
||||
|
@ -37,55 +37,82 @@ type Types struct {
|
||||
// Register registers a type for generation.
|
||||
func (types *Types) Register(t reflect.Type) {
|
||||
if t.Name() == "" {
|
||||
panic("register an anonymous type is not supported. All the types must have a name")
|
||||
switch t.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.Ptr:
|
||||
if t.Elem().Name() == "" {
|
||||
panic(
|
||||
fmt.Sprintf("register an %q of elements of an anonymous type is not supported", t.Name()),
|
||||
)
|
||||
}
|
||||
default:
|
||||
panic("register an anonymous type is not supported. All the types must have a name")
|
||||
}
|
||||
}
|
||||
types.top[t] = struct{}{}
|
||||
}
|
||||
|
||||
// All returns a slice containing every top-level type and their dependencies.
|
||||
// All returns a map containing every top-level and their dependency types with their associated name.
|
||||
//
|
||||
// TODO: see how to have a better implementation for adding to seen, uniqueNames, and all.
|
||||
func (types *Types) All() []reflect.Type {
|
||||
seen := map[reflect.Type]struct{}{}
|
||||
func (types *Types) All() map[reflect.Type]string {
|
||||
all := map[reflect.Type]string{}
|
||||
uniqueNames := map[string]struct{}{}
|
||||
all := []reflect.Type{}
|
||||
|
||||
var walk func(t reflect.Type, alternateTypeName string)
|
||||
walk = func(t reflect.Type, altTypeName string) {
|
||||
if _, ok := seen[t]; ok {
|
||||
if _, ok := all[t]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Type isn't seen it but it has the same name than a seen it one.
|
||||
// This cannot be because we would generate more than one TypeScript type with the same name.
|
||||
if _, ok := uniqueNames[t.Name()]; ok {
|
||||
panic(fmt.Sprintf("Found different types with the same name (%s)", t.Name()))
|
||||
if t.Name() != "" {
|
||||
// Type isn't seen it but it has the same name than a seen it one.
|
||||
// This cannot be because we would generate more than one TypeScript type with the same name.
|
||||
if _, ok := uniqueNames[t.Name()]; ok {
|
||||
panic(fmt.Sprintf("Found different types with the same name (%s)", t.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := commonClasses[t]; ok {
|
||||
seen[t] = struct{}{}
|
||||
uniqueNames[t.Name()] = struct{}{}
|
||||
all = append(all, t)
|
||||
if n, ok := commonClasses[t]; ok {
|
||||
all[t] = n
|
||||
uniqueNames[n] = struct{}{}
|
||||
return
|
||||
}
|
||||
|
||||
switch k := t.Kind(); k {
|
||||
// TODO: Does reflect.Ptr to be registered?, I believe that could skip it and only register
|
||||
// the type that points to.
|
||||
case reflect.Array, reflect.Ptr, reflect.Slice:
|
||||
t = typeCustomName{Type: t, name: compoundTypeName(altTypeName, k.String())}
|
||||
seen[t] = struct{}{}
|
||||
uniqueNames[t.Name()] = struct{}{}
|
||||
all = append(all, t)
|
||||
case reflect.Ptr:
|
||||
walk(t.Elem(), altTypeName)
|
||||
case reflect.Array, reflect.Slice:
|
||||
// If element type has a TypeScript name then an array of the element type will be defined
|
||||
// otherwise we have to create a compound type.
|
||||
if tsen := TypescriptTypeName(t.Elem()); tsen == "" {
|
||||
if altTypeName == "" {
|
||||
panic(
|
||||
fmt.Sprintf(
|
||||
"BUG: found a %q with elements of an anonymous type and without an alternative name. Found type=%q",
|
||||
t.Kind(),
|
||||
t,
|
||||
))
|
||||
}
|
||||
all[t] = altTypeName
|
||||
uniqueNames[altTypeName] = struct{}{}
|
||||
walk(t.Elem(), compoundTypeName(altTypeName, "item"))
|
||||
}
|
||||
case reflect.Struct:
|
||||
if t.Name() == "" {
|
||||
t = typeCustomName{Type: t, name: altTypeName}
|
||||
n := t.Name()
|
||||
if n == "" {
|
||||
if altTypeName == "" {
|
||||
panic(
|
||||
fmt.Sprintf(
|
||||
"BUG: found an anonymous 'struct' and without an alternative name; an alternative name is required. Found type=%q",
|
||||
t,
|
||||
))
|
||||
}
|
||||
|
||||
n = altTypeName
|
||||
}
|
||||
|
||||
seen[t] = struct{}{}
|
||||
uniqueNames[t.Name()] = struct{}{}
|
||||
all = append(all, t)
|
||||
all[t] = n
|
||||
uniqueNames[n] = struct{}{}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
@ -96,11 +123,10 @@ func (types *Types) All() []reflect.Type {
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64,
|
||||
reflect.String:
|
||||
seen[t] = struct{}{}
|
||||
all[t] = t.Name()
|
||||
uniqueNames[t.Name()] = struct{}{}
|
||||
all = append(all, t)
|
||||
default:
|
||||
panic(fmt.Sprintf("type '%s' is not supported", t.Kind().String()))
|
||||
panic(fmt.Sprintf("type %q is not supported", t.Kind().String()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,10 +134,6 @@ func (types *Types) All() []reflect.Type {
|
||||
walk(t, t.Name())
|
||||
}
|
||||
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return strings.Compare(all[i].Name(), all[j].Name()) < 0
|
||||
})
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
@ -122,27 +144,28 @@ func (types *Types) GenerateTypescriptDefinitions() string {
|
||||
|
||||
pf(types.getTypescriptImports())
|
||||
|
||||
all := filter(types.All(), func(t reflect.Type) bool {
|
||||
if _, ok := commonClasses[t]; ok {
|
||||
allTypes := types.All()
|
||||
namedTypes := mapToSlice(allTypes)
|
||||
allStructs := filter(namedTypes, func(tn typeAndName) bool {
|
||||
if _, ok := commonClasses[tn.Type]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO, we should be able to handle arrays and slices as defined types now
|
||||
return t.Kind() == reflect.Struct
|
||||
return tn.Type.Kind() == reflect.Struct
|
||||
})
|
||||
|
||||
for _, t := range all {
|
||||
for _, t := range allStructs {
|
||||
func() {
|
||||
pf("\nexport class %s {", t.Name())
|
||||
pf("\nexport class %s {", t.Name)
|
||||
defer pf("}")
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
for i := 0; i < t.Type.NumField(); i++ {
|
||||
field := t.Type.Field(i)
|
||||
attributes := strings.Fields(field.Tag.Get("json"))
|
||||
if len(attributes) == 0 || attributes[0] == "" {
|
||||
pathParts := strings.Split(t.PkgPath(), "/")
|
||||
pathParts := strings.Split(t.Type.PkgPath(), "/")
|
||||
pkg := pathParts[len(pathParts)-1]
|
||||
panic(fmt.Sprintf("(%s.%s).%s missing json declaration", pkg, t.Name(), field.Name))
|
||||
panic(fmt.Sprintf("(%s.%s).%s missing json declaration", pkg, t.Name, field.Name))
|
||||
}
|
||||
|
||||
jsonField := attributes[0]
|
||||
@ -151,15 +174,41 @@ func (types *Types) GenerateTypescriptDefinitions() string {
|
||||
}
|
||||
|
||||
isOptional := ""
|
||||
if isNillableType(t) {
|
||||
if isNillableType(t.Type) {
|
||||
isOptional = "?"
|
||||
}
|
||||
|
||||
pf("\t%s%s: %s;", jsonField, isOptional, TypescriptTypeName(field.Type))
|
||||
if field.Type.Name() != "" {
|
||||
pf("\t%s%s: %s;", jsonField, isOptional, TypescriptTypeName(field.Type))
|
||||
} else {
|
||||
typeName := allTypes[field.Type]
|
||||
pf("\t%s%s: %s;", jsonField, isOptional, TypescriptTypeName(typeCustomName{Type: field.Type, name: typeName}))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
allArraySlices := filter(namedTypes, func(t typeAndName) bool {
|
||||
if _, ok := commonClasses[t.Type]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
switch t.Type.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
for _, t := range allArraySlices {
|
||||
elemTypeName, ok := allTypes[t.Type.Elem()]
|
||||
if !ok {
|
||||
panic("BUG: the element types of an Slice or Array isn't in the all types map")
|
||||
}
|
||||
pf("\nexport type %s = Array<%s>", t.Name, elemTypeName)
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
@ -167,8 +216,7 @@ func (types *Types) GenerateTypescriptDefinitions() string {
|
||||
func (types *Types) getTypescriptImports() string {
|
||||
classes := []string{}
|
||||
|
||||
all := types.All()
|
||||
for _, t := range all {
|
||||
for t := range types.All() {
|
||||
if tsClass, ok := commonClasses[t]; ok {
|
||||
classes = append(classes, tsClass)
|
||||
}
|
||||
@ -195,15 +243,18 @@ func TypescriptTypeName(t reflect.Type) string {
|
||||
switch t.Kind() {
|
||||
case reflect.Ptr:
|
||||
return TypescriptTypeName(t.Elem())
|
||||
case reflect.Slice:
|
||||
case reflect.Array, reflect.Slice:
|
||||
if t.Name() != "" {
|
||||
return t.Name()
|
||||
}
|
||||
|
||||
// []byte ([]uint8) is marshaled as a base64 string
|
||||
elem := t.Elem()
|
||||
if elem.Kind() == reflect.Uint8 {
|
||||
return "string"
|
||||
}
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
return TypescriptTypeName(t.Elem()) + "[]"
|
||||
|
||||
return TypescriptTypeName(elem) + "[]"
|
||||
case reflect.String:
|
||||
return "string"
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
@ -217,6 +268,6 @@ func TypescriptTypeName(t reflect.Type) string {
|
||||
case reflect.Struct:
|
||||
return t.Name()
|
||||
default:
|
||||
panic("unhandled type: " + t.Name())
|
||||
panic(fmt.Sprintf(`unhandled type. Type="%+v"`, t))
|
||||
}
|
||||
}
|
||||
|
@ -15,14 +15,14 @@ type testTypesValoration struct {
|
||||
}
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
t.Run("Register panics with anonymous types", func(t *testing.T) {
|
||||
t.Run("Register panics with some anonymous types", func(t *testing.T) {
|
||||
types := NewTypes()
|
||||
require.Panics(t, func() {
|
||||
types.Register(reflect.TypeOf([2]int{}))
|
||||
types.Register(reflect.TypeOf([2]struct{}{}))
|
||||
}, "array")
|
||||
|
||||
require.Panics(t, func() {
|
||||
types.Register(reflect.TypeOf([]float64{}))
|
||||
types.Register(reflect.TypeOf([]struct{}{}))
|
||||
}, "slice")
|
||||
|
||||
require.Panics(t, func() {
|
||||
@ -79,16 +79,16 @@ func TestTypes(t *testing.T) {
|
||||
require.Len(t, allTypes, 9, "total number of types")
|
||||
|
||||
typesNames := []string{}
|
||||
for _, tp := range allTypes {
|
||||
typesNames = append(typesNames, tp.Name())
|
||||
for _, name := range allTypes {
|
||||
typesNames = append(typesNames, name)
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, []string{
|
||||
"string", "uint",
|
||||
"Response",
|
||||
"ResponseAddressesSlice", "ResponseAddresses",
|
||||
"ResponseAddresses", "ResponseAddressesItem",
|
||||
"ResponseJob",
|
||||
"ResponseDocumentsSlice", "ResponseDocuments", "testTypesValoration",
|
||||
"ResponseDocuments", "ResponseDocumentsItem", "testTypesValoration",
|
||||
}, typesNames)
|
||||
})
|
||||
|
||||
|
@ -42,7 +42,26 @@ Creates new Project with given info
|
||||
**Response body:**
|
||||
|
||||
```typescript
|
||||
unknown
|
||||
{
|
||||
id: string // UUID formatted as `00000000-0000-0000-0000-000000000000`
|
||||
publicId: string // UUID formatted as `00000000-0000-0000-0000-000000000000`
|
||||
name: string
|
||||
description: string
|
||||
userAgent: string
|
||||
ownerId: string // UUID formatted as `00000000-0000-0000-0000-000000000000`
|
||||
rateLimit: number
|
||||
burstLimit: number
|
||||
maxBuckets: number
|
||||
createdAt: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
|
||||
memberCount: number
|
||||
storageLimit: string // Amount of memory formatted as `15 GB`
|
||||
bandwidthLimit: string // Amount of memory formatted as `15 GB`
|
||||
userSpecifiedStorageLimit: string // Amount of memory formatted as `15 GB`
|
||||
userSpecifiedBandwidthLimit: string // Amount of memory formatted as `15 GB`
|
||||
segmentLimit: number
|
||||
defaultPlacement: number
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<h3 id='projectmanagement-update-project'>Update Project (<a href='#list-of-endpoints'>go to full list</a>)</h3>
|
||||
|
@ -35,7 +35,7 @@ func main() {
|
||||
Description: "Creates new Project with given info",
|
||||
MethodName: "GenCreateProject",
|
||||
RequestName: "createProject",
|
||||
Response: &console.Project{},
|
||||
Response: console.Project{},
|
||||
Request: console.UpsertProjectInfo{},
|
||||
})
|
||||
|
||||
|
@ -99,8 +99,8 @@ export class projectsHttpApiV0 {
|
||||
private readonly ROOT_PATH: string = '/api/v0/projects';
|
||||
|
||||
public async createProject(request: UpsertProjectInfo): Promise<Project> {
|
||||
const path = `${this.ROOT_PATH}/create`;
|
||||
const response = await this.http.post(path, JSON.stringify(request));
|
||||
const fullPath = `${this.ROOT_PATH}/create`;
|
||||
const response = await this.http.post(fullPath, JSON.stringify(request));
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as Project);
|
||||
}
|
||||
@ -109,8 +109,8 @@ export class projectsHttpApiV0 {
|
||||
}
|
||||
|
||||
public async updateProject(request: UpsertProjectInfo, id: UUID): Promise<Project> {
|
||||
const path = `${this.ROOT_PATH}/update/${id}`;
|
||||
const response = await this.http.patch(path, JSON.stringify(request));
|
||||
const fullPath = `${this.ROOT_PATH}/update/${id}`;
|
||||
const response = await this.http.patch(fullPath, JSON.stringify(request));
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as Project);
|
||||
}
|
||||
@ -119,8 +119,8 @@ export class projectsHttpApiV0 {
|
||||
}
|
||||
|
||||
public async deleteProject(id: UUID): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/delete/${id}`;
|
||||
const response = await this.http.delete(path);
|
||||
const fullPath = `${this.ROOT_PATH}/delete/${id}`;
|
||||
const response = await this.http.delete(fullPath);
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
@ -128,11 +128,11 @@ export class projectsHttpApiV0 {
|
||||
throw new Error(err.error);
|
||||
}
|
||||
|
||||
public async getProjects(): Promise<Array<Project>> {
|
||||
const path = `${this.ROOT_PATH}/`;
|
||||
const response = await this.http.get(path);
|
||||
public async getProjects(): Promise<Project[]> {
|
||||
const fullPath = `${this.ROOT_PATH}/`;
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as Array<Project>);
|
||||
return response.json().then((body) => body as Project[]);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new Error(err.error);
|
||||
@ -144,8 +144,8 @@ export class projectsHttpApiV0 {
|
||||
u.searchParams.set('bucket', bucket);
|
||||
u.searchParams.set('since', since);
|
||||
u.searchParams.set('before', before);
|
||||
const path = u.toString();
|
||||
const response = await this.http.get(path);
|
||||
const fullPath = u.toString();
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as BucketUsageRollup);
|
||||
}
|
||||
@ -153,15 +153,15 @@ export class projectsHttpApiV0 {
|
||||
throw new Error(err.error);
|
||||
}
|
||||
|
||||
public async getBucketRollups(projectID: UUID, since: Time, before: Time): Promise<Array<BucketUsageRollup>> {
|
||||
public async getBucketRollups(projectID: UUID, since: Time, before: Time): Promise<BucketUsageRollup[]> {
|
||||
const u = new URL(`${this.ROOT_PATH}/bucket-rollups`);
|
||||
u.searchParams.set('projectID', projectID);
|
||||
u.searchParams.set('since', since);
|
||||
u.searchParams.set('before', before);
|
||||
const path = u.toString();
|
||||
const response = await this.http.get(path);
|
||||
const fullPath = u.toString();
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as Array<BucketUsageRollup>);
|
||||
return response.json().then((body) => body as BucketUsageRollup[]);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new Error(err.error);
|
||||
@ -174,8 +174,8 @@ export class projectsHttpApiV0 {
|
||||
u.searchParams.set('page', page);
|
||||
u.searchParams.set('order', order);
|
||||
u.searchParams.set('orderDirection', orderDirection);
|
||||
const path = u.toString();
|
||||
const response = await this.http.get(path);
|
||||
const fullPath = u.toString();
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as APIKeyPage);
|
||||
}
|
||||
@ -189,8 +189,8 @@ export class apikeysHttpApiV0 {
|
||||
private readonly ROOT_PATH: string = '/api/v0/apikeys';
|
||||
|
||||
public async createAPIKey(request: CreateAPIKeyRequest): Promise<CreateAPIKeyResponse> {
|
||||
const path = `${this.ROOT_PATH}/create`;
|
||||
const response = await this.http.post(path, JSON.stringify(request));
|
||||
const fullPath = `${this.ROOT_PATH}/create`;
|
||||
const response = await this.http.post(fullPath, JSON.stringify(request));
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as CreateAPIKeyResponse);
|
||||
}
|
||||
@ -199,8 +199,8 @@ export class apikeysHttpApiV0 {
|
||||
}
|
||||
|
||||
public async deleteAPIKey(id: UUID): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/delete/${id}`;
|
||||
const response = await this.http.delete(path);
|
||||
const fullPath = `${this.ROOT_PATH}/delete/${id}`;
|
||||
const response = await this.http.delete(fullPath);
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
@ -214,8 +214,8 @@ export class usersHttpApiV0 {
|
||||
private readonly ROOT_PATH: string = '/api/v0/users';
|
||||
|
||||
public async getUser(): Promise<ResponseUser> {
|
||||
const path = `${this.ROOT_PATH}/`;
|
||||
const response = await this.http.get(path);
|
||||
const fullPath = `${this.ROOT_PATH}/`;
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as ResponseUser);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user