github.com/hasura/ndc-sdk-go/cmd/hasura-ndc-go@v0.0.0-20240508172728-e960be013ca2/schema.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"go/ast"
     9  	"go/token"
    10  	"go/types"
    11  	"path"
    12  	"regexp"
    13  	"runtime/trace"
    14  	"strings"
    15  
    16  	"github.com/fatih/structtag"
    17  	"github.com/hasura/ndc-sdk-go/schema"
    18  	"github.com/rs/zerolog/log"
    19  	"golang.org/x/tools/go/packages"
    20  )
    21  
    22  type ScalarName string
    23  
    24  const (
    25  	ScalarBoolean     ScalarName = "Boolean"
    26  	ScalarString      ScalarName = "String"
    27  	ScalarInt8        ScalarName = "Int8"
    28  	ScalarInt16       ScalarName = "Int16"
    29  	ScalarInt32       ScalarName = "Int32"
    30  	ScalarInt64       ScalarName = "Int64"
    31  	ScalarFloat32     ScalarName = "Float32"
    32  	ScalarFloat64     ScalarName = "Float64"
    33  	ScalarBigInt      ScalarName = "BigInt"
    34  	ScalarBigDecimal  ScalarName = "BigDecimal"
    35  	ScalarUUID        ScalarName = "UUID"
    36  	ScalarDate        ScalarName = "Date"
    37  	ScalarTimestamp   ScalarName = "Timestamp"
    38  	ScalarTimestampTZ ScalarName = "TimestampTZ"
    39  	ScalarGeography   ScalarName = "Geography"
    40  	ScalarBytes       ScalarName = "Bytes"
    41  	ScalarJSON        ScalarName = "JSON"
    42  	// ScalarRawJSON is a special scalar for raw json data serialization.
    43  	// The underlying Go type for this scalar is json.RawMessage.
    44  	// Note: we don't recommend to use this scalar for function arguments
    45  	// because the decoder will re-encode the value to []byte that isn't performance-wise.
    46  	ScalarRawJSON ScalarName = "RawJSON"
    47  )
    48  
    49  var defaultScalarTypes = map[ScalarName]schema.ScalarType{
    50  	ScalarBoolean: {
    51  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    52  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    53  		Representation:      schema.NewTypeRepresentationBoolean().Encode(),
    54  	},
    55  	ScalarString: {
    56  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    57  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    58  		Representation:      schema.NewTypeRepresentationString().Encode(),
    59  	},
    60  	ScalarInt8: {
    61  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    62  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    63  		Representation:      schema.NewTypeRepresentationInt8().Encode(),
    64  	},
    65  	ScalarInt16: {
    66  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    67  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    68  		Representation:      schema.NewTypeRepresentationInt16().Encode(),
    69  	},
    70  	ScalarInt32: {
    71  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    72  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    73  		Representation:      schema.NewTypeRepresentationInt32().Encode(),
    74  	},
    75  	ScalarInt64: {
    76  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    77  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    78  		Representation:      schema.NewTypeRepresentationInt64().Encode(),
    79  	},
    80  	ScalarFloat32: {
    81  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    82  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    83  		Representation:      schema.NewTypeRepresentationFloat32().Encode(),
    84  	},
    85  	ScalarFloat64: {
    86  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    87  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    88  		Representation:      schema.NewTypeRepresentationFloat64().Encode(),
    89  	},
    90  	ScalarBigInt: {
    91  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    92  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    93  		Representation:      schema.NewTypeRepresentationBigInteger().Encode(),
    94  	},
    95  	ScalarBigDecimal: {
    96  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
    97  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
    98  		Representation:      schema.NewTypeRepresentationBigDecimal().Encode(),
    99  	},
   100  	ScalarUUID: {
   101  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
   102  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
   103  		Representation:      schema.NewTypeRepresentationUUID().Encode(),
   104  	},
   105  	ScalarDate: {
   106  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
   107  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
   108  		Representation:      schema.NewTypeRepresentationDate().Encode(),
   109  	},
   110  	ScalarTimestamp: {
   111  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
   112  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
   113  		Representation:      schema.NewTypeRepresentationTimestamp().Encode(),
   114  	},
   115  	ScalarTimestampTZ: {
   116  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
   117  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
   118  		Representation:      schema.NewTypeRepresentationTimestampTZ().Encode(),
   119  	},
   120  	ScalarGeography: {
   121  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
   122  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
   123  		Representation:      schema.NewTypeRepresentationGeography().Encode(),
   124  	},
   125  	ScalarBytes: {
   126  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
   127  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
   128  		Representation:      schema.NewTypeRepresentationBytes().Encode(),
   129  	},
   130  	ScalarJSON: {
   131  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
   132  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
   133  		Representation:      schema.NewTypeRepresentationJSON().Encode(),
   134  	},
   135  	ScalarRawJSON: {
   136  		AggregateFunctions:  schema.ScalarTypeAggregateFunctions{},
   137  		ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{},
   138  		Representation:      schema.NewTypeRepresentationJSON().Encode(),
   139  	},
   140  }
   141  
   142  var ndcOperationNameRegex = regexp.MustCompile(`^(Function|Procedure)([A-Z][A-Za-z0-9]*)$`)
   143  var ndcOperationCommentRegex = regexp.MustCompile(`^@(function|procedure)(\s+([A-Za-z]\w*))?`)
   144  var ndcScalarNameRegex = regexp.MustCompile(`^Scalar([A-Z]\w*)$`)
   145  var ndcScalarCommentRegex = regexp.MustCompile(`^@scalar(\s+(\w+))?(\s+([a-z]+))?$`)
   146  var ndcEnumCommentRegex = regexp.MustCompile(`^@enum\s+([\w-.,!@#$%^&*()+=~\s\t]+)$`)
   147  
   148  type OperationKind string
   149  
   150  const (
   151  	OperationFunction  OperationKind = "Function"
   152  	OperationProcedure OperationKind = "Procedure"
   153  )
   154  
   155  type TypeKind string
   156  
   157  // TypeInfo represents the serialization information of a type
   158  type TypeInfo struct {
   159  	Name                 string
   160  	SchemaName           string
   161  	Description          *string
   162  	PackagePath          string
   163  	PackageName          string
   164  	IsScalar             bool
   165  	ScalarRepresentation schema.TypeRepresentation
   166  	TypeFragments        []string
   167  	TypeAST              types.Type
   168  	Schema               schema.TypeEncoder
   169  }
   170  
   171  // IsNullable checks if the current type is nullable
   172  func (ti *TypeInfo) IsNullable() bool {
   173  	return isNullableFragments(ti.TypeFragments)
   174  }
   175  
   176  func isNullableFragment(fragment string) bool {
   177  	return fragment == "*"
   178  }
   179  
   180  func isNullableFragments(fragments []string) bool {
   181  	return len(fragments) > 0 && isNullableFragment(fragments[0])
   182  }
   183  
   184  // IsArray checks if the current type is an array
   185  func (ti *TypeInfo) IsArray() bool {
   186  	return isArrayFragments(ti.TypeFragments)
   187  }
   188  
   189  func isArrayFragment(fragment string) bool {
   190  	return fragment == "[]"
   191  }
   192  
   193  func isArrayFragments(fragments []string) bool {
   194  	return len(fragments) > 0 && isArrayFragment(fragments[0])
   195  }
   196  
   197  // ObjectField represents the serialization information of an object field
   198  type ObjectField struct {
   199  	Name string
   200  	Key  string
   201  	Type *TypeInfo
   202  }
   203  
   204  // ObjectInfo represents the serialization information of an object type
   205  type ObjectInfo struct {
   206  	PackagePath string
   207  	PackageName string
   208  	IsAnonymous bool
   209  	Fields      map[string]*ObjectField
   210  }
   211  
   212  // ArgumentInfo represents the serialization information of an argument type
   213  type ArgumentInfo struct {
   214  	FieldName   string
   215  	Description *string
   216  	Type        *TypeInfo
   217  }
   218  
   219  // Schema converts to ArgumentInfo schema
   220  func (ai ArgumentInfo) Schema() schema.ArgumentInfo {
   221  	return schema.ArgumentInfo{
   222  		Description: ai.Description,
   223  		Type:        ai.Type.Schema.Encode(),
   224  	}
   225  }
   226  
   227  func buildArgumentInfosSchema(input map[string]ArgumentInfo) map[string]schema.ArgumentInfo {
   228  	result := make(map[string]schema.ArgumentInfo)
   229  	for k, arg := range input {
   230  		result[k] = arg.Schema()
   231  	}
   232  	return result
   233  }
   234  
   235  // FunctionInfo represents a readable Go function info
   236  // which can convert to a NDC function or procedure schema
   237  type OperationInfo struct {
   238  	Kind          OperationKind
   239  	Name          string
   240  	OriginName    string
   241  	PackageName   string
   242  	PackagePath   string
   243  	Description   *string
   244  	ArgumentsType string
   245  	Arguments     map[string]ArgumentInfo
   246  	ResultType    *TypeInfo
   247  }
   248  
   249  // FunctionInfo represents a readable Go function info
   250  // which can convert to a NDC function schema
   251  type FunctionInfo OperationInfo
   252  
   253  // Schema returns a NDC function schema
   254  func (op FunctionInfo) Schema() schema.FunctionInfo {
   255  	result := schema.FunctionInfo{
   256  		Name:        op.Name,
   257  		Description: op.Description,
   258  		ResultType:  op.ResultType.Schema.Encode(),
   259  		Arguments:   buildArgumentInfosSchema(op.Arguments),
   260  	}
   261  	return result
   262  }
   263  
   264  // ProcedureInfo represents a readable Go function info
   265  // which can convert to a NDC procedure schema
   266  type ProcedureInfo FunctionInfo
   267  
   268  // Schema returns a NDC procedure schema
   269  func (op ProcedureInfo) Schema() schema.ProcedureInfo {
   270  	result := schema.ProcedureInfo{
   271  		Name:        op.Name,
   272  		Description: op.Description,
   273  		ResultType:  op.ResultType.Schema.Encode(),
   274  		Arguments:   schema.ProcedureInfoArguments(buildArgumentInfosSchema(op.Arguments)),
   275  	}
   276  	return result
   277  }
   278  
   279  // RawConnectorSchema represents a readable Go schema object
   280  // which can encode to NDC schema
   281  type RawConnectorSchema struct {
   282  	Imports       map[string]bool
   283  	CustomScalars map[string]*TypeInfo
   284  	ScalarSchemas schema.SchemaResponseScalarTypes
   285  	Objects       map[string]*ObjectInfo
   286  	ObjectSchemas schema.SchemaResponseObjectTypes
   287  	Functions     []FunctionInfo
   288  	Procedures    []ProcedureInfo
   289  }
   290  
   291  // NewRawConnectorSchema creates an empty RawConnectorSchema instance
   292  func NewRawConnectorSchema() *RawConnectorSchema {
   293  	return &RawConnectorSchema{
   294  		Imports:       make(map[string]bool),
   295  		CustomScalars: make(map[string]*TypeInfo),
   296  		ScalarSchemas: make(schema.SchemaResponseScalarTypes),
   297  		Objects:       make(map[string]*ObjectInfo),
   298  		ObjectSchemas: make(schema.SchemaResponseObjectTypes),
   299  		Functions:     []FunctionInfo{},
   300  		Procedures:    []ProcedureInfo{},
   301  	}
   302  }
   303  
   304  // Schema converts to a NDC schema
   305  func (rcs RawConnectorSchema) Schema() *schema.SchemaResponse {
   306  	result := &schema.SchemaResponse{
   307  		ScalarTypes: rcs.ScalarSchemas,
   308  		ObjectTypes: rcs.ObjectSchemas,
   309  		Collections: []schema.CollectionInfo{},
   310  		Functions:   []schema.FunctionInfo{},
   311  		Procedures:  []schema.ProcedureInfo{},
   312  	}
   313  	for _, function := range rcs.Functions {
   314  		result.Functions = append(result.Functions, function.Schema())
   315  	}
   316  	for _, procedure := range rcs.Procedures {
   317  		result.Procedures = append(result.Procedures, procedure.Schema())
   318  	}
   319  
   320  	return result
   321  }
   322  
   323  // IsCustomType checks if the type name is a custom scalar or an exported object
   324  func (rcs RawConnectorSchema) IsCustomType(name string) bool {
   325  	if _, ok := rcs.CustomScalars[name]; ok {
   326  		return true
   327  	}
   328  	if obj, ok := rcs.Objects[name]; ok {
   329  		return !obj.IsAnonymous
   330  	}
   331  	return false
   332  }
   333  
   334  type SchemaParser struct {
   335  	context    context.Context
   336  	moduleName string
   337  	pkg        *packages.Package
   338  }
   339  
   340  func parseRawConnectorSchemaFromGoCode(ctx context.Context, moduleName string, filePath string, folders []string) (*RawConnectorSchema, error) {
   341  	rawSchema := NewRawConnectorSchema()
   342  	fset := token.NewFileSet()
   343  	for _, folder := range folders {
   344  		_, parseCodeTask := trace.NewTask(ctx, fmt.Sprintf("parse_%s_code", folder))
   345  		folderPath := path.Join(filePath, folder)
   346  
   347  		cfg := &packages.Config{
   348  			Mode: packages.NeedSyntax | packages.NeedTypes,
   349  			Dir:  folderPath,
   350  			Fset: fset,
   351  		}
   352  		pkgList, err := packages.Load(cfg, flag.Args()...)
   353  		parseCodeTask.End()
   354  		if err != nil {
   355  			return nil, err
   356  		}
   357  
   358  		for i, pkg := range pkgList {
   359  			parseSchemaCtx, parseSchemaTask := trace.NewTask(ctx, fmt.Sprintf("parse_%s_schema_%d_%s", folder, i, pkg.Name))
   360  			sp := &SchemaParser{
   361  				context:    parseSchemaCtx,
   362  				moduleName: moduleName,
   363  				pkg:        pkg,
   364  			}
   365  
   366  			err = sp.parseRawConnectorSchema(rawSchema, pkg.Types)
   367  			parseSchemaTask.End()
   368  			if err != nil {
   369  				return nil, err
   370  			}
   371  		}
   372  	}
   373  
   374  	return rawSchema, nil
   375  }
   376  
   377  // parse raw connector schema from Go code
   378  func (sp *SchemaParser) parseRawConnectorSchema(rawSchema *RawConnectorSchema, pkg *types.Package) error {
   379  
   380  	for _, name := range pkg.Scope().Names() {
   381  		_, task := trace.NewTask(sp.context, fmt.Sprintf("parse_%s_schema_%s", sp.pkg.Name, name))
   382  		err := sp.parsePackageScope(rawSchema, pkg, name)
   383  		task.End()
   384  		if err != nil {
   385  			return err
   386  		}
   387  	}
   388  
   389  	return nil
   390  }
   391  
   392  func (sp *SchemaParser) parsePackageScope(rawSchema *RawConnectorSchema, pkg *types.Package, name string) error {
   393  	switch obj := pkg.Scope().Lookup(name).(type) {
   394  	case *types.Func:
   395  		// only parse public functions
   396  		if !obj.Exported() {
   397  			return nil
   398  		}
   399  		opInfo := sp.parseOperationInfo(obj)
   400  		if opInfo == nil {
   401  			return nil
   402  		}
   403  		opInfo.PackageName = pkg.Name()
   404  		opInfo.PackagePath = pkg.Path()
   405  		var resultTuple *types.Tuple
   406  		var params *types.Tuple
   407  		switch sig := obj.Type().(type) {
   408  		case *types.Signature:
   409  			params = sig.Params()
   410  			resultTuple = sig.Results()
   411  		default:
   412  			return fmt.Errorf("expected function signature, got: %s", sig.String())
   413  		}
   414  
   415  		if params == nil || (params.Len() < 2 || params.Len() > 3) {
   416  			return fmt.Errorf("%s: expect 2 or 3 parameters only (ctx context.Context, state types.State, arguments *[ArgumentType]), got %s", opInfo.OriginName, params)
   417  		}
   418  
   419  		if resultTuple == nil || resultTuple.Len() != 2 {
   420  			return fmt.Errorf("%s: expect result tuple ([type], error), got %s", opInfo.OriginName, resultTuple)
   421  		}
   422  
   423  		// parse arguments in the function if exists
   424  		// ignore 2 first parameters (context and state)
   425  		if params.Len() == 3 {
   426  			arg := params.At(2)
   427  			arguments, argumentTypeName, err := sp.parseArgumentTypes(rawSchema, arg.Type(), []string{})
   428  			if err != nil {
   429  				return err
   430  			}
   431  			opInfo.ArgumentsType = argumentTypeName
   432  			opInfo.Arguments = arguments
   433  		}
   434  
   435  		resultType, err := sp.parseType(rawSchema, nil, resultTuple.At(0).Type(), []string{}, false)
   436  		if err != nil {
   437  			return err
   438  		}
   439  		opInfo.ResultType = resultType
   440  
   441  		switch opInfo.Kind {
   442  		case OperationProcedure:
   443  			rawSchema.Procedures = append(rawSchema.Procedures, ProcedureInfo(*opInfo))
   444  		case OperationFunction:
   445  			rawSchema.Functions = append(rawSchema.Functions, FunctionInfo(*opInfo))
   446  		}
   447  	}
   448  	return nil
   449  }
   450  
   451  func (sp *SchemaParser) parseArgumentTypes(rawSchema *RawConnectorSchema, ty types.Type, fieldPaths []string) (map[string]ArgumentInfo, string, error) {
   452  
   453  	switch inferredType := ty.(type) {
   454  	case *types.Pointer:
   455  		return sp.parseArgumentTypes(rawSchema, inferredType.Elem(), fieldPaths)
   456  	case *types.Struct:
   457  		result := make(map[string]ArgumentInfo)
   458  		for i := 0; i < inferredType.NumFields(); i++ {
   459  			fieldVar := inferredType.Field(i)
   460  			fieldTag := inferredType.Tag(i)
   461  			fieldPackage := fieldVar.Pkg()
   462  			var typeInfo *TypeInfo
   463  			if fieldPackage != nil {
   464  				typeInfo = &TypeInfo{
   465  					PackageName: fieldPackage.Name(),
   466  					PackagePath: fieldPackage.Path(),
   467  				}
   468  			}
   469  			fieldType, err := sp.parseType(rawSchema, typeInfo, fieldVar.Type(), append(fieldPaths, fieldVar.Name()), false)
   470  			if err != nil {
   471  				return nil, "", err
   472  			}
   473  			fieldName := getFieldNameOrTag(fieldVar.Name(), fieldTag)
   474  			if fieldType.TypeAST == nil {
   475  				fieldType.TypeAST = fieldVar.Type()
   476  			}
   477  			result[fieldName] = ArgumentInfo{
   478  				FieldName: fieldVar.Name(),
   479  				Type:      fieldType,
   480  			}
   481  		}
   482  		return result, "", nil
   483  	case *types.Named:
   484  		arguments, _, err := sp.parseArgumentTypes(rawSchema, inferredType.Obj().Type().Underlying(), append(fieldPaths, inferredType.Obj().Name()))
   485  		if err != nil {
   486  			return nil, "", err
   487  		}
   488  		return arguments, inferredType.Obj().Name(), nil
   489  	default:
   490  		return nil, "", fmt.Errorf("expected struct type, got %s", ty.String())
   491  	}
   492  }
   493  
   494  func (sp *SchemaParser) parseType(rawSchema *RawConnectorSchema, rootType *TypeInfo, ty types.Type, fieldPaths []string, skipNullable bool) (*TypeInfo, error) {
   495  
   496  	switch inferredType := ty.(type) {
   497  	case *types.Pointer:
   498  		if skipNullable {
   499  			return sp.parseType(rawSchema, rootType, inferredType.Elem(), fieldPaths, false)
   500  		}
   501  		innerType, err := sp.parseType(rawSchema, rootType, inferredType.Elem(), fieldPaths, false)
   502  		if err != nil {
   503  			return nil, err
   504  		}
   505  		return &TypeInfo{
   506  			Name:          innerType.Name,
   507  			SchemaName:    innerType.Name,
   508  			Description:   innerType.Description,
   509  			PackagePath:   innerType.PackagePath,
   510  			PackageName:   innerType.PackageName,
   511  			TypeAST:       ty,
   512  			TypeFragments: append([]string{"*"}, innerType.TypeFragments...),
   513  			IsScalar:      innerType.IsScalar,
   514  			Schema:        schema.NewNullableType(innerType.Schema),
   515  		}, nil
   516  	case *types.Struct:
   517  		isAnonymous := false
   518  		if rootType == nil {
   519  			rootType = &TypeInfo{}
   520  		}
   521  
   522  		name := strings.Join(fieldPaths, "")
   523  		if rootType.Name == "" {
   524  			rootType.Name = name
   525  			isAnonymous = true
   526  			rootType.TypeFragments = append(rootType.TypeFragments, ty.String())
   527  		}
   528  		if rootType.SchemaName == "" {
   529  			rootType.SchemaName = name
   530  		}
   531  		if rootType.TypeAST == nil {
   532  			rootType.TypeAST = ty
   533  		}
   534  
   535  		if rootType.Schema == nil {
   536  			rootType.Schema = schema.NewNamedType(name)
   537  		}
   538  		objType := schema.ObjectType{
   539  			Description: rootType.Description,
   540  			Fields:      make(schema.ObjectTypeFields),
   541  		}
   542  		objFields := &ObjectInfo{
   543  			PackagePath: rootType.PackagePath,
   544  			PackageName: rootType.PackageName,
   545  			IsAnonymous: isAnonymous,
   546  			Fields:      map[string]*ObjectField{},
   547  		}
   548  		for i := 0; i < inferredType.NumFields(); i++ {
   549  			fieldVar := inferredType.Field(i)
   550  			fieldTag := inferredType.Tag(i)
   551  			fieldType, err := sp.parseType(rawSchema, nil, fieldVar.Type(), append(fieldPaths, fieldVar.Name()), false)
   552  			if err != nil {
   553  				return nil, err
   554  			}
   555  			fieldKey := getFieldNameOrTag(fieldVar.Name(), fieldTag)
   556  			objType.Fields[fieldKey] = schema.ObjectField{
   557  				Type: fieldType.Schema.Encode(),
   558  			}
   559  			objFields.Fields[fieldVar.Name()] = &ObjectField{
   560  				Name: fieldVar.Name(),
   561  				Key:  fieldKey,
   562  				Type: fieldType,
   563  			}
   564  		}
   565  		rawSchema.ObjectSchemas[rootType.Name] = objType
   566  		rawSchema.Objects[rootType.Name] = objFields
   567  
   568  		return rootType, nil
   569  	case *types.Named:
   570  
   571  		innerType := inferredType.Obj()
   572  		if innerType == nil {
   573  			return nil, fmt.Errorf("failed to parse named type: %s", inferredType.String())
   574  		}
   575  		typeInfo, err := sp.parseTypeInfoFromComments(innerType.Name(), innerType.Parent())
   576  		if err != nil {
   577  			return nil, err
   578  		}
   579  		innerPkg := innerType.Pkg()
   580  
   581  		if innerPkg != nil {
   582  			var scalarName ScalarName
   583  			typeInfo.PackageName = innerPkg.Name()
   584  			typeInfo.PackagePath = innerPkg.Path()
   585  			scalarSchema := schema.NewScalarType()
   586  
   587  			switch innerPkg.Path() {
   588  			case "time":
   589  				switch innerType.Name() {
   590  				case "Time":
   591  					scalarName = ScalarTimestampTZ
   592  					scalarSchema.Representation = schema.NewTypeRepresentationTimestampTZ().Encode()
   593  				case "Duration":
   594  					return nil, errors.New("unsupported type time.Duration. Create a scalar type wrapper with FromValue method to decode the any value")
   595  				}
   596  			case "encoding/json":
   597  				switch innerType.Name() {
   598  				case "RawMessage":
   599  					scalarName = ScalarRawJSON
   600  					scalarSchema.Representation = schema.NewTypeRepresentationJSON().Encode()
   601  				}
   602  			case "github.com/google/uuid":
   603  				switch innerType.Name() {
   604  				case "UUID":
   605  					scalarName = ScalarUUID
   606  					scalarSchema.Representation = schema.NewTypeRepresentationUUID().Encode()
   607  				}
   608  			case "github.com/hasura/ndc-sdk-go/scalar":
   609  				scalarName = ScalarName(innerType.Name())
   610  				switch innerType.Name() {
   611  				case "Date":
   612  					scalarSchema.Representation = schema.NewTypeRepresentationDate().Encode()
   613  				case "BigInt":
   614  					scalarSchema.Representation = schema.NewTypeRepresentationBigInteger().Encode()
   615  				case "Bytes":
   616  					scalarSchema.Representation = schema.NewTypeRepresentationBytes().Encode()
   617  				}
   618  			}
   619  
   620  			if scalarName != "" {
   621  				typeInfo.IsScalar = true
   622  				typeInfo.Schema = schema.NewNamedType(string(scalarName))
   623  				rawSchema.ScalarSchemas[string(scalarName)] = *scalarSchema
   624  				return typeInfo, nil
   625  			}
   626  		}
   627  
   628  		if typeInfo.IsScalar {
   629  			rawSchema.CustomScalars[typeInfo.Name] = typeInfo
   630  			scalarSchema := schema.NewScalarType()
   631  			if typeInfo.ScalarRepresentation != nil {
   632  				scalarSchema.Representation = typeInfo.ScalarRepresentation
   633  			} else {
   634  				// requires representation since NDC spec v0.1.2
   635  				scalarSchema.Representation = schema.NewTypeRepresentationJSON().Encode()
   636  			}
   637  			rawSchema.ScalarSchemas[typeInfo.SchemaName] = *scalarSchema
   638  			return typeInfo, nil
   639  		}
   640  
   641  		return sp.parseType(rawSchema, typeInfo, innerType.Type().Underlying(), append(fieldPaths, innerType.Name()), false)
   642  	case *types.Basic:
   643  		var scalarName ScalarName
   644  		switch inferredType.Kind() {
   645  		case types.Bool:
   646  			scalarName = ScalarBoolean
   647  			rawSchema.ScalarSchemas[string(scalarName)] = defaultScalarTypes[scalarName]
   648  		case types.Int8, types.Uint8:
   649  			scalarName = ScalarInt8
   650  			rawSchema.ScalarSchemas[string(scalarName)] = defaultScalarTypes[scalarName]
   651  		case types.Int16, types.Uint16:
   652  			scalarName = ScalarInt16
   653  			rawSchema.ScalarSchemas[string(scalarName)] = defaultScalarTypes[scalarName]
   654  		case types.Int, types.Int32, types.Uint, types.Uint32:
   655  			scalarName = ScalarInt32
   656  			rawSchema.ScalarSchemas[string(scalarName)] = defaultScalarTypes[scalarName]
   657  		case types.Int64, types.Uint64:
   658  			scalarName = ScalarInt64
   659  			rawSchema.ScalarSchemas[string(scalarName)] = defaultScalarTypes[scalarName]
   660  		case types.Float32:
   661  			scalarName = ScalarFloat32
   662  			rawSchema.ScalarSchemas[string(scalarName)] = defaultScalarTypes[scalarName]
   663  		case types.Float64:
   664  			scalarName = ScalarFloat64
   665  			rawSchema.ScalarSchemas[string(scalarName)] = defaultScalarTypes[scalarName]
   666  		case types.String:
   667  			scalarName = ScalarString
   668  			rawSchema.ScalarSchemas[string(scalarName)] = defaultScalarTypes[scalarName]
   669  		default:
   670  			return nil, fmt.Errorf("unsupported scalar type: %s", inferredType.String())
   671  		}
   672  		if rootType == nil {
   673  			rootType = &TypeInfo{
   674  				Name:          inferredType.Name(),
   675  				SchemaName:    inferredType.Name(),
   676  				TypeFragments: []string{inferredType.Name()},
   677  				TypeAST:       ty,
   678  			}
   679  		}
   680  
   681  		rootType.Schema = schema.NewNamedType(string(scalarName))
   682  		rootType.IsScalar = true
   683  
   684  		return rootType, nil
   685  	case *types.Array:
   686  		innerType, err := sp.parseType(rawSchema, nil, inferredType.Elem(), fieldPaths, false)
   687  		if err != nil {
   688  			return nil, err
   689  		}
   690  		innerType.TypeFragments = append([]string{"[]"}, innerType.TypeFragments...)
   691  		innerType.Schema = schema.NewArrayType(innerType.Schema)
   692  		return innerType, nil
   693  	case *types.Slice:
   694  		innerType, err := sp.parseType(rawSchema, nil, inferredType.Elem(), fieldPaths, false)
   695  		if err != nil {
   696  			return nil, err
   697  		}
   698  
   699  		innerType.TypeFragments = append([]string{"[]"}, innerType.TypeFragments...)
   700  		innerType.Schema = schema.NewArrayType(innerType.Schema)
   701  		return innerType, nil
   702  	case *types.Map, *types.Interface:
   703  		scalarName := ScalarJSON
   704  		if rootType == nil {
   705  			rootType = &TypeInfo{
   706  				Name:       inferredType.String(),
   707  				SchemaName: string(scalarName),
   708  				TypeAST:    ty,
   709  			}
   710  		}
   711  		rootType.TypeFragments = append(rootType.TypeFragments, inferredType.String())
   712  		rootType.Schema = schema.NewNamedType(string(scalarName))
   713  		rootType.IsScalar = true
   714  
   715  		return rootType, nil
   716  	default:
   717  		return nil, fmt.Errorf("unsupported type: %s", ty.String())
   718  	}
   719  }
   720  
   721  func (sp *SchemaParser) parseTypeInfoFromComments(typeName string, scope *types.Scope) (*TypeInfo, error) {
   722  	typeInfo := &TypeInfo{
   723  		Name:          typeName,
   724  		SchemaName:    typeName,
   725  		IsScalar:      false,
   726  		TypeFragments: []string{typeName},
   727  		Schema:        schema.NewNamedType(typeName),
   728  	}
   729  	comments := make([]string, 0)
   730  	commentGroup := findCommentsFromPos(sp.pkg, scope, typeName)
   731  
   732  	if commentGroup != nil {
   733  		for i, line := range commentGroup.List {
   734  			text := strings.TrimSpace(strings.TrimLeft(line.Text, "/"))
   735  			if text == "" {
   736  				continue
   737  			}
   738  			if i == 0 {
   739  				text = strings.TrimPrefix(text, fmt.Sprintf("%s ", typeName))
   740  			}
   741  
   742  			enumMatches := ndcEnumCommentRegex.FindStringSubmatch(text)
   743  
   744  			if len(enumMatches) == 2 {
   745  				typeInfo.IsScalar = true
   746  				rawEnumItems := strings.Split(enumMatches[1], ",")
   747  				var enums []string
   748  				for _, item := range rawEnumItems {
   749  					trimmed := strings.TrimSpace(item)
   750  					if trimmed != "" {
   751  						enums = append(enums, trimmed)
   752  					}
   753  				}
   754  				if len(enums) == 0 {
   755  					return nil, fmt.Errorf("require enum values in the comment of %s", typeName)
   756  				}
   757  				typeInfo.ScalarRepresentation = schema.NewTypeRepresentationEnum(enums).Encode()
   758  				continue
   759  			}
   760  
   761  			matches := ndcScalarCommentRegex.FindStringSubmatch(text)
   762  			matchesLen := len(matches)
   763  			if matchesLen > 1 {
   764  				typeInfo.IsScalar = true
   765  				if matchesLen > 3 && matches[3] != "" {
   766  					typeInfo.SchemaName = matches[2]
   767  					typeInfo.Schema = schema.NewNamedType(matches[2])
   768  					typeRep, err := schema.ParseTypeRepresentationType(strings.TrimSpace(matches[3]))
   769  					if err != nil {
   770  						return nil, fmt.Errorf("failed to parse type representation of scalar %s: %s", typeName, err)
   771  					}
   772  					if typeRep == schema.TypeRepresentationTypeEnum {
   773  						return nil, errors.New("use @enum tag with values instead")
   774  					}
   775  					typeInfo.ScalarRepresentation = schema.TypeRepresentation{
   776  						"type": typeRep,
   777  					}
   778  				} else if matchesLen > 2 && matches[2] != "" {
   779  					// if the second string is a type representation, use it as a TypeRepresentation instead
   780  					// e.g @scalar string
   781  					typeRep, err := schema.ParseTypeRepresentationType(matches[2])
   782  					if err == nil {
   783  						if typeRep == schema.TypeRepresentationTypeEnum {
   784  							return nil, errors.New("use @enum tag with values instead")
   785  						}
   786  						typeInfo.ScalarRepresentation = schema.TypeRepresentation{
   787  							"type": typeRep,
   788  						}
   789  						continue
   790  					}
   791  
   792  					typeInfo.SchemaName = matches[2]
   793  					typeInfo.Schema = schema.NewNamedType(matches[2])
   794  				}
   795  				continue
   796  			}
   797  
   798  			comments = append(comments, text)
   799  		}
   800  	}
   801  
   802  	if !typeInfo.IsScalar {
   803  		// fallback to parse scalar from type name with Scalar prefix
   804  		matches := ndcScalarNameRegex.FindStringSubmatch(typeName)
   805  		if len(matches) > 1 {
   806  			typeInfo.IsScalar = true
   807  			typeInfo.SchemaName = matches[1]
   808  			typeInfo.Schema = schema.NewNamedType(matches[1])
   809  		}
   810  	}
   811  
   812  	desc := strings.Join(comments, " ")
   813  	if desc != "" {
   814  		typeInfo.Description = &desc
   815  	}
   816  
   817  	return typeInfo, nil
   818  }
   819  
   820  func (sp *SchemaParser) parseOperationInfo(fn *types.Func) *OperationInfo {
   821  	functionName := fn.Name()
   822  	result := OperationInfo{
   823  		OriginName: functionName,
   824  		Arguments:  make(map[string]ArgumentInfo),
   825  	}
   826  
   827  	var descriptions []string
   828  	commentGroup := findCommentsFromPos(sp.pkg, fn.Scope(), functionName)
   829  	if commentGroup != nil {
   830  		for i, comment := range commentGroup.List {
   831  			text := strings.TrimSpace(strings.TrimLeft(comment.Text, "/"))
   832  
   833  			// trim the function name in the first line if exists
   834  			if i == 0 {
   835  				text = strings.TrimPrefix(text, fmt.Sprintf("%s ", functionName))
   836  			}
   837  			matches := ndcOperationCommentRegex.FindStringSubmatch(text)
   838  			matchesLen := len(matches)
   839  			if matchesLen > 1 {
   840  				switch matches[1] {
   841  				case strings.ToLower(string(OperationFunction)):
   842  					result.Kind = OperationFunction
   843  				case strings.ToLower(string(OperationProcedure)):
   844  					result.Kind = OperationProcedure
   845  				default:
   846  					log.Debug().Msgf("unsupported operation kind: %s", matches)
   847  				}
   848  
   849  				if matchesLen > 3 && strings.TrimSpace(matches[3]) != "" {
   850  					result.Name = strings.TrimSpace(matches[3])
   851  				} else {
   852  					result.Name = ToCamelCase(functionName)
   853  				}
   854  			} else {
   855  				descriptions = append(descriptions, text)
   856  			}
   857  		}
   858  	}
   859  
   860  	// try to parse function with following prefixes:
   861  	// - FunctionXxx as a query function
   862  	// - ProcedureXxx as a mutation procedure
   863  	if result.Kind == "" {
   864  		operationNameResults := ndcOperationNameRegex.FindStringSubmatch(functionName)
   865  		if len(operationNameResults) < 3 {
   866  			return nil
   867  		}
   868  		result.Kind = OperationKind(operationNameResults[1])
   869  		result.Name = ToCamelCase(operationNameResults[2])
   870  	}
   871  
   872  	desc := strings.TrimSpace(strings.Join(descriptions, " "))
   873  	if desc != "" {
   874  		result.Description = &desc
   875  	}
   876  
   877  	return &result
   878  }
   879  
   880  func findCommentsFromPos(pkg *packages.Package, scope *types.Scope, name string) *ast.CommentGroup {
   881  	for _, f := range pkg.Syntax {
   882  		for _, cg := range f.Comments {
   883  			if len(cg.List) == 0 {
   884  				continue
   885  			}
   886  			exp := regexp.MustCompile(fmt.Sprintf(`^//\s+%s`, name))
   887  			if !exp.MatchString(cg.List[0].Text) {
   888  				continue
   889  			}
   890  			if _, obj := scope.LookupParent(name, cg.Pos()); obj != nil {
   891  				return cg
   892  			}
   893  		}
   894  	}
   895  	return nil
   896  }
   897  
   898  // get field name by json tag
   899  // return the struct field name if not exist
   900  func getFieldNameOrTag(name string, tag string) string {
   901  	if tag == "" {
   902  		return name
   903  	}
   904  	tags, err := structtag.Parse(tag)
   905  	if err != nil {
   906  		log.Warn().Err(err).Msgf("failed to parse tag of struct field: %s", name)
   907  		return name
   908  	}
   909  
   910  	jsonTag, err := tags.Get("json")
   911  	if err != nil {
   912  		log.Warn().Err(err).Msgf("json tag does not exist in struct field: %s", name)
   913  		return name
   914  	}
   915  
   916  	return jsonTag.Name
   917  }