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...)) 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. // getElementaryType simplifies a Go type.
func getElementaryType(t reflect.Type) reflect.Type { func getElementaryType(t reflect.Type) reflect.Type {
switch t.Kind() { switch t.Kind() {
@ -132,17 +121,6 @@ func isNillableType(t reflect.Type) bool {
return false 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 { func capitalize(s string) string {
r, size := utf8.DecodeRuneInString(s) r, size := utf8.DecodeRuneInString(s)
if size <= 0 { if size <= 0 {

View File

@ -24,6 +24,11 @@ var (
) )
// Endpoint represents endpoint's configuration. // 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 { type Endpoint struct {
// Name is a free text used to name the endpoint for documentation purpose. // Name is a free text used to name the endpoint for documentation purpose.
// It cannot be empty. // 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. // Validate validates the endpoint fields values are correct according to the documented constraints.
func (e *Endpoint) Validate() error { 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 == "" { if e.Name == "" {
return errsEndpoint.New("Name cannot be empty") return newErr("Name cannot be empty")
} }
if e.Description == "" { if e.Description == "" {
return errsEndpoint.New("Description cannot be empty") return newErr("Description cannot be empty")
} }
if !goNameRegExp.MatchString(e.GoName) { 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) { 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 { if e.Request != nil {
switch k := reflect.TypeOf(e.Request).Kind(); k { switch t := reflect.TypeOf(e.Request); t.Kind() {
case reflect.Invalid, case reflect.Invalid,
reflect.Complex64, reflect.Complex64,
reflect.Complex128, reflect.Complex128,
@ -99,12 +110,20 @@ func (e *Endpoint) Validate() error {
reflect.Map, reflect.Map,
reflect.Pointer, reflect.Pointer,
reflect.UnsafePointer: 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 { if e.Response != nil {
switch k := reflect.TypeOf(e.Response).Kind(); k { switch t := reflect.TypeOf(e.Response); t.Kind() {
case reflect.Invalid, case reflect.Invalid,
reflect.Complex64, reflect.Complex64,
reflect.Complex128, reflect.Complex128,
@ -114,12 +133,20 @@ func (e *Endpoint) Validate() error {
reflect.Map, reflect.Map,
reflect.Pointer, reflect.Pointer,
reflect.UnsafePointer: 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 e.ResponseMock != nil {
if m, r := reflect.TypeOf(e.ResponseMock), reflect.TypeOf(e.Response); m != r { 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, "ResponseMock isn't of the same type than Response. Have=%q Want=%q", m, r,
) )
} }
@ -136,62 +163,6 @@ type fullEndpoint struct {
Method string 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. // EndpointGroup represents endpoints group.
// You should always create a group using API.Group because it validates the field values to // You should always create a group using API.Group because it validates the field values to
// guarantee correct code generation. // guarantee correct code generation.
@ -295,7 +266,10 @@ type Param struct {
Type reflect.Type 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 { func NewParam(name string, instance interface{}) Param {
switch t := reflect.TypeOf(instance); t { switch t := reflect.TypeOf(instance); t {
case reflect.TypeOf(uuid.UUID{}), reflect.TypeOf(time.Time{}): case reflect.TypeOf(uuid.UUID{}), reflect.TypeOf(time.Time{}):
@ -320,16 +294,3 @@ func NewParam(name string, instance interface{}) Param {
Type: reflect.TypeOf(instance), 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") var ErrUsersAPI = errs.Class("example users api")
type DocumentsService interface { type DocumentsService interface {
Get(ctx context.Context) ([]struct { Get(ctx context.Context) ([]myapi.Document, api.HTTPError)
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) GetOne(ctx context.Context, path string) (*myapi.Document, api.HTTPError)
GetTag(ctx context.Context, path, tagName string) (*[2]string, api.HTTPError) GetTag(ctx context.Context, path, tagName string) (*[2]string, api.HTTPError)
GetVersions(ctx context.Context, path string) ([]myapi.Version, 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 { UpdateContent(ctx context.Context, path string, id uuid.UUID, date time.Time, request myapi.NewDocument) (*myapi.Document, api.HTTPError)
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)
} }
type UsersService interface { type UsersService interface {
Get(ctx context.Context) ([]struct { Get(ctx context.Context) ([]myapi.User, api.HTTPError)
Name string "json:\"name\"" Create(ctx context.Context, request []myapi.User) api.HTTPError
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
} }
// DocumentsHandler is an api handler that implements all Documents API endpoints functionality. // 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 return
} }
payload := struct { payload := myapi.NewDocument{}
Content string "json:\"content\""
}{}
if err = json.NewDecoder(r.Body).Decode(&payload); err != nil { if err = json.NewDecoder(r.Body).Decode(&payload); err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err) api.ServeError(h.log, w, http.StatusBadRequest, err)
return return
@ -335,11 +309,7 @@ func (h *UsersHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
payload := []struct { payload := []myapi.User{}
Name string "json:\"name\""
Surname string "json:\"surname\""
Email string "json:\"email\""
}{}
if err = json.NewDecoder(r.Body).Decode(&payload); err != nil { if err = json.NewDecoder(r.Body).Decode(&payload); err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err) api.ServeError(h.log, w, http.StatusBadRequest, err)
return 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` id: string // UUID formatted as `00000000-0000-0000-0000-000000000000`
path: string
date: string // Date timestamp formatted as `2006-01-02T15:00:00Z` 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: { metadata: {
owner: string owner: string
tags: [ 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 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` date: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
pathParam: string pathParam: string
body: 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", Description: "Get the paths to all the documents under the specified paths",
GoName: "Get", GoName: "Get",
TypeScriptName: "get", TypeScriptName: "get",
Response: []struct { Response: []myapi.Document{},
ID uuid.UUID `json:"id"` ResponseMock: []myapi.Document{{
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{}, ID: uuid.UUID{},
Path: "/workspace/notes.md", PathParam: "/workspace/notes.md",
Metadata: myapi.Metadata{ Metadata: myapi.Metadata{
Owner: "Storj", Owner: "Storj",
Tags: [][2]string{{"category", "general"}}, 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", Description: "Update the content of the document with the specified path and ID if the last update is before the indicated date",
GoName: "UpdateContent", GoName: "UpdateContent",
TypeScriptName: "updateContent", TypeScriptName: "updateContent",
Response: struct { Response: myapi.Document{},
ID uuid.UUID `json:"id"` Request: myapi.NewDocument{},
Date time.Time `json:"date"`
PathParam string `json:"pathParam"`
Body string `json:"body"`
}{},
Request: struct {
Content string `json:"content"`
}{},
QueryParams: []apigen.Param{ QueryParams: []apigen.Param{
apigen.NewParam("id", uuid.UUID{}), apigen.NewParam("id", uuid.UUID{}),
apigen.NewParam("date", time.Time{}), apigen.NewParam("date", time.Time{}),
@ -137,12 +105,7 @@ func main() {
PathParams: []apigen.Param{ PathParams: []apigen.Param{
apigen.NewParam("path", ""), apigen.NewParam("path", ""),
}, },
ResponseMock: struct { ResponseMock: myapi.Document{
ID uuid.UUID `json:"id"`
Date time.Time `json:"date"`
PathParam string `json:"pathParam"`
Body string `json:"body"`
}{
ID: uuid.UUID{}, ID: uuid.UUID{},
Date: now, Date: now,
PathParam: "ID", PathParam: "ID",
@ -157,16 +120,8 @@ func main() {
Description: "Get the list of registered users", Description: "Get the list of registered users",
GoName: "Get", GoName: "Get",
TypeScriptName: "get", TypeScriptName: "get",
Response: []struct { Response: []myapi.User{},
Name string `json:"name"` ResponseMock: []myapi.User{
Surname string `json:"surname"`
Email string `json:"email"`
}{},
ResponseMock: []struct {
Name string `json:"name"`
Surname string `json:"surname"`
Email string `json:"email"`
}{
{Name: "Storj", Surname: "Labs", Email: "storj@storj.test"}, {Name: "Storj", Surname: "Labs", Email: "storj@storj.test"},
{Name: "Test1", Surname: "Testing", Email: "test1@example.test"}, {Name: "Test1", Surname: "Testing", Email: "test1@example.test"},
{Name: "Test2", Surname: "Testing", Email: "test2@example.test"}, {Name: "Test2", Surname: "Testing", Email: "test2@example.test"},
@ -178,11 +133,7 @@ func main() {
Description: "Create a user", Description: "Create a user",
GoName: "Create", GoName: "Create",
TypeScriptName: "create", TypeScriptName: "create",
Request: []struct { Request: []myapi.User{},
Name string `json:"name"`
Surname string `json:"surname"`
Email string `json:"email"`
}{},
}) })
a.MustWriteGo("api.gen.go") a.MustWriteGo("api.gen.go")

View File

@ -16,6 +16,7 @@ type Document struct {
PathParam string `json:"pathParam"` PathParam string `json:"pathParam"`
Body string `json:"body"` Body string `json:"body"`
Version Version `json:"version"` Version Version `json:"version"`
Metadata Metadata `json:"metadata"`
} }
// Version is document version. // Version is document version.
@ -29,3 +30,15 @@ type Metadata struct {
Owner string `json:"owner"` Owner string `json:"owner"`
Tags [][2]string `json:"tags"` 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

@ -31,22 +31,6 @@ import (
type ( type (
auth struct{} auth struct{}
service 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"`
}
) )
func (a auth) IsAuthenticated(ctx context.Context, r *http.Request, isCookieAuth, isKeyAuth bool) (context.Context, error) { 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( func (s service) Get(
ctx context.Context, ctx context.Context,
) ([]responseGet, api.HTTPError) { ) ([]myapi.Document, api.HTTPError) {
return []responseGet{}, api.HTTPError{} return []myapi.Document{}, api.HTTPError{}
} }
func (s service) GetOne( func (s service) GetOne(
@ -88,11 +72,9 @@ func (s service) UpdateContent(
pathParam string, pathParam string,
id uuid.UUID, id uuid.UUID,
date time.Time, date time.Time,
body struct { body myapi.NewDocument,
Content string `json:"content"` ) (*myapi.Document, api.HTTPError) {
}, return &myapi.Document{
) (*responseUpdateContent, api.HTTPError) {
return &responseUpdateContent{
ID: id, ID: id,
Date: date, Date: date,
PathParam: pathParam, PathParam: pathParam,
@ -100,7 +82,9 @@ func (s service) UpdateContent(
}, api.HTTPError{} }, 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 var bodyReader io.Reader = http.NoBody
if body != nil { if body != nil {
bodyJSON, err := json.Marshal(body) bodyJSON, err := json.Marshal(body)
@ -120,6 +104,10 @@ func send(ctx context.Context, method string, url string, body interface{}) ([]b
return nil, err 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) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
@ -145,14 +133,14 @@ func TestAPIServer(t *testing.T) {
id, err := uuid.New() id, err := uuid.New()
require.NoError(t, err) require.NoError(t, err)
expected := responseUpdateContent{ expected := myapi.Document{
ID: id, ID: id,
Date: time.Now(), Date: time.Now(),
PathParam: "foo", PathParam: "foo",
Body: "bar", 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", fmt.Sprintf("%s/api/v0/docs/%s?id=%s&date=%s",
server.URL, server.URL,
expected.PathParam, expected.PathParam,
@ -162,14 +150,16 @@ func TestAPIServer(t *testing.T) {
) )
require.NoError(t, err) 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)) require.NoError(t, json.Unmarshal(resp, &actual))
for _, key := range []string{"id", "date", "pathParam", "body"} { for _, key := range []string{"id", "date", "pathParam", "body"} {
require.Contains(t, actual, key) require.Contains(t, actual, key)
} }
require.Equal(t, expected.ID.String(), actual["id"]) require.Equal(t, expected.ID.String(), actual["id"].(string))
require.Equal(t, expected.Date.Format(apigen.DateFormat), actual["date"]) require.Equal(t, expected.Date.Format(apigen.DateFormat), actual["date"].(string))
require.Equal(t, expected.PathParam, actual["pathParam"]) require.Equal(t, expected.PathParam, actual["pathParam"].(string))
require.Equal(t, expected.Body, actual["body"]) require.Equal(t, expected.Body, actual["body"].(string))
} }

View File

@ -6,6 +6,7 @@ package apigen
import ( import (
"fmt" "fmt"
"os" "os"
"reflect"
"strings" "strings"
"github.com/zeebo/errs" "github.com/zeebo/errs"
@ -79,14 +80,14 @@ func (f *tsGenFile) registerTypes() {
for _, group := range f.api.EndpointGroups { for _, group := range f.api.EndpointGroups {
for _, method := range group.endpoints { for _, method := range group.endpoints {
if method.Request != nil { if method.Request != nil {
f.types.Register(method.requestType(group)) f.types.Register(reflect.TypeOf(method.Request))
} }
if method.Response != nil { if method.Response != nil {
f.types.Register(method.responseType(group)) f.types.Register(reflect.TypeOf(method.Response))
} }
if len(method.QueryParams) > 0 { if len(method.QueryParams) > 0 {
for _, p := range method.QueryParams { for _, p := range method.QueryParams {
t := getElementaryType(p.namedType(method.Endpoint, "query")) t := getElementaryType(p.Type)
f.types.Register(t) f.types.Register(t)
} }
} }
@ -106,8 +107,7 @@ func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
returnStmt := "return" returnStmt := "return"
returnType := "void" returnType := "void"
if method.Response != nil { if method.Response != nil {
respType := method.responseType(group) returnType = TypescriptTypeName(reflect.TypeOf(method.Response))
returnType = TypescriptTypeName(respType)
returnStmt += fmt.Sprintf(" response.json().then((body) => body as %s)", returnType) returnStmt += fmt.Sprintf(" response.json().then((body) => body as %s)", returnType)
} }
returnStmt += ";" returnStmt += ";"
@ -149,16 +149,16 @@ func (f *tsGenFile) getArgsAndPath(method *fullEndpoint, group *EndpointGroup) (
path = "${this.ROOT_PATH}" + path path = "${this.ROOT_PATH}" + path
if method.Request != nil { 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 { 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) path += fmt.Sprintf("/${%s}", p.Name)
} }
for _, p := range method.QueryParams { 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, "//", "/") path = strings.ReplaceAll(path, "//", "/")

View File

@ -6,6 +6,7 @@ package apigen
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/zeebo/errs" "github.com/zeebo/errs"
@ -98,8 +99,7 @@ func (f *tsGenMockFile) createAPIClient(group *EndpointGroup) {
)) ))
} }
respType := method.responseType(group) returnType = TypescriptTypeName(reflect.TypeOf(method.Response))
returnType = TypescriptTypeName(respType)
} }
f.pf("\tpublic async %s(%s): Promise<%s> {", method.TypeScriptName, funcArgs, returnType) 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. // 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 { func (types *Types) All() map[reflect.Type]string {
all := map[reflect.Type]string{} all := map[reflect.Type]string{}
uniqueNames := map[string]struct{}{}
var walk func(t reflect.Type, alternateTypeName string) var walk func(t reflect.Type)
walk = func(t reflect.Type, altTypeName string) { walk = func(t reflect.Type) {
if _, ok := all[t]; ok { if _, ok := all[t]; ok {
return 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 { if n, ok := commonClasses[t]; ok {
all[t] = n all[t] = n
uniqueNames[n] = struct{}{}
return return
} }
switch k := t.Kind(); k { switch k := t.Kind(); k {
case reflect.Ptr: case reflect.Ptr:
walk(t.Elem(), altTypeName) walk(t.Elem())
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice:
// If element type has a TypeScript name then an array of the element type will be defined walk(t.Elem())
// 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)
case reflect.Struct: case reflect.Struct:
n := t.Name() if t.Name() == "" {
if n == "" { panic(fmt.Sprintf("BUG: found an anonymous 'struct'. Found type=%q", t))
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 all[t] = t.Name()
}
all[t] = n
uniqueNames[n] = struct{}{}
for i := 0; i < t.NumField(); i++ { for i := 0; i < t.NumField(); i++ {
field := t.Field(i) field := t.Field(i)
walk(field.Type, compoundTypeName(altTypeName, field.Name)) walk(field.Type)
} }
case reflect.Bool, case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 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.Float32, reflect.Float64,
reflect.String: reflect.String:
all[t] = t.Name() all[t] = t.Name()
uniqueNames[t.Name()] = struct{}{}
default: default:
panic(fmt.Sprintf("type %q is not supported", t.Kind().String())) panic(fmt.Sprintf("type %q is not supported", t.Kind().String()))
} }
} }
for t := range types.top { for t := range types.top {
walk(t, t.Name()) walk(t)
} }
return all return all
@ -186,42 +147,11 @@ func (types *Types) GenerateTypescriptDefinitions() string {
isOptional = "?" isOptional = "?"
} }
if field.Type.Name() != "" {
pf("\t%s%s: %s;", jsonField, isOptional, TypescriptTypeName(field.Type)) 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>",
TypescriptTypeName(
typeCustomName{Type: t.Type, name: t.Name}),
TypescriptTypeName(typeCustomName{Type: t.Type.Elem(), name: elemTypeName}),
)
}
return out.String() return out.String()
} }
@ -279,6 +209,9 @@ func TypescriptTypeName(t reflect.Type) string {
case reflect.Bool: case reflect.Bool:
return "boolean" return "boolean"
case reflect.Struct: 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()) return capitalize(t.Name())
default: default:
panic(fmt.Sprintf(`unhandled type. Type="%+v"`, t)) 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") require.Subset(t, allTypes, typesList, "all types contains at least the registered ones")
}) })
t.Run("All nested structs and slices", func(t *testing.T) { t.Run("Anonymous types panics", func(t *testing.T) {
type Item struct{} type Address struct {
types := NewTypes()
types.Register(
typeCustomName{
Type: reflect.TypeOf(struct {
Name string
Addresses []struct {
Address string Address string
PO string PO string
} }
Job struct { type Job struct {
Company string Company string
Position string Position string
StartingYear uint StartingYear uint
ContractClauses []struct { // This is what it makes Types.All to panic
ClauseID uint
CauseDesc string
} }
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)
} }
require.ElementsMatch(t, []string{ type Citizen struct {
"string", "uint",
"Response",
"ResponseAddresses", "ResponseAddressesItem",
"ResponseJob",
"ResponseDocuments", "ResponseDocumentsItem", "testTypesValoration",
"Item",
}, typesNames)
})
t.Run("All panic types without unique names", func(t *testing.T) {
types := NewTypes()
types.Register(typeCustomName{
Type: reflect.TypeOf(struct {
Name string Name string
Addresses []struct { Addresses []Address
Address string Job Job
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 := NewTypes()
types.Register(reflect.TypeOf(Citizen{}))
require.Panics(t, func() { require.Panics(t, func() {
types.All() types.All()
}) })