private/apigen: Generate valid TypeScript code with anonymous types
The API generator didn't generate valid TypeScript code when using Go anonymous types. This commit fixes that issue creating names for anonymous types. Change-Id: Ice0748d8650686e3d3979523b8f218dc20eade5a
This commit is contained in:
parent
a9901cc7d0
commit
00484429d6
@ -10,6 +10,9 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"storj.io/storj/private/api"
|
||||
)
|
||||
|
||||
@ -85,6 +88,17 @@ 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() {
|
||||
@ -114,3 +128,15 @@ 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.
|
||||
func compoundTypeName(base string, parts ...string) string {
|
||||
caser := cases.Title(language.Und)
|
||||
titled := make([]string, len(parts))
|
||||
for i := 0; i < len(parts); i++ {
|
||||
titled[i] = caser.String(parts[i])
|
||||
}
|
||||
|
||||
return base + strings.Join(titled, "")
|
||||
}
|
||||
|
@ -57,6 +57,31 @@ type fullEndpoint struct {
|
||||
Method string
|
||||
}
|
||||
|
||||
// requestType guarantees to return a named Go type associated to the Endpoint.Request field.
|
||||
func (fe fullEndpoint) requestType() reflect.Type {
|
||||
t := reflect.TypeOf(fe.Request)
|
||||
if t.Name() == "" {
|
||||
name := fe.RequestName
|
||||
if name == "" {
|
||||
name = fe.MethodName
|
||||
}
|
||||
|
||||
t = typeCustomName{Type: t, name: compoundTypeName(name, "Request")}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// responseType guarantees to return a named Go type associated to the Endpoint.Response field.
|
||||
func (fe fullEndpoint) responseType() reflect.Type {
|
||||
t := reflect.TypeOf(fe.Response)
|
||||
if t.Name() == "" {
|
||||
t = typeCustomName{Type: t, name: compoundTypeName(fe.MethodName, "Response")}
|
||||
}
|
||||
|
||||
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.
|
||||
@ -127,3 +152,16 @@ 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.MethodName, where, "param", p.Name),
|
||||
}
|
||||
}
|
||||
|
||||
return p.Type
|
||||
}
|
||||
|
@ -4,11 +4,11 @@
|
||||
import { HttpClient } from '@/utils/httpClient';
|
||||
import { Time, UUID } from '@/types/common';
|
||||
|
||||
export class {
|
||||
export class UpdateContentRequest {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class {
|
||||
export class UpdateContentResponse {
|
||||
id: UUID;
|
||||
date: Time;
|
||||
pathParam: string;
|
||||
@ -19,14 +19,14 @@ export class docsHttpApiV0 {
|
||||
private readonly http: HttpClient = new HttpClient();
|
||||
private readonly ROOT_PATH: string = '/api/v0/docs';
|
||||
|
||||
public async (request: , path: string, id: UUID, date: Time): Promise<> {
|
||||
public async UpdateContent(request: UpdateContentRequest, path: string, id: UUID, date: Time): Promise<UpdateContentResponse> {
|
||||
const u = new URL(`${this.ROOT_PATH}/${path}`);
|
||||
u.searchParams.set('id', id);
|
||||
u.searchParams.set('date', date);
|
||||
const path = u.toString();
|
||||
const response = await this.http.post(path, JSON.stringify(request));
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as );
|
||||
return response.json().then((body) => body as UpdateContentResponse);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new Error(err.error);
|
||||
|
@ -65,17 +65,19 @@ func (f *tsGenFile) generateTS() {
|
||||
}
|
||||
|
||||
func (f *tsGenFile) registerTypes() {
|
||||
// TODO: what happen with path parameters?
|
||||
for _, group := range f.api.EndpointGroups {
|
||||
for _, method := range group.endpoints {
|
||||
if method.Request != nil {
|
||||
f.types.Register(reflect.TypeOf(method.Request))
|
||||
f.types.Register(method.requestType())
|
||||
}
|
||||
if method.Response != nil {
|
||||
f.types.Register(reflect.TypeOf(method.Response))
|
||||
f.types.Register(method.responseType())
|
||||
}
|
||||
if len(method.QueryParams) > 0 {
|
||||
for _, p := range method.QueryParams {
|
||||
t := getElementaryType(p.Type)
|
||||
// TODO: Is this call needed? this breaks the named type for slices and arrays and pointers.
|
||||
t := getElementaryType(p.namedType(method.Endpoint, "query"))
|
||||
f.types.Register(t)
|
||||
}
|
||||
}
|
||||
@ -95,15 +97,22 @@ func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
||||
returnStmt := "return"
|
||||
returnType := "void"
|
||||
if method.Response != nil {
|
||||
returnType = TypescriptTypeName(getElementaryType(reflect.TypeOf(method.Response)))
|
||||
if v := reflect.ValueOf(method.Response); v.Kind() == reflect.Array || v.Kind() == reflect.Slice {
|
||||
respType := method.responseType()
|
||||
returnType = TypescriptTypeName(getElementaryType(respType))
|
||||
// TODO: see if this is needed after we are creating types for array and slices
|
||||
if respType.Kind() == reflect.Array || respType.Kind() == reflect.Slice {
|
||||
returnType = fmt.Sprintf("Array<%s>", returnType)
|
||||
}
|
||||
returnStmt += fmt.Sprintf(" response.json().then((body) => body as %s)", returnType)
|
||||
}
|
||||
returnStmt += ";"
|
||||
|
||||
f.pf("\tpublic async %s(%s): Promise<%s> {", method.RequestName, funcArgs, returnType)
|
||||
methodName := method.RequestName
|
||||
if methodName == "" {
|
||||
methodName = method.MethodName
|
||||
}
|
||||
|
||||
f.pf("\tpublic async %s(%s): Promise<%s> {", methodName, funcArgs, returnType)
|
||||
if len(method.QueryParams) > 0 {
|
||||
f.pf("\t\tconst u = new URL(`%s`);", path)
|
||||
for _, p := range method.QueryParams {
|
||||
@ -140,17 +149,18 @@ func (f *tsGenFile) getArgsAndPath(method *fullEndpoint) (funcArgs, path string)
|
||||
path = "${this.ROOT_PATH}" + path
|
||||
|
||||
if method.Request != nil {
|
||||
t := getElementaryType(reflect.TypeOf(method.Request))
|
||||
// TODO: This should map slices and arrays because a request could be one of them.
|
||||
t := getElementaryType(method.requestType())
|
||||
funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(t))
|
||||
}
|
||||
|
||||
for _, p := range method.PathParams {
|
||||
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type))
|
||||
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.namedType(method.Endpoint, "path")))
|
||||
path += fmt.Sprintf("/${%s}", p.Name)
|
||||
}
|
||||
|
||||
for _, p := range method.QueryParams {
|
||||
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type))
|
||||
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.namedType(method.Endpoint, "query")))
|
||||
}
|
||||
|
||||
path = strings.ReplaceAll(path, "//", "/")
|
||||
|
@ -36,46 +36,76 @@ type Types struct {
|
||||
|
||||
// Register registers a type for generation.
|
||||
func (types *Types) Register(t reflect.Type) {
|
||||
if t.Name() == "" {
|
||||
panic("register an anonymous type is not supported. All the types must have a name")
|
||||
}
|
||||
types.top[t] = struct{}{}
|
||||
}
|
||||
|
||||
// All returns a slice containing every top-level type and their dependencies.
|
||||
//
|
||||
// TODO: see how to have a better implementation for adding to seen, uniqueNames, and all.
|
||||
func (types *Types) All() []reflect.Type {
|
||||
seen := map[reflect.Type]struct{}{}
|
||||
uniqueNames := map[string]struct{}{}
|
||||
all := []reflect.Type{}
|
||||
|
||||
var walk func(t reflect.Type)
|
||||
walk = func(t reflect.Type) {
|
||||
var walk func(t reflect.Type, alternateTypeName string)
|
||||
walk = func(t reflect.Type, altTypeName string) {
|
||||
if _, ok := seen[t]; ok {
|
||||
return
|
||||
}
|
||||
seen[t] = struct{}{}
|
||||
all = append(all, t)
|
||||
|
||||
// Type isn't seen it but it has the same name than a seen it one.
|
||||
// This cannot be because we would generate more than one TypeScript type with the same name.
|
||||
if _, ok := uniqueNames[t.Name()]; ok {
|
||||
panic(fmt.Sprintf("Found different types with the same name (%s)", t.Name()))
|
||||
}
|
||||
|
||||
if _, ok := commonClasses[t]; ok {
|
||||
seen[t] = struct{}{}
|
||||
uniqueNames[t.Name()] = struct{}{}
|
||||
all = append(all, t)
|
||||
return
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
switch k := t.Kind(); k {
|
||||
// TODO: Does reflect.Ptr to be registered?, I believe that could skip it and only register
|
||||
// the type that points to.
|
||||
case reflect.Array, reflect.Ptr, reflect.Slice:
|
||||
walk(t.Elem())
|
||||
t = typeCustomName{Type: t, name: compoundTypeName(altTypeName, k.String())}
|
||||
seen[t] = struct{}{}
|
||||
uniqueNames[t.Name()] = struct{}{}
|
||||
all = append(all, t)
|
||||
walk(t.Elem(), altTypeName)
|
||||
case reflect.Struct:
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
walk(t.Field(i).Type)
|
||||
if t.Name() == "" {
|
||||
t = typeCustomName{Type: t, name: altTypeName}
|
||||
}
|
||||
case reflect.Bool:
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
case reflect.Float32, reflect.Float64:
|
||||
case reflect.String:
|
||||
break
|
||||
|
||||
seen[t] = struct{}{}
|
||||
uniqueNames[t.Name()] = struct{}{}
|
||||
all = append(all, t)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
walk(field.Type, compoundTypeName(altTypeName, field.Name))
|
||||
}
|
||||
case reflect.Bool,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64,
|
||||
reflect.String:
|
||||
seen[t] = struct{}{}
|
||||
uniqueNames[t.Name()] = struct{}{}
|
||||
all = append(all, t)
|
||||
default:
|
||||
panic(fmt.Sprintf("type '%s' is not supported", t.Kind().String()))
|
||||
}
|
||||
}
|
||||
|
||||
for t := range types.top {
|
||||
walk(t)
|
||||
walk(t, t.Name())
|
||||
}
|
||||
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
@ -96,6 +126,8 @@ func (types *Types) GenerateTypescriptDefinitions() string {
|
||||
if _, ok := commonClasses[t]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO, we should be able to handle arrays and slices as defined types now
|
||||
return t.Kind() == reflect.Struct
|
||||
})
|
||||
|
||||
@ -154,6 +186,7 @@ func (types *Types) getTypescriptImports() string {
|
||||
}
|
||||
|
||||
// TypescriptTypeName gets the corresponding TypeScript type for a provided reflect.Type.
|
||||
// If the type is an anonymous struct, it returns an empty string.
|
||||
func TypescriptTypeName(t reflect.Type) string {
|
||||
if override, ok := commonClasses[t]; ok {
|
||||
return override
|
||||
|
@ -10,7 +10,26 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testTypesValoration struct {
|
||||
Points uint
|
||||
}
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
t.Run("Register panics with anonymous types", func(t *testing.T) {
|
||||
types := NewTypes()
|
||||
require.Panics(t, func() {
|
||||
types.Register(reflect.TypeOf([2]int{}))
|
||||
}, "array")
|
||||
|
||||
require.Panics(t, func() {
|
||||
types.Register(reflect.TypeOf([]float64{}))
|
||||
}, "slice")
|
||||
|
||||
require.Panics(t, func() {
|
||||
types.Register(reflect.TypeOf(struct{}{}))
|
||||
}, "struct")
|
||||
})
|
||||
|
||||
t.Run("All returns nested types", func(t *testing.T) {
|
||||
typesList := []reflect.Type{
|
||||
reflect.TypeOf(true),
|
||||
@ -18,23 +37,7 @@ func TestTypes(t *testing.T) {
|
||||
reflect.TypeOf(uint8(9)),
|
||||
reflect.TypeOf(float64(99.9)),
|
||||
reflect.TypeOf("this is a test"),
|
||||
reflect.TypeOf(struct {
|
||||
Name string
|
||||
Addresses []struct {
|
||||
Address string
|
||||
PO string
|
||||
}
|
||||
Job struct {
|
||||
Company string
|
||||
Position string
|
||||
StartingYear uint
|
||||
}
|
||||
}{}),
|
||||
reflect.TypeOf([]string{}),
|
||||
reflect.TypeOf([]struct {
|
||||
Path string
|
||||
content string
|
||||
}{}),
|
||||
reflect.TypeOf(testTypesValoration{}),
|
||||
}
|
||||
|
||||
types := NewTypes()
|
||||
@ -44,13 +47,15 @@ func TestTypes(t *testing.T) {
|
||||
|
||||
allTypes := types.All()
|
||||
|
||||
require.Len(t, allTypes, 13, "total number of types")
|
||||
require.Len(t, allTypes, 7, "total number of types")
|
||||
require.Subset(t, allTypes, typesList, "all types contains at least the registered ones")
|
||||
})
|
||||
|
||||
t.Run("anonymous structs", func(t *testing.T) {
|
||||
typesList := []reflect.Type{
|
||||
reflect.TypeOf(struct {
|
||||
t.Run("All nested structs and slices", func(t *testing.T) {
|
||||
types := NewTypes()
|
||||
types.Register(
|
||||
typeCustomName{
|
||||
Type: reflect.TypeOf(struct {
|
||||
Name string
|
||||
Addresses []struct {
|
||||
Address string
|
||||
@ -64,25 +69,61 @@ func TestTypes(t *testing.T) {
|
||||
Documents []struct {
|
||||
Path string
|
||||
Content string
|
||||
Valoration testTypesValoration
|
||||
}
|
||||
}{}),
|
||||
}
|
||||
|
||||
types := NewTypes()
|
||||
for _, li := range typesList {
|
||||
types.Register(li)
|
||||
}
|
||||
name: "Response",
|
||||
})
|
||||
|
||||
allTypes := types.All()
|
||||
|
||||
require.Len(t, allTypes, 8, "total number of types")
|
||||
require.Subset(t, allTypes, typesList, "all types contains at least the registered ones")
|
||||
require.Len(t, allTypes, 9, "total number of types")
|
||||
|
||||
typesNames := []string{}
|
||||
for _, tp := range allTypes {
|
||||
typesNames = append(typesNames, tp.Name())
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, []string{"", "", "", "", "", "", "string", "uint"}, typesNames)
|
||||
require.ElementsMatch(t, []string{
|
||||
"string", "uint",
|
||||
"Response",
|
||||
"ResponseAddressesSlice", "ResponseAddresses",
|
||||
"ResponseJob",
|
||||
"ResponseDocumentsSlice", "ResponseDocuments", "testTypesValoration",
|
||||
}, typesNames)
|
||||
})
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
require.Panics(t, func() {
|
||||
types.All()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user