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 }