From 9d7ef17a2652dce4e33a0dbb2062b2be3e829db6 Mon Sep 17 00:00:00 2001 From: Ivan Fraixedes Date: Mon, 25 Sep 2023 18:43:30 +0200 Subject: [PATCH] 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 --- private/apigen/common.go | 41 +++-- private/apigen/endpoint.go | 44 ++++- private/apigen/example/api.gen.go | 111 +++++++++++++ private/apigen/example/apidocs.gen.md | 91 +++++++++- private/apigen/example/client-api.gen.ts | 59 ++++++- private/apigen/example/gen.go | 40 ++++- private/apigen/example/myapi/types.go | 14 +- private/apigen/gogen_test.go | 43 ++++- private/apigen/tsgen.go | 11 +- private/apigen/tstypes.go | 155 ++++++++++++------ private/apigen/tstypes_test.go | 14 +- .../consoleweb/consoleapi/apidocs.gen.md | 21 ++- .../console/consoleweb/consoleapi/gen/main.go | 2 +- web/satellite/src/api/v0.gen.ts | 48 +++--- 14 files changed, 572 insertions(+), 122 deletions(-) diff --git a/private/apigen/common.go b/private/apigen/common.go index 58f4542f7..3f76d7236 100644 --- a/private/apigen/common.go +++ b/private/apigen/common.go @@ -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 +} diff --git a/private/apigen/endpoint.go b/private/apigen/endpoint.go index ae80e162c..7853ec208 100644 --- a/private/apigen/endpoint.go +++ b/private/apigen/endpoint.go @@ -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 diff --git a/private/apigen/example/api.gen.go b/private/apigen/example/api.gen.go index e876f1e1b..d6f5866b2 100644 --- a/private/apigen/example/api.gen.go +++ b/private/apigen/example/api.gen.go @@ -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 diff --git a/private/apigen/example/apidocs.gen.md b/private/apigen/example/apidocs.gen.md index 45b173ab4..22d9ac873 100644 --- a/private/apigen/example/apidocs.gen.md +++ b/private/apigen/example/apidocs.gen.md @@ -7,12 +7,51 @@

List of Endpoints

* 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) +

Get Documents (go to full list)

+ +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` + } + + ] + + } + +] + +``` +

Get One (go to full list)

-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 + } + } ``` +

Get a tag (go to full list)

+ +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 +``` + +

Get Version (go to full list)

+ +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 + } + +] + +``` +

Update Content (go to full list)

Update the content of the document with the specified path and ID if the last update is before the indicated date diff --git a/private/apigen/example/client-api.gen.ts b/private/apigen/example/client-api.gen.ts index b414c9f63..5b64536e5 100644 --- a/private/apigen/example/client-api.gen.ts +++ b/private/apigen/example/client-api.gen.ts @@ -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 + +export type GetResponseItemLastretrievals = Array + export class docsHttpApiV0 { private readonly http: HttpClient = new HttpClient(); private readonly ROOT_PATH: string = '/api/v0/docs'; + public async Get(): Promise { + 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 { 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 { + 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 { + 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 { const u = new URL(`${this.ROOT_PATH}/${path}`); u.searchParams.set('id', id); diff --git a/private/apigen/example/gen.go b/private/apigen/example/gen.go index 05dcd4f97..41e32b11a 100644 --- a/private/apigen/example/gen.go +++ b/private/apigen/example/gen.go @@ -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", diff --git a/private/apigen/example/myapi/types.go b/private/apigen/example/myapi/types.go index ab0e2479e..15d000340 100644 --- a/private/apigen/example/myapi/types.go +++ b/private/apigen/example/myapi/types.go @@ -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"` } diff --git a/private/apigen/gogen_test.go b/private/apigen/gogen_test.go index f7d698880..97bfe4297 100644 --- a/private/apigen/gogen_test.go +++ b/private/apigen/gogen_test.go @@ -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", diff --git a/private/apigen/tsgen.go b/private/apigen/tsgen.go index 46eae5504..eda9472f6 100644 --- a/private/apigen/tsgen.go +++ b/private/apigen/tsgen.go @@ -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 { diff --git a/private/apigen/tstypes.go b/private/apigen/tstypes.go index 2d4a899a5..fa1d94fa4 100644 --- a/private/apigen/tstypes.go +++ b/private/apigen/tstypes.go @@ -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)) } } diff --git a/private/apigen/tstypes_test.go b/private/apigen/tstypes_test.go index 9eec19a1f..7a4124f32 100644 --- a/private/apigen/tstypes_test.go +++ b/private/apigen/tstypes_test.go @@ -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) }) diff --git a/satellite/console/consoleweb/consoleapi/apidocs.gen.md b/satellite/console/consoleweb/consoleapi/apidocs.gen.md index 8a1a193d4..b35d4ca18 100644 --- a/satellite/console/consoleweb/consoleapi/apidocs.gen.md +++ b/satellite/console/consoleweb/consoleapi/apidocs.gen.md @@ -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 +} + ```

Update Project (go to full list)

diff --git a/satellite/console/consoleweb/consoleapi/gen/main.go b/satellite/console/consoleweb/consoleapi/gen/main.go index d43b153d3..9453dea29 100644 --- a/satellite/console/consoleweb/consoleapi/gen/main.go +++ b/satellite/console/consoleweb/consoleapi/gen/main.go @@ -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{}, }) diff --git a/web/satellite/src/api/v0.gen.ts b/web/satellite/src/api/v0.gen.ts index dceb20b10..711dab8e4 100644 --- a/web/satellite/src/api/v0.gen.ts +++ b/web/satellite/src/api/v0.gen.ts @@ -99,8 +99,8 @@ export class projectsHttpApiV0 { private readonly ROOT_PATH: string = '/api/v0/projects'; public async createProject(request: UpsertProjectInfo): Promise { - 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 { - 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 { - 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> { - const path = `${this.ROOT_PATH}/`; - const response = await this.http.get(path); + public async getProjects(): Promise { + const fullPath = `${this.ROOT_PATH}/`; + const response = await this.http.get(fullPath); if (response.ok) { - return response.json().then((body) => body as Array); + 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> { + public async getBucketRollups(projectID: UUID, since: Time, before: Time): Promise { 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); + 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 { - 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 { - 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 { - 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); }