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:
Ivan Fraixedes 2023-09-25 18:43:30 +02:00
parent bde3e48842
commit 9d7ef17a26
No known key found for this signature in database
GPG Key ID: FB6101AFB5CB5AD5
14 changed files with 572 additions and 122 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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",

View File

@ -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"`
}

View File

@ -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",

View File

@ -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 {

View File

@ -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))
}
}

View File

@ -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)
})

View File

@ -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>

View File

@ -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{},
})

View File

@ -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);
}