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
This commit is contained in:
Ivan Fraixedes 2023-11-10 18:31:11 +01:00 committed by Storj Robot
parent 418673f7a2
commit e67691d51c
11 changed files with 162 additions and 398 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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