Skip to content

Commit

Permalink
Merge pull request #19 from danielvladco/research/grpc-reflection
Browse files Browse the repository at this point in the history
Refactor: change generation logic / add gateway

- Add new alpha gateway functionality
- Refactor folder structure
- Refactor Makefiles
- change generation logic
- add more generation options. Instead of using the proto plugin you have the option to generate graphql using a special binary
- get rid of `gogqlcfg` now there is no reason to use it with gqlgen autobind features
- gql plugin adds documentation to generated graphql schema from protobuf comments
  • Loading branch information
danielvladco authored Nov 30, 2020
2 parents 67d3b4a + 3cf14e1 commit 0645feb
Show file tree
Hide file tree
Showing 71 changed files with 30,740 additions and 5,404 deletions.
Binary file added .DS_Store
Binary file not shown.
48 changes: 31 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
.PHONY: install example

install:
go install github.com/gogo/protobuf/protoc-gen-gogo
protoc --gogo_out=paths=source_relative,Mgoogle/protobuf/descriptor.proto=github.com/gogo/protobuf/protoc-gen-gogo/descriptor:./pb \
-I=${GOPATH}/pkg/mod/ -I=./pb ./pb/*.proto
go install ./protoc-gen-gql
go install ./protoc-gen-gogqlgen
go install ./protoc-gen-gqlgencfg

example:
protoc \
--go_out=plugins=grpc,paths=source_relative:. \
--gqlgencfg_out=paths=source_relative:. \
--gql_out=svcdir=true,paths=source_relative:. \
--gogqlgen_out=paths=source_relative,gogoimport=false:. \
-I=. -I=./example/ ./example/*.proto
# Go tools dependencies
GO_TOOLS := \
google.golang.org/grpc/cmd/protoc-gen-go-grpc \
google.golang.org/protobuf/cmd/protoc-gen-go \
github.com/99designs/gqlgen \
./protoc-gen-gql \
./protoc-gen-gogql

GOPATH := $(shell go env GOPATH)

.PHONY: all
all: all-tools pb/graphql.pb.go

.PHONY: all-tools
all-tools: ${GOPATH}/bin/protoc go-tools

.PHONY: go-tools
go-tools: $(foreach l, ${GO_TOOLS}, ${GOPATH}/bin/$(notdir $(l)))

define LIST_RULE
${GOPATH}/bin/$(notdir $(1)): go.mod
go install $(1)
endef

$(foreach l, $(GO_TOOLS), $(eval $(call LIST_RULE, $(l) )))

pb/graphql.pb.go: ./pb/graphql.proto all-tools
protoc --go_out=paths=source_relative:. ./pb/graphql.proto

${GOPATH}/bin/protoc:
./scripts/install-protoc.sh
37 changes: 13 additions & 24 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Protoc plugins for generating graphql schema
Protoc plugins for generating graphql schema and go graphql code

If you use micro-service architecture with grpc for backend and graphql api gateway for frontend you will find yourself
If you use micro-service architecture with grpc for back-end and graphql api gateway for front-end, you will find yourself
repeating a lot of code for translating from one transport layer to another (which many times may be a source of bugs)

This repository aims to simplify working with grpc trough protocol buffers and graphql by generating code.
Expand All @@ -10,8 +10,7 @@ Install:

```sh
go install github.com/danielvladco/go-proto-gql/protoc-gen-gql
go install github.com/danielvladco/go-proto-gql/protoc-gen-gogqlgen
go install github.com/danielvladco/go-proto-gql/protoc-gen-gqlgencfg
go install github.com/danielvladco/go-proto-gql/protoc-gen-gogql
```

Usage Examples:
Expand All @@ -23,7 +22,7 @@ export PATH=${PATH}:${GOPATH}/bin
```

---
`--gql_out` plugin will generate graphql files with extension .graphqls
`--gql_out` plugin will generate graphql files with extension `.graphqls`
rather than go code which means it can be further used for any other language or framework.

Example:
Expand All @@ -36,38 +35,28 @@ http://github.com/99designs/gqlgen plugin, and map all the generated go types wi
Luckily `--gqlgencfg_out` plugin does exactly this.

---
`--gqlgencfg_out` plugin generates yaml configs that can be used further by the http://github.com/99designs/gqlgen library.

Example:
```sh
protoc --gqlgencfg_out=paths=source_relative:. -I=. -I=./example/ ./example/*.proto
```

The generated go code will work fine if you don't have any `enum`s, `oneof`s or `map`s. For this purpose use `--gogqlgen_out` plugin.

---
`--gogqlgen_out` plugin generates generates methods for implementing
`--gogql_out` plugin generates methods for implementing
`github.com/99designs/gqlgen/graphql.Marshaler` and `github.com/99designs/gqlgen/graphql.Unmarshaler` interfaces. Now proto `enum`s, `oneof`s and `map`s will work fine with graphql.

This plugin also creates convenience methods that will implement generated by the `gqlgen` `MutationResolver` and `QueryResolver` interfaces.

NOTE: to generate with gogo import add `gogoimport=true` as a parameter

Example:
```sh
protoc --gogqlgen_out=gogoimport=false,paths=source_relative:. -I=. -I=./example/ ./example/*.proto
protoc --gogql_out=gogoimport=false,paths=source_relative:. -I=. -I=./example/ ./example/*.proto
```

---
See `/example` folder for more examples.

TODO:
- Create a better implementation of `map`s and `oneof`s.
- Add comments from proto to gql for documentation purposes.
- Add more documentation.
## Gateway (alpha)
A unified gateway is also possible. Right now a gateway can be spawn up
pointing to a list of grpc endpoints (grpc reflection must be enabled on the grpc servers).
The gateway will query the servers for protobuf descriptors and generate a graphql schema abstract tree.
The requests to the gateway will be transformed on the fly to grpc servers without any additional code generation
or writing any code at all. See `examples/gateway` for usage more info.

## Community:
I am one on this. Will be very glad for any contributions so feel free to create issues and forks.
Will be very glad for any contributions so feel free to create issues, forks and PRs.

## License:

Expand Down
121 changes: 121 additions & 0 deletions cmd/gateway/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package main

import (
"context"
"encoding/json"
"flag"
"log"
"net/http"
"os"

"github.com/mitchellh/mapstructure"
"github.com/nautilus/gateway"
"github.com/nautilus/graphql"

"github.com/danielvladco/go-proto-gql/pkg/generator"
"github.com/danielvladco/go-proto-gql/pkg/server"
)

type Config struct {
Endpoints []string `json:"endpoints"`
Playground *bool `json:"playground"`
Address string `json:"address"`
}

var (
configFile = flag.String("cfg", "/opt/config.json", "")
)

func main() {
flag.Parse()

f, err := os.Open(*configFile)
fatalOnErr(err)
cfg := &Config{}
err = json.NewDecoder(f).Decode(cfg)
fatalOnErr(err)
if cfg.Address == "" {
cfg.Address = ":8080"
}
if cfg.Playground == nil {
plg := true
cfg.Playground = &plg
}

caller, descs, _, err := server.NewReflectCaller(cfg.Endpoints)
if err != nil {
log.Fatal(err)
}

gqlDesc, err := generator.NewSchemas(descs, true, true)
fatalOnErr(err)

repo := generator.NewRegistry(gqlDesc)

queryFactory := gateway.QueryerFactory(func(ctx *gateway.PlanningContext, url string) graphql.Queryer {
return server.QueryerLogger{server.NewQueryer(repo, caller)}
})
sources := []*graphql.RemoteSchema{{URL: "url1"}}
sources[0].Schema = gqlDesc.AsGraphql()[0]

//sc, _ := os.Create("schema.graphql")
//defer sc.Close()
//formatter.NewFormatter(sc).FormatSchema(sources[0].Schema)

g, err := gateway.New(sources, gateway.WithQueryerFactory(&queryFactory))
if err != nil {
log.Fatal(err)
}
result := &graphql.IntrospectionQueryResult{}
err = schemaTestLoadQuery(g, graphql.IntrospectionQuery, result, map[string]interface{}{})

//in, _ := os.Create("introspection.json")
//defer in.Close()
//_ = json.NewEncoder(in).Encode(result)

mux := http.NewServeMux()
mux.HandleFunc("/query", g.GraphQLHandler)
if *cfg.Playground {
mux.HandleFunc("/playground", g.PlaygroundHandler)
}
log.Fatal(http.ListenAndServe(cfg.Address, mux))
}

func fatalOnErr(err error) {
if err != nil {
panic(err)
}
}

func schemaTestLoadQuery(qw *gateway.Gateway, query string, target interface{}, variables map[string]interface{}) error {
reqCtx := &gateway.RequestContext{
Context: context.Background(),
Query: query,
Variables: variables,
}
plan, err := qw.GetPlans(reqCtx)
if err != nil {
return err
}

// executing the introspection query should return a full description of the schema
response, err := qw.Execute(reqCtx, plan)
if err != nil {
return err
}

// massage the map into the structure
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
Result: target,
})
if err != nil {
return err
}
err = decoder.Decode(response)
if err != nil {
return err
}

return nil
}
94 changes: 94 additions & 0 deletions cmd/protogql/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"flag"
"log"
"os"
"path"
"path/filepath"
"strings"

"github.com/jhump/protoreflect/desc/protoparse"
"github.com/vektah/gqlparser/v2/formatter"

"github.com/danielvladco/go-proto-gql/pkg/generator"
)

type arrayFlags []string

func (i *arrayFlags) String() string {
return "str list"
}

func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}

var (
importPath = arrayFlags{}
fileNames = arrayFlags{}
svc = flag.Bool("svc", false, "")
merge = flag.Bool("merge", false, "")
)

func main() {
flag.Var(&importPath, "I", "path")
flag.Var(&fileNames, "f", "path")
flag.Parse()

newFileNames, err := protoparse.ResolveFilenames(importPath, fileNames...)
if err != nil {
log.Fatal(err)
}
descs, err := protoparse.Parser{ImportPaths: importPath}.ParseFiles(newFileNames...)
if err != nil {
log.Fatal(err)
}
gqlDesc, err := generator.NewSchemas(descs, *merge, *svc)
if err != nil {
log.Fatal(err)
}
for _, schema := range gqlDesc {
if len(schema.FileDescriptors) < 1 {
log.Fatalf("unexpected number of proto descriptors: %d for gql schema", len(schema.FileDescriptors))
}
if len(schema.FileDescriptors) > 1 {
if err := generateFile(schema, true); err != nil {
log.Fatal(err)
}
break
}
if err := generateFile(schema, *merge); err != nil {
log.Fatal(err)
}
}
}

func generateFile(schema *generator.SchemaDescriptor, merge bool) error {
sc, err := os.Create(resolveGraphqlFilename(schema.FileDescriptors[0].GetName(), merge))
if err != nil {
return err
}
defer sc.Close()

formatter.NewFormatter(sc).FormatSchema(schema.AsGraphql())
return nil
}

func resolveGraphqlFilename(protoFileName string, merge bool) string {
if merge {
gqlFileName := "schema.graphqls"
absProtoFileName, err := filepath.Abs(protoFileName)
if err == nil {
protoDirSlice := strings.Split(filepath.Dir(absProtoFileName), string(filepath.Separator))
if len(protoDirSlice) > 0 {
gqlFileName = protoDirSlice[len(protoDirSlice)-1] + ".graphqls"
}
}
protoDir, _ := path.Split(protoFileName)
return path.Join(protoDir, gqlFileName)
}

return strings.TrimSuffix(protoFileName, path.Ext(protoFileName)) + ".graphqls"
}
22 changes: 22 additions & 0 deletions example/codegen/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.PHONY: generate start

start: generate
go run ./main.go

generate: gql/constructs/generated.go gql/options/generated.go

pb/%.gqlgen.pb.go: pb/%.proto pb/%_grpc.pb.go pb/%.pb.go
protoc --gogql_out=paths=source_relative:. -I . -I ../../ ./pb/$*.proto

pb/%_grpc.pb.go: pb/%.proto pb/%.pb.go
protoc --go-grpc_out=paths=source_relative:. -I . -I ../../ ./pb/$*.proto

pb/%.pb.go: pb/%.proto
protoc --go_out=paths=source_relative:. -I . -I ../../ ./pb/$*.proto

pb/%.graphqls: pb/%.proto
protoc --gql_out=svc=true:. -I . -I ../../ ./pb/$*.proto

gql/%/generated.go: pb/%.graphqls pb/%.gqlgen.pb.go
gqlgen --config ./gqlgen-$*.yaml

Loading

0 comments on commit 0645feb

Please sign in to comment.