Skip to content

Commit 3b6d7ce

Browse files
authored
cli: improve generic decoders and type generation (#164)
1 parent 3ea565e commit 3b6d7ce

File tree

16 files changed

+1221
-965
lines changed

16 files changed

+1221
-965
lines changed

cmd/hasura-ndc-go/command/internal/connector.go

+121-31
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"go/token"
9+
"go/types"
910
"io"
1011
"os"
1112
"path"
@@ -61,7 +62,6 @@ func (ctb connectorTypeBuilder) String() string {
6162
bs.WriteString(")\n")
6263
}
6364

64-
bs.WriteString("var connector_Decoder = utils.NewDecoder()\n")
6565
bs.WriteString(ctb.builder.String())
6666
return bs.String()
6767
}
@@ -642,51 +642,61 @@ func (cg *connectorGenerator) writeGetTypeValueDecoder(sb *connectorTypeBuilder,
642642
case "*[]*any", "*[]*interface{}":
643643
cg.writeScalarDecodeValue(sb, fieldName, "GetNullableArbitraryJSONPtrSlice", "", key, objectField, true)
644644
default:
645+
sb.builder.WriteString(" j.")
646+
sb.builder.WriteString(fieldName)
647+
sb.builder.WriteString(", err = utils.")
645648
switch t := ty.(type) {
646649
case *NullableType:
647-
packagePaths := getTypePackagePaths(t.UnderlyingType, sb.packagePath)
648-
tyName := getTypeArgumentName(t.UnderlyingType, sb.packagePath, false)
649-
650+
var tyName string
651+
var packagePaths []string
652+
if t.IsAnonymous() {
653+
tyName, packagePaths = cg.getAnonymousObjectTypeName(sb, field.TypeAST, true)
654+
} else {
655+
packagePaths = getTypePackagePaths(t.UnderlyingType, sb.packagePath)
656+
tyName = getTypeArgumentName(t.UnderlyingType, sb.packagePath, false)
657+
}
650658
for _, pkgPath := range packagePaths {
651659
sb.imports[pkgPath] = ""
652660
}
653-
sb.builder.WriteString(" j.")
654-
sb.builder.WriteString(fieldName)
655-
sb.builder.WriteString(" = new(")
656-
sb.builder.WriteString(tyName)
657-
sb.builder.WriteString(")\n err = connector_Decoder.")
658661
if field.Embedded {
659-
sb.builder.WriteString("DecodeObject(j.")
660-
sb.builder.WriteString(fieldName)
661-
sb.builder.WriteString(", input)")
662+
sb.builder.WriteString("DecodeNullableObject[")
663+
sb.builder.WriteString(tyName)
664+
sb.builder.WriteString("](input)")
662665
} else {
663-
sb.builder.WriteString("DecodeNullableObjectValue(j.")
664-
sb.builder.WriteString(fieldName)
665-
sb.builder.WriteString(`, input, "`)
666+
sb.builder.WriteString("DecodeNullableObjectValue[")
667+
sb.builder.WriteString(tyName)
668+
sb.builder.WriteString(`](input, "`)
666669
sb.builder.WriteString(key)
667670
sb.builder.WriteString(`")`)
668671
}
669672
default:
670-
var canEmpty bool
671-
if len(objectField.Type) > 0 {
672-
if typeEnum, err := objectField.Type.Type(); err == nil && typeEnum == schema.TypeNullable {
673-
canEmpty = true
674-
}
673+
var tyName string
674+
var packagePaths []string
675+
if t.IsAnonymous() {
676+
tyName, packagePaths = cg.getAnonymousObjectTypeName(sb, field.TypeAST, true)
677+
} else {
678+
packagePaths = getTypePackagePaths(ty, sb.packagePath)
679+
tyName = getTypeArgumentName(ty, sb.packagePath, false)
680+
}
681+
for _, pkgPath := range packagePaths {
682+
sb.imports[pkgPath] = ""
675683
}
684+
676685
if field.Embedded {
677-
sb.builder.WriteString(" err = connector_Decoder.DecodeObject(&j.")
678-
sb.builder.WriteString(fieldName)
679-
sb.builder.WriteString(", input)")
686+
sb.builder.WriteString("DecodeObject")
687+
sb.builder.WriteRune('[')
688+
sb.builder.WriteString(tyName)
689+
sb.builder.WriteString("](input)")
680690
} else {
681-
sb.builder.WriteString(" err = connector_Decoder.")
682-
if canEmpty {
683-
sb.builder.WriteString("DecodeNullableObjectValue")
684-
} else {
685-
sb.builder.WriteString("DecodeObjectValue")
691+
sb.builder.WriteString("DecodeObjectValue")
692+
if len(objectField.Type) > 0 {
693+
if typeEnum, err := objectField.Type.Type(); err == nil && typeEnum == schema.TypeNullable {
694+
sb.builder.WriteString("Default")
695+
}
686696
}
687-
sb.builder.WriteString("(&j.")
688-
sb.builder.WriteString(fieldName)
689-
sb.builder.WriteString(`, input, "`)
697+
sb.builder.WriteRune('[')
698+
sb.builder.WriteString(tyName)
699+
sb.builder.WriteString(`](input, "`)
690700
sb.builder.WriteString(key)
691701
sb.builder.WriteString(`")`)
692702
}
@@ -715,6 +725,86 @@ func (cg *connectorGenerator) writeScalarDecodeValue(sb *connectorTypeBuilder, f
715725
sb.builder.WriteString(`")`)
716726
}
717727

728+
// generate anonymous object type name with absolute package paths removed
729+
func (cg *connectorGenerator) getAnonymousObjectTypeName(sb *connectorTypeBuilder, goType types.Type, skipNullable bool) (string, []string) {
730+
switch inferredType := goType.(type) {
731+
case *types.Pointer:
732+
var result string
733+
if !skipNullable {
734+
result += "*"
735+
}
736+
underlyingName, packagePaths := cg.getAnonymousObjectTypeName(sb, inferredType.Elem(), false)
737+
return result + underlyingName, packagePaths
738+
case *types.Struct:
739+
packagePaths := []string{}
740+
result := "struct{"
741+
for i := 0; i < inferredType.NumFields(); i++ {
742+
fieldVar := inferredType.Field(i)
743+
fieldTag := inferredType.Tag(i)
744+
if i > 0 {
745+
result += "; "
746+
}
747+
result += fieldVar.Name() + " "
748+
underlyingName, pkgPaths := cg.getAnonymousObjectTypeName(sb, fieldVar.Type(), false)
749+
result += underlyingName
750+
packagePaths = append(packagePaths, pkgPaths...)
751+
if fieldTag != "" {
752+
result += " `" + fieldTag + "`"
753+
}
754+
}
755+
result += "}"
756+
return result, packagePaths
757+
case *types.Named:
758+
packagePaths := []string{}
759+
innerType := inferredType.Obj()
760+
if innerType == nil {
761+
return "", packagePaths
762+
}
763+
764+
var result string
765+
typeInfo := &TypeInfo{
766+
Name: innerType.Name(),
767+
}
768+
769+
innerPkg := innerType.Pkg()
770+
if innerPkg != nil && innerPkg.Name() != "" && innerPkg.Path() != sb.packagePath {
771+
packagePaths = append(packagePaths, innerPkg.Path())
772+
result += innerPkg.Name() + "."
773+
typeInfo.PackageName = innerPkg.Name()
774+
typeInfo.PackagePath = innerPkg.Path()
775+
}
776+
777+
result += innerType.Name()
778+
typeParams := inferredType.TypeParams()
779+
if typeParams != nil && typeParams.Len() > 0 {
780+
// unwrap the generic type parameters such as Foo[T]
781+
if err := parseTypeParameters(typeInfo, inferredType.String()); err == nil {
782+
result += "["
783+
for i, typeParam := range typeInfo.TypeParameters {
784+
if i > 0 {
785+
result += ", "
786+
}
787+
packagePaths = append(packagePaths, getTypePackagePaths(typeParam, sb.packagePath)...)
788+
result += getTypeArgumentName(typeParam, sb.packagePath, false)
789+
}
790+
result += "]"
791+
}
792+
}
793+
794+
return result, packagePaths
795+
case *types.Basic:
796+
return inferredType.Name(), []string{}
797+
case *types.Array:
798+
result, packagePaths := cg.getAnonymousObjectTypeName(sb, inferredType.Elem(), false)
799+
return "[]" + result, packagePaths
800+
case *types.Slice:
801+
result, packagePaths := cg.getAnonymousObjectTypeName(sb, inferredType.Elem(), false)
802+
return "[]" + result, packagePaths
803+
default:
804+
return inferredType.String(), []string{}
805+
}
806+
}
807+
718808
func formatLocalFieldName(input string, others ...string) string {
719809
name := fieldNameRegex.ReplaceAllString(input, "_")
720810
return strings.Trim(strings.Join(append([]string{name}, others...), "_"), "_")

cmd/hasura-ndc-go/command/internal/connector_handler.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,18 @@ func (dch DataConnectorHandler) execQuery(ctx context.Context, state *`)
131131
chb.Builder.imports[fn.ArgumentsType.PackagePath] = ""
132132
}
133133

134-
sb.WriteString(`
135-
var args `)
136-
sb.WriteString(argName)
137-
sb.WriteString("\n if parseErr := ")
138134
if fn.ArgumentsType.CanMethod() {
135+
sb.WriteString("\n var args ")
136+
sb.WriteString(argName)
137+
sb.WriteString("\n parseErr := ")
139138
sb.WriteString("args.FromValue(rawArgs)")
140139
} else {
141-
sb.WriteString("connector_Decoder.DecodeObject(&args, rawArgs)")
140+
sb.WriteString("\n args, parseErr := utils.DecodeObject[")
141+
sb.WriteString(argName)
142+
sb.WriteString("](rawArgs)")
142143
}
143-
sb.WriteString(`; parseErr != nil {
144+
sb.WriteString(`
145+
if parseErr != nil {
144146
return nil, schema.UnprocessableContentError("failed to resolve arguments", map[string]any{
145147
"cause": parseErr.Error(),
146148
})

cmd/hasura-ndc-go/command/internal/schema.go

+20
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,31 @@ import (
77
"github.com/hasura/ndc-sdk-go/schema"
88
)
99

10+
// OperationKind the operation kind of connectors
1011
type OperationKind string
1112

1213
const (
1314
OperationFunction OperationKind = "Function"
1415
OperationProcedure OperationKind = "Procedure"
1516
)
1617

18+
// Scalar the structured information of the scalar
1719
type Scalar struct {
1820
Schema schema.ScalarType
1921
NativeType *TypeInfo
2022
}
2123

24+
// Type the interface of a type schema
2225
type Type interface {
2326
Kind() schema.TypeEnum
2427
Schema() schema.TypeEncoder
2528
SchemaName(isAbsolute bool) string
2629
FullName() string
2730
String() string
31+
IsAnonymous() bool
2832
}
2933

34+
// NullableType the information of the nullable type
3035
type NullableType struct {
3136
UnderlyingType Type
3237
}
@@ -41,6 +46,10 @@ func (t *NullableType) Kind() schema.TypeEnum {
4146
return schema.TypeNullable
4247
}
4348

49+
func (t *NullableType) IsAnonymous() bool {
50+
return t.UnderlyingType.IsAnonymous()
51+
}
52+
4453
func (t NullableType) SchemaName(isAbsolute bool) string {
4554
var result string
4655
if isAbsolute {
@@ -65,6 +74,7 @@ func (t NullableType) String() string {
6574
return "*" + t.UnderlyingType.String()
6675
}
6776

77+
// ArrayType the information of the array type
6878
type ArrayType struct {
6979
ElementType Type
7080
}
@@ -79,6 +89,10 @@ func (t *ArrayType) Kind() schema.TypeEnum {
7989
return schema.TypeArray
8090
}
8191

92+
func (t *ArrayType) IsAnonymous() bool {
93+
return t.ElementType.IsAnonymous()
94+
}
95+
8296
func (t *ArrayType) Schema() schema.TypeEncoder {
8397
return schema.NewArrayType(t.ElementType.Schema())
8498
}
@@ -100,6 +114,7 @@ func (t ArrayType) String() string {
100114
return "[]" + t.ElementType.String()
101115
}
102116

117+
// NamedType the information of a named type
103118
type NamedType struct {
104119
Name string
105120
NativeType *TypeInfo
@@ -115,6 +130,10 @@ func (t *NamedType) Kind() schema.TypeEnum {
115130
return schema.TypeNamed
116131
}
117132

133+
func (t *NamedType) IsAnonymous() bool {
134+
return t.NativeType.IsAnonymous
135+
}
136+
118137
func (t *NamedType) Schema() schema.TypeEncoder {
119138
return schema.NewNamedType(t.Name)
120139
}
@@ -209,6 +228,7 @@ type Field struct {
209228
Description *string
210229
Embedded bool
211230
Type Type
231+
TypeAST types.Type
212232
}
213233

214234
// ObjectInfo represents the serialization information of an object type.

cmd/hasura-ndc-go/command/internal/schema_type_parser.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ func (tp *TypeParser) Parse(fieldPaths []string) (*Field, error) {
3434
if err != nil {
3535
return nil, err
3636
}
37+
if ty == nil {
38+
return nil, nil
39+
}
3740
tp.field.Type = ty
3841
return tp.field, nil
3942
}
@@ -314,6 +317,8 @@ func (tp *TypeParser) parseType(ty types.Type, fieldPaths []string) (Type, error
314317
})
315318

316319
return NewNamedType(string(ScalarJSON), typeInfo), nil
320+
case *types.Chan, *types.Signature, *types.Tuple, *types.Union:
321+
return nil, nil
317322
default:
318323
return nil, fmt.Errorf("unsupported type: %s", ty.String())
319324
}
@@ -322,6 +327,10 @@ func (tp *TypeParser) parseType(ty types.Type, fieldPaths []string) (Type, error
322327
func (tp *TypeParser) parseStructType(objectInfo *ObjectInfo, inferredType *types.Struct, fieldPaths []string) error {
323328
for i := 0; i < inferredType.NumFields(); i++ {
324329
fieldVar := inferredType.Field(i)
330+
if !fieldVar.Exported() {
331+
continue
332+
}
333+
325334
fieldTag := inferredType.Tag(i)
326335
fieldKey, jsonOption := getFieldNameOrTag(fieldVar.Name(), fieldTag)
327336
if jsonOption == jsonIgnore {
@@ -330,11 +339,15 @@ func (tp *TypeParser) parseStructType(objectInfo *ObjectInfo, inferredType *type
330339
fieldParser := NewTypeParser(tp.schemaParser, &Field{
331340
Name: fieldVar.Name(),
332341
Embedded: fieldVar.Embedded(),
342+
TypeAST: fieldVar.Type(),
333343
}, fieldVar.Type(), tp.argumentFor)
334344
field, err := fieldParser.Parse(append(fieldPaths, fieldVar.Name()))
335345
if err != nil {
336346
return err
337347
}
348+
if field == nil {
349+
continue
350+
}
338351
embeddedObject, ok := tp.schemaParser.rawSchema.Objects[field.Type.SchemaName(false)]
339352
if field.Embedded && ok {
340353
// flatten embedded object fields to the parent object
@@ -466,7 +479,13 @@ func (tp *TypeParser) parseTypeInfoFromComments(typeInfo *TypeInfo, scope *types
466479

467480
func parseTypeParameters(rootType *TypeInfo, input string) error {
468481
paramsString := strings.TrimPrefix(input, rootType.PackagePath+"."+rootType.Name)
469-
rawParams := strings.Split(paramsString[1:len(paramsString)-1], ",")
482+
if paramsString[0] == '[' {
483+
paramsString = paramsString[1:]
484+
}
485+
if paramsString[len(paramsString)-1] == ']' {
486+
paramsString = paramsString[:len(paramsString)-1]
487+
}
488+
rawParams := strings.Split(paramsString, ",")
470489

471490
for _, param := range rawParams {
472491
param = strings.TrimSpace(param)

0 commit comments

Comments
 (0)