Skip to content

Commit

Permalink
Proto field unwrapping.
Browse files Browse the repository at this point in the history
  • Loading branch information
Dmitriy Garanzha committed Jan 23, 2019
1 parent 644ade1 commit f0d9cbf
Show file tree
Hide file tree
Showing 14 changed files with 449 additions and 94 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
vendor
.idea
tests/dataloader/generated/
tests/*/generated/
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ proto2gql:
"methodName": # method name
alias: "methodAlias"
request_type: "QUERY" # method type in GraphQL Schema (QUERY|MUTATION)
unwrap_response_field: true # In proto we can't use primitive or repeated type in method response.
# If unwrap_response_field = true unpack response gql object with 1 field.
messages: # messages settings
- "Request$": # message name match regex
fields:
Expand Down Expand Up @@ -364,7 +366,9 @@ Default wait duration 10ms.

Full example can be found in [tests](https://github.com/EGT-Ukraine/go2gql/tree/master/tests/dataloader).

## UPGRADE FROM 1.x to 2.0
## Note to users migrating from older releases

### Migrating from 1.x to 2.0

### Swagger plugin

Expand All @@ -377,7 +381,7 @@ Use `queries_service_name` & `mutations_service_name` instead.

Use `queries_service_name` & `mutations_service_name` instead.

## UPGRADE FROM 2.x to 3.0
## Migrating from 2.x to 3.0

### Swagger plugin

Expand All @@ -390,8 +394,6 @@ Use `service_name` instead.
Use `service_name` instead.


## Note to users migrating from older releases

### Migrating from 3.x to 4.0
tracer.Tracer was replaced with opentracing.Tracer

Expand Down
2 changes: 1 addition & 1 deletion generator/plugins/graphql/templates.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions generator/plugins/proto2gql/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ type MessageConfig struct {
DataLoaders []dataloader.DataLoaderFieldConfig `mapstructure:"data_loaders"`
}
type MethodConfig struct {
Alias string `mapstructure:"alias"`
RequestType string `mapstructure:"request_type"` // QUERY | MUTATION
DataLoaderProvider dataloader.DataLoaderProviderConfig `mapstructure:"data_loader_provider"`
Alias string `mapstructure:"alias"`
RequestType string `mapstructure:"request_type"` // QUERY | MUTATION
DataLoaderProvider dataloader.DataLoaderProviderConfig `mapstructure:"data_loader_provider"`
UnwrapResponseField bool `mapstructure:"unwrap_response_field"`
}
type ServiceConfig struct {
ServiceName string `mapstructure:"service_name"`
Expand Down
162 changes: 137 additions & 25 deletions generator/plugins/proto2gql/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,30 @@ func (g Proto2GraphQL) serviceMethod(sc ServiceConfig, cfg MethodConfig, file *p
if err != nil {
return nil, errors.Wrap(err, "failed to resolve file type file")
}
outType, err := g.TypeOutputTypeResolver(outputMsgTypeFile, method.OutputMessage)

if cfg.UnwrapResponseField && len(method.OutputMessage.Fields) != 1 {
return nil, errors.Errorf("can't unwrap `%s` service `%s` method response. Output message must have 1 field.", method.Service.Name, method.Name)
}

var outProtoType parser.Type
var outProtoTypeRepeated bool

if cfg.UnwrapResponseField {
outProtoType = method.OutputMessage.Fields[0].Type
outProtoTypeRepeated = method.OutputMessage.Fields[0].Repeated
} else {
outProtoType = method.OutputMessage
}

outType, err := g.TypeOutputTypeResolver(outputMsgTypeFile, outProtoType)
if err != nil {
return nil, errors.Wrapf(err, "failed to get output type resolver for method: %s", method.Name)
}

if outProtoTypeRepeated {
outType = graphql.GqlListTypeResolver(graphql.GqlNonNullTypeResolver(outType))
}

requestType, err := g.goTypeByParserType(method.InputMessage)
if err != nil {
return nil, errors.Wrapf(err, "failed to get request go type for method: %s", method.Name)
Expand All @@ -177,14 +197,41 @@ func (g Proto2GraphQL) serviceMethod(sc ServiceConfig, cfg MethodConfig, file *p
return nil, errors.Wrap(err, "failed add data loader provider")
}

clientMethodCaller := func(client, arg string, ctx graphql.BodyContext) string {
return client + "." + camelCase(method.Name) + "(ctx," + arg + ")"
}

if len(method.OutputMessage.Fields) == 1 && !cfg.UnwrapResponseField {
fmt.Printf(
"Suggestion: service `%s` method `%s` in file `%s` has 1 output field. Can be unwrapped.\n",
method.Service.Name,
method.Name,
file.File.FilePath,
)
}

if cfg.UnwrapResponseField {
unwrapFieldName := camelCase(method.OutputMessage.Fields[0].Name)

clientMethodCaller = func(client, arg string, ctx graphql.BodyContext) string {
return `func() (interface{}, error) {
res, err := ` + client + "." + camelCase(method.Name) + `(ctx,` + arg + `)
if err != nil {
return nil, err
}
return res.` + unwrapFieldName + `, nil
}()`
}
}

return &graphql.Method{
Name: g.methodName(cfg, method),
QuotedComment: method.QuotedComment,
GraphQLOutputType: outType,
RequestType: requestType,
ClientMethodCaller: func(client, arg string, ctx graphql.BodyContext) string {
return client + "." + camelCase(method.Name) + "(ctx," + arg + ")"
},
Name: g.methodName(cfg, method),
QuotedComment: method.QuotedComment,
GraphQLOutputType: outType,
RequestType: requestType,
ClientMethodCaller: clientMethodCaller,
RequestResolver: valueResolver,
RequestResolverWithErr: valueResolverWithErr,
Arguments: args,
Expand All @@ -204,7 +251,34 @@ func (g Proto2GraphQL) addDataLoaderProvider(sc ServiceConfig, cfg MethodConfig,
return errors.New("Method " + method.Name + " must have 1 response argument")
}

responseGoType, err := g.goTypeByParserType(method.OutputMessage.Fields[0].Type)
var outProtoType *parser.Field

if cfg.UnwrapResponseField {
if len(method.OutputMessage.Fields) != 1 {
return errors.Errorf("response field unwrapping failed for method: %s. Output message must have 1 field", method.Name)
}

_, ok := method.OutputMessage.Fields[0].Type.(*parser.Message)

if !ok {
return errors.Errorf("can't unwrap %s method. Response must be message", method.Name)
}

outProtoType = method.OutputMessage.Fields[0].Type.(*parser.Message).Fields[0]
} else {
outProtoType = method.OutputMessage.Fields[0]
}

responseGoType, err := g.goTypeByParserType(outProtoType.Type)

if cfg.UnwrapResponseField && outProtoType.Repeated {
elementGoType := responseGoType

responseGoType = graphql.GoType{
Kind: reflect.Slice,
ElemType: &elementGoType,
}
}

if err != nil {
return err
Expand Down Expand Up @@ -234,31 +308,24 @@ func (g Proto2GraphQL) addDataLoaderProvider(sc ServiceConfig, cfg MethodConfig,
return errors.Wrap(err, "failed to resolve file type file")
}

dataLoaderOutType, err := g.TypeOutputTypeResolver(outputMsgTypeFile, method.OutputMessage.Fields[0].Type)
dataLoaderOutType, err := g.TypeOutputTypeResolver(outputMsgTypeFile, outProtoType.Type)
if err != nil {
return errors.Wrap(err, "failed to resolve output type")
}

fetchCode := g.dataLoaderFetchCode(file, method)

if cfg.UnwrapResponseField && outProtoType.Repeated {
dataLoaderOutType = graphql.GqlListTypeResolver(graphql.GqlNonNullTypeResolver(dataLoaderOutType))
fetchCode = g.dataLoaderFetchCodeUnwrappedSlice(file, method, responseGoType, outProtoType)
}

dataLoaderProvider := dataloader.LoaderModel{
Service: &dataloader.Service{
Name: g.serviceName(sc, svc),
CallInterface: g.serviceCallInterface(file, svc.Name),
},
FetchCode: func(importer *importer.Importer) string {
requestTypeName := method.InputMessage.Name
requestFieldName := camelCase(method.InputMessage.Fields[0].Name)
responseFieldName := camelCase(method.OutputMessage.Fields[0].Name)

return `
request := &` + importer.Prefix(file.GRPCSourcesPkg) + requestTypeName + `{
` + requestFieldName + `: keys,
}
response, err := client.` + method.Name + `(ctx, request)
return response.` + responseFieldName + `, []error{err}
`
},
FetchCode: fetchCode,
InputGoType: graphql.GoType{
Kind: reflect.Slice,
ElemType: &inputArgumentGoType,
Expand All @@ -272,6 +339,51 @@ func (g Proto2GraphQL) addDataLoaderProvider(sc ServiceConfig, cfg MethodConfig,

return nil
}
func (g Proto2GraphQL) dataLoaderFetchCode(file *parsedFile, method *parser.Method) func(importer *importer.Importer) string {
return func(importer *importer.Importer) string {
requestTypeName := method.InputMessage.Name
requestFieldName := camelCase(method.InputMessage.Fields[0].Name)
responseFieldName := camelCase(method.OutputMessage.Fields[0].Name)

return `
request := &` + importer.Prefix(file.GRPCSourcesPkg) + requestTypeName + `{
` + requestFieldName + `: keys,
}
response, err := client.` + method.Name + `(ctx, request)
return response.` + responseFieldName + `, []error{err}
`
}
}

func (g Proto2GraphQL) dataLoaderFetchCodeUnwrappedSlice(file *parsedFile, method *parser.Method, responseGoType graphql.GoType, outProtoType *parser.Field) func(importer *importer.Importer) string {
return func(importer *importer.Importer) string {
requestTypeName := method.InputMessage.Name
requestFieldName := camelCase(method.InputMessage.Fields[0].Name)
responseFieldName := camelCase(method.OutputMessage.Fields[0].Name)

responseGoTypeString := responseGoType.String(importer)

outProtoTypeName := camelCase(outProtoType.Name)

return `
request := &` + importer.Prefix(file.GRPCSourcesPkg) + requestTypeName + `{
` + requestFieldName + `: keys,
}
response, err := client.` + method.Name + `(ctx, request)
var normalized []` + responseGoTypeString + `
for _, item := range response.` + responseFieldName + ` {
normalized = append(normalized, item.` + outProtoTypeName + `)
}
return normalized, []error{err}
`
}
}

func (g Proto2GraphQL) serviceQueryMethods(sc ServiceConfig, file *parsedFile, service *parser.Service) ([]graphql.Method, error) {
var res []graphql.Method
Expand Down
1 change: 1 addition & 0 deletions tests/Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
test:
$(MAKE) -C dataloader/
$(MAKE) -C proto_unwrap/
Loading

0 comments on commit f0d9cbf

Please sign in to comment.