Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/jhump/protoreflect v1.8.2
github.com/klauspost/cpuid/v2 v2.2.9
github.com/stretchr/testify v1.9.0
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
google.golang.org/protobuf v1.33.0
)

Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
90 changes: 90 additions & 0 deletions thrift/idl.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,31 @@ type compilingInstance struct {

type compilingCache map[string]*compilingInstance

func thriftNamespace(ast *parser.Thrift) string {
if ast == nil || ast.Namespaces == nil || len(ast.Namespaces) == 0 {
return ""
}
var any, star string
for _, ns := range ast.Namespaces {
if ns == nil || ns.Name == "" {
continue
}
if ns.Language == "go" {
return ns.Name
}
if ns.Language == "*" {
star = ns.Name
}
if any == "" {
any = ns.Name
}
}
if star != "" {
return star
}
return any
}

// arg cache:
// only support self reference on the same file
// cross file self reference complicate matters
Expand Down Expand Up @@ -924,3 +949,68 @@ func makeDefaultValue(typ *TypeDescriptor, val *parser.ConstValue, tree *parser.
}
return nil, nil
}

// NewDescriptorByName parse thrift IDL and return the specified (file+typeName) type descriptor.
// The includes is the thrift file content map, and its keys are specific including thrift file path, values are the thrift file content.
// file is the main thrift file path, name is the type name to parse (supports format like "package.TypeName" for cross-file references).
// Returns a complete TypeDescriptor with all referenced types resolved.
func (opts Options) NewDescriptorByName(ctx context.Context, file string, name string, includes map[string]string) (*TypeDescriptor, error) {
// Parse the main IDL file and all includes
tree, err := parseIDLContent(file, includes[file], includes)
if err != nil {
return nil, err
}

// Resolve all symbols in the AST
if err := semantic.ResolveSymbols(tree); err != nil {
return nil, err
}

// Create a compilingCache to handle recursive type references
structsCache := compilingCache{}

// Parse the type using the parseType function
// First, we need to create a parser.Type that matches the requested type name
typePkg, typeName := util.SplitSubfix(name)
var targetTree *parser.Thrift = tree

// If cross-file reference, resolve the file reference
if typePkg != "" {
ref, ok := tree.GetReference(typePkg)
if !ok {
return nil, fmt.Errorf("miss reference: %s in file: %s", typePkg, file)
}
targetTree = ref
// Reset cache for cross-file reference
structsCache = compilingCache{}
}

// Verify the type exists in the target file
if _, ok := targetTree.GetStruct(typeName); !ok {
if _, ok := targetTree.GetUnion(typeName); !ok {
if _, ok := targetTree.GetException(typeName); !ok {
if _, ok := targetTree.GetTypedef(typeName); !ok {
if _, ok := targetTree.GetEnum(typeName); !ok {
return nil, fmt.Errorf("missing type: %s in file: %s", name, file)
}
// Handle enum type - return as i32 or i64 based on options
if opts.ParseEnumAsInt64 {
return builtinTypes["i64"], nil
}
return builtinTypes["i32"], nil
}
// Handle typedef - recursively parse the underlying type
typDef, _ := targetTree.GetTypedef(typeName)
return parseType(ctx, typDef.Type, targetTree, structsCache, 0, opts, nil, Request)
}
}
}

// Create a parser.Type for the target type
parserType := &parser.Type{
Name: name,
}

// Parse the type descriptor
return parseType(ctx, parserType, tree, structsCache, 0, opts, nil, Request)
}
166 changes: 166 additions & 0 deletions thrift/idl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,169 @@ func TestParseWithServiceName(t *testing.T) {
require.Nil(t, p)
require.Equal(t, err.Error(), "the idl service name UnknownService is not in the idl. Please check your idl")
}

func TestNewDescriptorByName(t *testing.T) {
t.Run("WithIncludes", func(t *testing.T) {
// Test cross-file struct parsing
mainPath := "main.thrift"
mainContent := `
namespace go kitex.test.server
include "base.thrift"

struct UserRequest {
1: required string username
2: base.User user
}
`

basePath := "base.thrift"
baseContent := `
namespace go kitex.test.server

struct User {
1: required i64 id
2: required string name
3: optional string email
}
`

includes := map[string]string{
mainPath: mainContent,
basePath: baseContent,
}

opts := Options{}

// Test parsing UserRequest which references User from another file
desc, err := opts.NewDescriptorByName(context.Background(), mainPath, "UserRequest", includes)
require.NoError(t, err)
require.NotNil(t, desc)
require.Equal(t, STRUCT, desc.Type())
require.Equal(t, "UserRequest", desc.Struct().Name())

// Verify fields
usernameField := desc.Struct().FieldByKey("username")
require.NotNil(t, usernameField)
require.Equal(t, STRING, usernameField.Type().Type())

userField := desc.Struct().FieldByKey("user")
require.NotNil(t, userField)
require.Equal(t, STRUCT, userField.Type().Type())
require.Equal(t, "User", userField.Type().Struct().Name())

// Verify nested User struct fields
userStruct := userField.Type().Struct()
idField := userStruct.FieldByKey("id")
require.NotNil(t, idField)
require.Equal(t, I64, idField.Type().Type())

nameField := userStruct.FieldByKey("name")
require.NotNil(t, nameField)
require.Equal(t, STRING, nameField.Type().Type())

emailField := userStruct.FieldByKey("email")
require.NotNil(t, emailField)
require.Equal(t, STRING, emailField.Type().Type())
})

t.Run("PackageNotation", func(t *testing.T) {
// Test cross-file reference using package notation (e.g., "base.User")
mainPath := "main.thrift"
mainContent := `
namespace go kitex.test.server
include "base.thrift"
`

basePath := "base.thrift"
baseContent := `
namespace go kitex.test.base

struct User {
1: required i64 id
2: required string name
}
`

includes := map[string]string{
mainPath: mainContent,
basePath: baseContent,
}

opts := Options{}

// Test parsing User struct with cross-file reference notation
desc, err := opts.NewDescriptorByName(context.Background(), mainPath, "base.User", includes)

require.NoError(t, err)
require.NotNil(t, desc)
require.Equal(t, STRUCT, desc.Type())
require.Equal(t, "User", desc.Struct().Name())

// Verify fields
idField := desc.Struct().FieldByKey("id")
require.NotNil(t, idField)
require.Equal(t, I64, idField.Type().Type())
})

// Test with mixed relative and absolute paths
t.Run("WithMixedPaths", func(t *testing.T) {
mainPath := "a/b/main.thrift"
mainContent := `
namespace go kitex.test.server
include "../c/base.thrift"
include "a/b/ref.thrift"

struct UserRequest {
1: required string username
2: base.User user
3: ref.Placeholder placeholder
}
`

basePath := "a/c/base.thrift"
baseContent := `
namespace go kitex.test.server

struct User {
1: required i64 id
2: required string name
}
`
refPath := "a/b/ref.thrift"
refContent := `
namespace go kitex.test.server

struct Placeholder {
1: required string info
}
`

includes := map[string]string{
mainPath: mainContent,
basePath: baseContent,
refPath: refContent,
}

opts := Options{}

// Test parsing UserRequest with relative path that goes up directories
desc, err := opts.NewDescriptorByName(context.Background(), mainPath, "UserRequest", includes)

require.NoError(t, err)
require.NotNil(t, desc)
require.Equal(t, STRUCT, desc.Type())
require.Equal(t, "UserRequest", desc.Struct().Name())

// Verify nested User struct is correctly resolved via relative path
userField := desc.Struct().FieldByKey("user")
require.NotNil(t, userField)
require.Equal(t, STRUCT, userField.Type().Type())
require.Equal(t, "User", userField.Type().Struct().Name())

// Verify nested Placeholder struct is correctly resolved via direct relative path
placeholderField := desc.Struct().FieldByKey("placeholder")
require.NotNil(t, placeholderField)
require.Equal(t, STRUCT, placeholderField.Type().Type())
require.Equal(t, "Placeholder", placeholderField.Type().Struct().Name())
})
}
Loading