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