From e67691d51c39840eefa52e42ae450f4fbaa53458 Mon Sep 17 00:00:00 2001 From: Ivan Fraixedes Date: Fri, 10 Nov 2023 18:31:11 +0100 Subject: [PATCH] private/apigen: Remove support for anonymous types I made several commits to add support to the API generator for anonymous types because the example was using them. Later, I was told that we don't need to support them, so the example should have never used anonymous types. Supporting anonymous types has added complexity to the API generator and we are finding bugs in corner cases whose fixes lead to adding more complexity and some fixes doesn't seem to have a neat solution without doing a big refactoring. We decided to reduce the side effects that supporting anonymous type has brought just removing it. This commit remove the support for anonymous types and reflect that in the example. Change-Id: I2f8c87a0db0e229971ab1bef46cca16fee924191 --- private/apigen/common.go | 22 ----- private/apigen/endpoint.go | 119 +++++++++----------------- private/apigen/example/api.gen.go | 42 ++------- private/apigen/example/apidocs.gen.md | 37 ++++++-- private/apigen/example/gen.go | 69 +++------------ private/apigen/example/myapi/types.go | 13 +++ private/apigen/gogen_test.go | 56 +++++------- private/apigen/tsgen.go | 16 ++-- private/apigen/tsgenmock.go | 4 +- private/apigen/tstypes.go | 93 +++----------------- private/apigen/tstypes_test.go | 89 ++++--------------- 11 files changed, 162 insertions(+), 398 deletions(-) diff --git a/private/apigen/common.go b/private/apigen/common.go index 7d2c2ff21..7cdffe356 100644 --- a/private/apigen/common.go +++ b/private/apigen/common.go @@ -102,17 +102,6 @@ func (s *StringBuilder) Writelnf(format string, a ...interface{}) { s.WriteString(fmt.Sprintf(format+"\n", a...)) } -// typeCustomName is a reflect.Type with a customized type's name. -type typeCustomName struct { - reflect.Type - - name string -} - -func (t typeCustomName) Name() string { - return t.name -} - // getElementaryType simplifies a Go type. func getElementaryType(t reflect.Type) reflect.Type { switch t.Kind() { @@ -132,17 +121,6 @@ func isNillableType(t reflect.Type) bool { return false } -// compoundTypeName create a name composed with base and parts, by joining base as it's and -// capitalizing each part. base is not altered. -func compoundTypeName(base string, parts ...string) string { - titled := make([]string, len(parts)) - for i := 0; i < len(parts); i++ { - titled[i] = capitalize(parts[i]) - } - - return base + strings.Join(titled, "") -} - func capitalize(s string) string { r, size := utf8.DecodeRuneInString(s) if size <= 0 { diff --git a/private/apigen/endpoint.go b/private/apigen/endpoint.go index 8f4ca8dda..ddb344a2b 100644 --- a/private/apigen/endpoint.go +++ b/private/apigen/endpoint.go @@ -24,6 +24,11 @@ var ( ) // Endpoint represents endpoint's configuration. +// +// Passing an anonymous type to the fields that define the request or response will make the API +// generator to panic. Anonymous types aren't allowed such as named structs that have fields with +// direct or indirect of anonymous types, slices or arrays whose direct or indirect elements are of +// anonymous types. type Endpoint struct { // Name is a free text used to name the endpoint for documentation purpose. // It cannot be empty. @@ -72,24 +77,30 @@ func (e *Endpoint) APIAuth() bool { // Validate validates the endpoint fields values are correct according to the documented constraints. func (e *Endpoint) Validate() error { + newErr := func(m string, a ...any) error { + e := fmt.Sprintf(". Endpoint: %s", e.Name) + m += e + return errsEndpoint.New(m, a...) + } + if e.Name == "" { - return errsEndpoint.New("Name cannot be empty") + return newErr("Name cannot be empty") } if e.Description == "" { - return errsEndpoint.New("Description cannot be empty") + return newErr("Description cannot be empty") } if !goNameRegExp.MatchString(e.GoName) { - return errsEndpoint.New("GoName doesn't match the regular expression %q", goNameRegExp) + return newErr("GoName doesn't match the regular expression %q", goNameRegExp) } if !typeScriptNameRegExp.MatchString(e.TypeScriptName) { - return errsEndpoint.New("TypeScriptName doesn't match the regular expression %q", typeScriptNameRegExp) + return newErr("TypeScriptName doesn't match the regular expression %q", typeScriptNameRegExp) } if e.Request != nil { - switch k := reflect.TypeOf(e.Request).Kind(); k { + switch t := reflect.TypeOf(e.Request); t.Kind() { case reflect.Invalid, reflect.Complex64, reflect.Complex128, @@ -99,12 +110,20 @@ func (e *Endpoint) Validate() error { reflect.Map, reflect.Pointer, reflect.UnsafePointer: - return errsEndpoint.New("Request cannot be of a type %q", k) + return newErr("Request cannot be of a type %q", t.Kind()) + case reflect.Array, reflect.Slice: + if t.Elem().Name() == "" { + return newErr("Request cannot be of %q of anonymous struct elements", t.Kind()) + } + case reflect.Struct: + if t.Name() == "" { + return newErr("Request cannot be of an anonymous struct") + } } } if e.Response != nil { - switch k := reflect.TypeOf(e.Response).Kind(); k { + switch t := reflect.TypeOf(e.Response); t.Kind() { case reflect.Invalid, reflect.Complex64, reflect.Complex128, @@ -114,12 +133,20 @@ func (e *Endpoint) Validate() error { reflect.Map, reflect.Pointer, reflect.UnsafePointer: - return errsEndpoint.New("Response cannot be of a type %q", k) + return newErr("Response cannot be of a type %q", t.Kind()) + case reflect.Array, reflect.Slice: + if t.Elem().Name() == "" { + return newErr("Response cannot be of %q of anonymous struct elements", t.Kind()) + } + case reflect.Struct: + if t.Name() == "" { + return newErr("Response cannot be of an anonymous struct") + } } if e.ResponseMock != nil { if m, r := reflect.TypeOf(e.ResponseMock), reflect.TypeOf(e.Response); m != r { - return errsEndpoint.New( + return newErr( "ResponseMock isn't of the same type than Response. Have=%q Want=%q", m, r, ) } @@ -136,62 +163,6 @@ type fullEndpoint struct { Method string } -// requestType guarantees to return a named Go type associated to the Endpoint.Request field. -// g is used to avoid clashes with types defined in different groups that are different, but with -// the same name. It cannot be nil. -func (fe fullEndpoint) requestType(g *EndpointGroup) reflect.Type { - t := reflect.TypeOf(fe.Request) - if t.Name() != "" { - return t - } - - switch k := t.Kind(); k { - case reflect.Array, reflect.Slice: - if t.Elem().Name() == "" { - t = typeCustomName{Type: t, name: compoundTypeName(capitalize(g.Prefix), fe.TypeScriptName, "Request")} - } - case reflect.Struct: - t = typeCustomName{Type: t, name: compoundTypeName(capitalize(g.Prefix), fe.TypeScriptName, "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 -} - -// responseType guarantees to return a named Go type associated to the Endpoint.Response field. -// g is used to avoid clashes with types defined in different groups that are different, but with -// the same name. It cannot be nil. -func (fe fullEndpoint) responseType(g *EndpointGroup) reflect.Type { - t := reflect.TypeOf(fe.Response) - if t.Name() != "" { - return t - } - - switch k := t.Kind(); k { - case reflect.Array, reflect.Slice: - if t.Elem().Name() == "" { - t = typeCustomName{Type: t, name: compoundTypeName(capitalize(g.Prefix), fe.TypeScriptName, "Response")} - } - case reflect.Struct: - t = typeCustomName{Type: t, name: compoundTypeName(capitalize(g.Prefix), fe.TypeScriptName, "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 -} - // EndpointGroup represents endpoints group. // You should always create a group using API.Group because it validates the field values to // guarantee correct code generation. @@ -295,7 +266,10 @@ type Param struct { Type reflect.Type } -// NewParam constructor which creates new Param entity by given name and type. +// NewParam constructor which creates new Param entity by given name and type through instance. +// +// instance can only be a unsigned integer (of any size), string, uuid.UUID or time.Time, otherwise +// it panics. func NewParam(name string, instance interface{}) Param { switch t := reflect.TypeOf(instance); t { case reflect.TypeOf(uuid.UUID{}), reflect.TypeOf(time.Time{}): @@ -320,16 +294,3 @@ func NewParam(name string, instance interface{}) Param { Type: reflect.TypeOf(instance), } } - -// namedType guarantees to return a named Go type. where defines where the param is defined (e.g. -// path, query, etc.). -func (p Param) namedType(ep Endpoint, where string) reflect.Type { - if p.Type.Name() == "" { - return typeCustomName{ - Type: p.Type, - name: compoundTypeName(ep.TypeScriptName, where, "param", p.Name), - } - } - - return p.Type -} diff --git a/private/apigen/example/api.gen.go b/private/apigen/example/api.gen.go index 94967baf2..a7258c6dd 100644 --- a/private/apigen/example/api.gen.go +++ b/private/apigen/example/api.gen.go @@ -25,40 +25,16 @@ var ErrDocsAPI = errs.Class("example docs api") var ErrUsersAPI = errs.Class("example users 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) + Get(ctx context.Context) ([]myapi.Document, 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 { - ID uuid.UUID "json:\"id\"" - Date time.Time "json:\"date\"" - PathParam string "json:\"pathParam\"" - Body string "json:\"body\"" - }, api.HTTPError) + UpdateContent(ctx context.Context, path string, id uuid.UUID, date time.Time, request myapi.NewDocument) (*myapi.Document, api.HTTPError) } type UsersService interface { - Get(ctx context.Context) ([]struct { - Name string "json:\"name\"" - Surname string "json:\"surname\"" - Email string "json:\"email\"" - }, api.HTTPError) - Create(ctx context.Context, request []struct { - Name string "json:\"name\"" - Surname string "json:\"surname\"" - Email string "json:\"email\"" - }) api.HTTPError + Get(ctx context.Context) ([]myapi.User, api.HTTPError) + Create(ctx context.Context, request []myapi.User) api.HTTPError } // DocumentsHandler is an api handler that implements all Documents API endpoints functionality. @@ -275,9 +251,7 @@ func (h *DocumentsHandler) handleUpdateContent(w http.ResponseWriter, r *http.Re return } - payload := struct { - Content string "json:\"content\"" - }{} + payload := myapi.NewDocument{} if err = json.NewDecoder(r.Body).Decode(&payload); err != nil { api.ServeError(h.log, w, http.StatusBadRequest, err) return @@ -335,11 +309,7 @@ func (h *UsersHandler) handleCreate(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - payload := []struct { - Name string "json:\"name\"" - Surname string "json:\"surname\"" - Email string "json:\"email\"" - }{} + payload := []myapi.User{} if err = json.NewDecoder(r.Body).Decode(&payload); err != nil { api.ServeError(h.log, w, http.StatusBadRequest, err) return diff --git a/private/apigen/example/apidocs.gen.md b/private/apigen/example/apidocs.gen.md index 625cbb81d..5685cd9ce 100644 --- a/private/apigen/example/apidocs.gen.md +++ b/private/apigen/example/apidocs.gen.md @@ -28,8 +28,14 @@ Get the paths to all the documents under the specified paths [ { id: string // UUID formatted as `00000000-0000-0000-0000-000000000000` - path: string date: string // Date timestamp formatted as `2006-01-02T15:00:00Z` + pathParam: string + body: string + version: { + date: string // Date timestamp formatted as `2006-01-02T15:00:00Z` + number: number + } + metadata: { owner: string tags: [ @@ -38,14 +44,6 @@ unknown } - last_retrievals: [ - { - user: string - when: string // Date timestamp formatted as `2006-01-02T15:00:00Z` - } - - ] - } ] @@ -77,6 +75,14 @@ Get the document in the specified path number: number } + metadata: { + owner: string + tags: [ +unknown + ] + + } + } ``` @@ -161,6 +167,19 @@ Update the content of the document with the specified path and ID if the last up date: string // Date timestamp formatted as `2006-01-02T15:00:00Z` pathParam: string body: string + version: { + date: string // Date timestamp formatted as `2006-01-02T15:00:00Z` + number: number + } + + metadata: { + owner: string + tags: [ +unknown + ] + + } + } ``` diff --git a/private/apigen/example/gen.go b/private/apigen/example/gen.go index 39345f3d0..501390ce0 100644 --- a/private/apigen/example/gen.go +++ b/private/apigen/example/gen.go @@ -31,39 +31,14 @@ func main() { Description: "Get the paths to all the documents under the specified paths", GoName: "Get", TypeScriptName: "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"` - }{}, - ResponseMock: []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"` - }{{ - ID: uuid.UUID{}, - Path: "/workspace/notes.md", + Response: []myapi.Document{}, + ResponseMock: []myapi.Document{{ + ID: uuid.UUID{}, + PathParam: "/workspace/notes.md", Metadata: myapi.Metadata{ Owner: "Storj", Tags: [][2]string{{"category", "general"}}, }, - LastRetrievals: []struct { - User string `json:"user"` - When time.Time `json:"when"` - }{{ - User: "Storj", - When: now.Add(-time.Hour), - }}, }}, }) @@ -121,15 +96,8 @@ func main() { Description: "Update the content of the document with the specified path and ID if the last update is before the indicated date", GoName: "UpdateContent", TypeScriptName: "updateContent", - Response: struct { - ID uuid.UUID `json:"id"` - Date time.Time `json:"date"` - PathParam string `json:"pathParam"` - Body string `json:"body"` - }{}, - Request: struct { - Content string `json:"content"` - }{}, + Response: myapi.Document{}, + Request: myapi.NewDocument{}, QueryParams: []apigen.Param{ apigen.NewParam("id", uuid.UUID{}), apigen.NewParam("date", time.Time{}), @@ -137,12 +105,7 @@ func main() { PathParams: []apigen.Param{ apigen.NewParam("path", ""), }, - ResponseMock: struct { - ID uuid.UUID `json:"id"` - Date time.Time `json:"date"` - PathParam string `json:"pathParam"` - Body string `json:"body"` - }{ + ResponseMock: myapi.Document{ ID: uuid.UUID{}, Date: now, PathParam: "ID", @@ -157,16 +120,8 @@ func main() { Description: "Get the list of registered users", GoName: "Get", TypeScriptName: "get", - Response: []struct { - Name string `json:"name"` - Surname string `json:"surname"` - Email string `json:"email"` - }{}, - ResponseMock: []struct { - Name string `json:"name"` - Surname string `json:"surname"` - Email string `json:"email"` - }{ + Response: []myapi.User{}, + ResponseMock: []myapi.User{ {Name: "Storj", Surname: "Labs", Email: "storj@storj.test"}, {Name: "Test1", Surname: "Testing", Email: "test1@example.test"}, {Name: "Test2", Surname: "Testing", Email: "test2@example.test"}, @@ -178,11 +133,7 @@ func main() { Description: "Create a user", GoName: "Create", TypeScriptName: "create", - Request: []struct { - Name string `json:"name"` - Surname string `json:"surname"` - Email string `json:"email"` - }{}, + Request: []myapi.User{}, }) a.MustWriteGo("api.gen.go") diff --git a/private/apigen/example/myapi/types.go b/private/apigen/example/myapi/types.go index 15d000340..65380dd7a 100644 --- a/private/apigen/example/myapi/types.go +++ b/private/apigen/example/myapi/types.go @@ -16,6 +16,7 @@ type Document struct { PathParam string `json:"pathParam"` Body string `json:"body"` Version Version `json:"version"` + Metadata Metadata `json:"metadata"` } // Version is document version. @@ -29,3 +30,15 @@ type Metadata struct { Owner string `json:"owner"` Tags [][2]string `json:"tags"` } + +// NewDocument contains the content the data to create a new document. +type NewDocument struct { + Content string `json:"content"` +} + +// User contains information of a user. +type User struct { + Name string `json:"name"` + Surname string `json:"surname"` + Email string `json:"email"` +} diff --git a/private/apigen/gogen_test.go b/private/apigen/gogen_test.go index 97bfe4297..ead389c36 100644 --- a/private/apigen/gogen_test.go +++ b/private/apigen/gogen_test.go @@ -29,24 +29,8 @@ import ( ) type ( - 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"` - Body string `json:"body"` - } + auth struct{} + service struct{} ) func (a auth) IsAuthenticated(ctx context.Context, r *http.Request, isCookieAuth, isKeyAuth bool) (context.Context, error) { @@ -57,8 +41,8 @@ func (a auth) RemoveAuthCookie(w http.ResponseWriter) {} func (s service) Get( ctx context.Context, -) ([]responseGet, api.HTTPError) { - return []responseGet{}, api.HTTPError{} +) ([]myapi.Document, api.HTTPError) { + return []myapi.Document{}, api.HTTPError{} } func (s service) GetOne( @@ -88,11 +72,9 @@ func (s service) UpdateContent( pathParam string, id uuid.UUID, date time.Time, - body struct { - Content string `json:"content"` - }, -) (*responseUpdateContent, api.HTTPError) { - return &responseUpdateContent{ + body myapi.NewDocument, +) (*myapi.Document, api.HTTPError) { + return &myapi.Document{ ID: id, Date: date, PathParam: pathParam, @@ -100,7 +82,9 @@ func (s service) UpdateContent( }, api.HTTPError{} } -func send(ctx context.Context, method string, url string, body interface{}) ([]byte, error) { +func send(ctx context.Context, t *testing.T, method string, url string, body interface{}) ([]byte, error) { + t.Helper() + var bodyReader io.Reader = http.NoBody if body != nil { bodyJSON, err := json.Marshal(body) @@ -120,6 +104,10 @@ func send(ctx context.Context, method string, url string, body interface{}) ([]b return nil, err } + if c := resp.StatusCode; c != http.StatusOK { + t.Fatalf("unexpected status code. Want=%d, Got=%d", http.StatusOK, c) + } + respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err @@ -145,14 +133,14 @@ func TestAPIServer(t *testing.T) { id, err := uuid.New() require.NoError(t, err) - expected := responseUpdateContent{ + expected := myapi.Document{ ID: id, Date: time.Now(), PathParam: "foo", Body: "bar", } - resp, err := send(ctx, http.MethodPost, + resp, err := send(ctx, t, http.MethodPost, fmt.Sprintf("%s/api/v0/docs/%s?id=%s&date=%s", server.URL, expected.PathParam, @@ -162,14 +150,16 @@ func TestAPIServer(t *testing.T) { ) require.NoError(t, err) - var actual map[string]string + fmt.Println(string(resp)) + + var actual map[string]any require.NoError(t, json.Unmarshal(resp, &actual)) for _, key := range []string{"id", "date", "pathParam", "body"} { require.Contains(t, actual, key) } - require.Equal(t, expected.ID.String(), actual["id"]) - require.Equal(t, expected.Date.Format(apigen.DateFormat), actual["date"]) - require.Equal(t, expected.PathParam, actual["pathParam"]) - require.Equal(t, expected.Body, actual["body"]) + require.Equal(t, expected.ID.String(), actual["id"].(string)) + require.Equal(t, expected.Date.Format(apigen.DateFormat), actual["date"].(string)) + require.Equal(t, expected.PathParam, actual["pathParam"].(string)) + require.Equal(t, expected.Body, actual["body"].(string)) } diff --git a/private/apigen/tsgen.go b/private/apigen/tsgen.go index 5770ebabb..ffcbc1366 100644 --- a/private/apigen/tsgen.go +++ b/private/apigen/tsgen.go @@ -6,6 +6,7 @@ package apigen import ( "fmt" "os" + "reflect" "strings" "github.com/zeebo/errs" @@ -79,14 +80,14 @@ func (f *tsGenFile) registerTypes() { for _, group := range f.api.EndpointGroups { for _, method := range group.endpoints { if method.Request != nil { - f.types.Register(method.requestType(group)) + f.types.Register(reflect.TypeOf(method.Request)) } if method.Response != nil { - f.types.Register(method.responseType(group)) + f.types.Register(reflect.TypeOf(method.Response)) } if len(method.QueryParams) > 0 { for _, p := range method.QueryParams { - t := getElementaryType(p.namedType(method.Endpoint, "query")) + t := getElementaryType(p.Type) f.types.Register(t) } } @@ -106,8 +107,7 @@ func (f *tsGenFile) createAPIClient(group *EndpointGroup) { returnStmt := "return" returnType := "void" if method.Response != nil { - respType := method.responseType(group) - returnType = TypescriptTypeName(respType) + returnType = TypescriptTypeName(reflect.TypeOf(method.Response)) returnStmt += fmt.Sprintf(" response.json().then((body) => body as %s)", returnType) } returnStmt += ";" @@ -149,16 +149,16 @@ func (f *tsGenFile) getArgsAndPath(method *fullEndpoint, group *EndpointGroup) ( path = "${this.ROOT_PATH}" + path if method.Request != nil { - funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(method.requestType(group))) + funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(reflect.TypeOf(method.Request))) } for _, p := range method.PathParams { - funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.namedType(method.Endpoint, "path"))) + funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type)) path += fmt.Sprintf("/${%s}", p.Name) } for _, p := range method.QueryParams { - funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.namedType(method.Endpoint, "query"))) + funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type)) } path = strings.ReplaceAll(path, "//", "/") diff --git a/private/apigen/tsgenmock.go b/private/apigen/tsgenmock.go index 84eb78244..987ecb116 100644 --- a/private/apigen/tsgenmock.go +++ b/private/apigen/tsgenmock.go @@ -6,6 +6,7 @@ package apigen import ( "encoding/json" "fmt" + "reflect" "strings" "github.com/zeebo/errs" @@ -98,8 +99,7 @@ func (f *tsGenMockFile) createAPIClient(group *EndpointGroup) { )) } - respType := method.responseType(group) - returnType = TypescriptTypeName(respType) + returnType = TypescriptTypeName(reflect.TypeOf(method.Response)) } f.pf("\tpublic async %s(%s): Promise<%s> {", method.TypeScriptName, funcArgs, returnType) diff --git a/private/apigen/tstypes.go b/private/apigen/tstypes.go index 442f66284..aae0f3020 100644 --- a/private/apigen/tstypes.go +++ b/private/apigen/tstypes.go @@ -52,73 +52,35 @@ func (types *Types) Register(t reflect.Type) { } // 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() map[reflect.Type]string { all := map[reflect.Type]string{} - uniqueNames := map[string]struct{}{} - var walk func(t reflect.Type, alternateTypeName string) - walk = func(t reflect.Type, altTypeName string) { + var walk func(t reflect.Type) + walk = func(t reflect.Type) { if _, ok := all[t]; ok { return } - 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 n, ok := commonClasses[t]; ok { all[t] = n - uniqueNames[n] = struct{}{} return } switch k := t.Kind(); k { case reflect.Ptr: - walk(t.Elem(), altTypeName) + walk(t.Elem()) 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. - elemTypeName := t.Elem().Name() - 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{}{} - elemTypeName = compoundTypeName(altTypeName, "item") - } - walk(t.Elem(), elemTypeName) + walk(t.Elem()) case reflect.Struct: - 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 + if t.Name() == "" { + panic(fmt.Sprintf("BUG: found an anonymous 'struct'. Found type=%q", t)) } - all[t] = n - uniqueNames[n] = struct{}{} + all[t] = t.Name() for i := 0; i < t.NumField(); i++ { field := t.Field(i) - walk(field.Type, compoundTypeName(altTypeName, field.Name)) + walk(field.Type) } case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, @@ -126,14 +88,13 @@ func (types *Types) All() map[reflect.Type]string { reflect.Float32, reflect.Float64, reflect.String: all[t] = t.Name() - uniqueNames[t.Name()] = struct{}{} default: panic(fmt.Sprintf("type %q is not supported", t.Kind().String())) } } for t := range types.top { - walk(t, t.Name()) + walk(t) } return all @@ -186,42 +147,11 @@ func (types *Types) GenerateTypescriptDefinitions() string { isOptional = "?" } - 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})) - } + pf("\t%s%s: %s;", jsonField, isOptional, TypescriptTypeName(field.Type)) } }() } - 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>", - TypescriptTypeName( - typeCustomName{Type: t.Type, name: t.Name}), - TypescriptTypeName(typeCustomName{Type: t.Type.Elem(), name: elemTypeName}), - ) - } - return out.String() } @@ -279,6 +209,9 @@ func TypescriptTypeName(t reflect.Type) string { case reflect.Bool: return "boolean" case reflect.Struct: + if t.Name() == "" { + panic(fmt.Sprintf(`anonymous struct aren't accepted because their type doesn't have a name. Type="%+v"`, t)) + } return capitalize(t.Name()) default: panic(fmt.Sprintf(`unhandled type. Type="%+v"`, t)) diff --git a/private/apigen/tstypes_test.go b/private/apigen/tstypes_test.go index b07dec2a7..f8fd639dd 100644 --- a/private/apigen/tstypes_test.go +++ b/private/apigen/tstypes_test.go @@ -51,80 +51,29 @@ func TestTypes(t *testing.T) { require.Subset(t, allTypes, typesList, "all types contains at least the registered ones") }) - t.Run("All nested structs and slices", func(t *testing.T) { - type Item struct{} - types := NewTypes() - types.Register( - typeCustomName{ - Type: reflect.TypeOf(struct { - Name string - Addresses []struct { - Address string - PO string - } - Job struct { - Company string - Position string - StartingYear uint - } - Documents []struct { - Path string - Content string - Valoration testTypesValoration - } - Items []Item - }{}), - name: "Response", - }) - - allTypes := types.All() - require.Len(t, allTypes, 10, "total number of types") - - typesNames := []string{} - for _, name := range allTypes { - typesNames = append(typesNames, name) + t.Run("Anonymous types panics", func(t *testing.T) { + type Address struct { + Address string + PO string + } + type Job struct { + Company string + Position string + StartingYear uint + ContractClauses []struct { // This is what it makes Types.All to panic + ClauseID uint + CauseDesc string + } } - require.ElementsMatch(t, []string{ - "string", "uint", - "Response", - "ResponseAddresses", "ResponseAddressesItem", - "ResponseJob", - "ResponseDocuments", "ResponseDocumentsItem", "testTypesValoration", - "Item", - }, typesNames) - }) + type Citizen struct { + Name string + Addresses []Address + Job Job + } - t.Run("All panic types without unique names", func(t *testing.T) { types := NewTypes() - types.Register(typeCustomName{ - Type: reflect.TypeOf(struct { - Name string - Addresses []struct { - Address string - PO string - } - Job struct { - Company string - Position string - StartingYear uint - } - Documents []struct { - Path string - Content string - Valoration testTypesValoration - } - }{}), - name: "Response", - }) - - types.Register(typeCustomName{ - Type: reflect.TypeOf(struct { - Reference string - }{}), - name: "Response", - }) - + types.Register(reflect.TypeOf(Citizen{})) require.Panics(t, func() { types.All() })