truss used to be composed of several protoc plugins, with lots of side
effects.

truss is now composed of one protoc plugin, protoc-gen-truss-protocout
which outputs the output of protoc.

This output, plus the .proto file with the service definition are used
to create a doctree

This doctree is then used by gendocs and gengokit to generate the
microservice and documentation.

This stucture has less side effects, and should be more testable
This commit is contained in:
Adam Ryman 2016-08-10 20:53:05 -07:00
Родитель e8e3e8ec8c
Коммит 78d2ce7eaa
34 изменённых файлов: 1020 добавлений и 679 удалений

10
gendoc/README.md Normal file
Просмотреть файл

@ -0,0 +1,10 @@
# `gendocs`
A `truss` plugin which can generate documentation from an annotated Protobuf definition file. Handles http-options.
## Limitations and Bugs
Currently, there are a variety of limitations in the documentation parser.
- Having additional http bindings via the `additional_bindings` directive when declaring http options causes the parser to break.

Просмотреть файл

@ -1,9 +1,12 @@
package httpopts
import (
"github.com/TuneLab/gob/gendoc/doctree"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/TuneLab/gob/gendoc/doctree"
)
// Assemble takes a doctree that's already had http options parsed by svcparse
@ -11,7 +14,7 @@ import (
// ServiceMethod's http annotations. After this, each `HttpBinding` will have a
// populated list of all the http parameters that that binding requires, where
// that parameter should be located, and the type of each parameter.
func Assemble(dt doctree.Doctree) {
func Assemble(dt doctree.Doctree) error {
md := dt.(*doctree.MicroserviceDefinition)
for _, file := range md.Files {
for _, svc := range file.Services {
@ -19,12 +22,14 @@ func Assemble(dt doctree.Doctree) {
for _, pbind := range meth.HttpBindings {
err := contextualizeBinding(meth, pbind)
if err != nil {
panic(err)
return errors.Wrap(err, "contextualizing http bindings failed")
}
}
}
}
}
return nil
}
func contextualizeBinding(meth *doctree.ServiceMethod, binding *doctree.MethodHttpBinding) error {

Просмотреть файл

@ -7,6 +7,7 @@ package makedt
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
@ -71,28 +72,36 @@ func findMessage(md *doctree.MicroserviceDefinition, newFile *doctree.ProtoFile,
}
}
}
return nil, fmt.Errorf("Couldn't find message.")
return nil, fmt.Errorf("couldn't find message.")
}
// New accepts a Protobuf CodeGeneratorRequest and returns a Doctree struct
func New(req *plugin.CodeGeneratorRequest) (doctree.Doctree, error) {
// New accepts a Protobuf plugin.CodeGeneratorRequest and the contents of the
// file containing the service declaration and returns a Doctree struct
func New(req *plugin.CodeGeneratorRequest, serviceFile io.Reader) (doctree.Doctree, error) {
dt := doctree.MicroserviceDefinition{}
dt.SetName(findDoctreePackage(req))
var svc *doctree.ProtoService
var serviceFileName string
for _, file := range req.ProtoFile {
// Check if this file is one we even should examine, and if it's not,
// skip it
if file.GetPackage() != findDoctreePackage(req) {
continue
}
// This is a file we are meant to examine, so contine with its creation
// in the Doctree
newFile, err := NewFile(file, &dt)
if err != nil {
return nil, errors.Wrapf(err, "file creation of %q failed", file.GetName())
}
if len(newFile.Services) > 0 {
svc = newFile.Services[0]
serviceFileName = newFile.GetName()
}
dt.Files = append(dt.Files, newFile)
}
@ -103,7 +112,11 @@ func New(req *plugin.CodeGeneratorRequest) (doctree.Doctree, error) {
// The implementation of this function is in doctree/associate_comments.go
doctree.AssociateComments(&dt, req)
addHttpOptions(&dt, req)
err := addHttpOptions(&dt, svc, serviceFile)
if err != nil {
log.WithError(err).Warnf("Error found while parsing file %v", serviceFileName)
log.Warnf("Due to the above warning(s), http options and bindings where not parsed and will not be present in the generated documentation.")
}
return &dt, nil
}
@ -146,7 +159,8 @@ func NewFile(
return &newFile, nil
}
// NewEnum returns a *doctree.ProtoEnum created from a *descriptor.EnumDescriptorProto
// NewEnum returns a *doctree.ProtoEnum created from a
// *descriptor.EnumDescriptorProto
func NewEnum(enum *descriptor.EnumDescriptorProto) (*doctree.ProtoEnum, error) {
newEnum := doctree.ProtoEnum{}
@ -162,7 +176,8 @@ func NewEnum(enum *descriptor.EnumDescriptorProto) (*doctree.ProtoEnum, error) {
return &newEnum, nil
}
// NewMessage returns a *doctree.ProtoMessage created from a *descriptor.DescriptorProto
// NewMessage returns a *doctree.ProtoMessage created from a
// *descriptor.DescriptorProto
func NewMessage(msg *descriptor.DescriptorProto) (*doctree.ProtoMessage, error) {
newMsg := doctree.ProtoMessage{}
newMsg.Name = *msg.Name
@ -206,11 +221,11 @@ func NewService(
// Message types
reqMsg, err := findMessage(curNewDt, curNewFile, *meth.InputType)
if reqMsg == nil || err != nil {
return nil, fmt.Errorf("Couldn't find request message of type '%v' for method '%v'", *meth.InputType, *meth.Name)
return nil, fmt.Errorf("couldn't find request message of type '%v' for method '%v'", *meth.InputType, *meth.Name)
}
respMsg, err := findMessage(curNewDt, curNewFile, *meth.OutputType)
if respMsg == nil || err != nil {
return nil, fmt.Errorf("Couldn't find response message of type '%v' for method '%v'", *meth.InputType, *meth.Name)
return nil, fmt.Errorf("couldn't find response message of type '%v' for method '%v'", *meth.InputType, *meth.Name)
}
newMeth.RequestType = reqMsg
newMeth.ResponseType = respMsg
@ -251,35 +266,27 @@ func searchFileName(fname string) string {
// Parse the protobuf files for comments surrounding http options, then add
// those to the Doctree in place.
func addHttpOptions(dt doctree.Doctree, req *plugin.CodeGeneratorRequest) {
func addHttpOptions(dt doctree.Doctree, svc *doctree.ProtoService, protoFile io.Reader) error {
fname := FindServiceFile(req)
fullPath := searchFileName(fname)
f, err := os.Open(fullPath)
if err != nil {
cwd, _ := os.Getwd()
log.Warnf("From current directory '%v', error opening file '%v', '%v'\n", cwd, fullPath, err)
log.Warnf("Due to the above warning(s), http options and bindings where not parsed and will not be present in the generated documentation.")
return
}
lex := svcparse.NewSvcLexer(f)
lex := svcparse.NewSvcLexer(protoFile)
parsedSvc, err := svcparse.ParseService(lex)
if err != nil {
log.Warnf("Error found while parsing file '%v': %v", fullPath, err)
log.Warnf("Due to the above warning(s), http options and bindings where not parsed and will not be present in the generated documentation.")
return
return errors.Wrapf(err, "error while parsing http options for the %v service definition", svc.GetName())
}
svc := dt.GetByName(fname).GetByName(parsedSvc.GetName()).(*doctree.ProtoService)
for _, pmeth := range parsedSvc.Methods {
meth := svc.GetByName(pmeth.GetName()).(*doctree.ServiceMethod)
meth.HttpBindings = pmeth.HttpBindings
}
// Assemble the http parameters for each http binding
httpopts.Assemble(dt)
err = httpopts.Assemble(dt)
if err != nil {
return errors.Wrap(err, "could not assemble http parameters for each http binding")
}
return nil
}
// Searches through the files in the request and returns the path to the first

26
gendoc/gendoc.go Normal file
Просмотреть файл

@ -0,0 +1,26 @@
// Package gendoc is a truss plugin
// to generate markdown documentation for a protobuf definition file.
package gendoc
import (
"github.com/TuneLab/gob/gendoc/doctree"
"github.com/TuneLab/gob/truss/truss"
)
// GenerateDocs accepts a doctree that represents an ast of a group of
// protofiles and returns a []truss.SimpleFile that represents a relative
// filestructure of generated docs
func GenerateDocs(dt doctree.Doctree) []truss.SimpleFile {
response := dt.Markdown()
name := "service/docs/docs.md"
file := truss.SimpleFile{
Name: &name,
Content: &response,
}
var files []truss.SimpleFile
files = append(files, file)
return files
}

Просмотреть файл

@ -1,6 +1,6 @@
# protoc-gen-truss-gokit
# gengokit
protoc-gen-truss-gokit is a `protoc` plugin that from a grpc service definition:
gengokit is a `truss` plugin that from a `doctree` and `[]truss.SimpleFile`
1. Generates Golang code for a gokit microservice that includes:
- Logging
@ -32,7 +32,7 @@ protoc-gen-truss-gokit is a `protoc` plugin that from a grpc service definition:
`./astmodifier` provides functions to modify source code already generated and/or user modified.
`./generator` executes the template files using `doctree`'s representation of the `protoc` AST. `./generator` also uses `./astmodifier` to rewrite code to insert/remove handler methods/rpcs that are defined/removed from a definition file without touching user written logic.
`gengokit.go` executes the template files using `doctree`'s representation of the `protoc` AST. `./generator` also uses `./astmodifier` to rewrite code to insert/remove handler methods/rpcs that are defined/removed from a definition file without touching user written logic.
# NOTE:

Просмотреть файл

@ -22,7 +22,7 @@ func init() {
type astModifier struct {
fset *token.FileSet
fileAst *ast.File
fileAst ast.Node
funcIndexer *functionIndexer
funcRemover *functionRemover
interfaceRemover *interfaceRemover
@ -41,7 +41,7 @@ type interfaceRemover struct {
}
// New returns a new astModifier from a source file which modifies code intelligently
func New(sourcePath string) *astModifier {
func NewFromFile(sourcePath string) *astModifier {
fset := token.NewFileSet()
fileAst, err := parser.ParseFile(fset, sourcePath, nil, 0)
if err != nil {
@ -61,7 +61,29 @@ func New(sourcePath string) *astModifier {
interfaceToRemove: "",
},
}
}
// New returns a new astModifier from a source file which modifies code intelligently
func New(source *string) *astModifier {
fset := token.NewFileSet()
fileAst, err := parser.ParseFile(fset, "", *source, 0)
if err != nil {
log.WithError(err).Fatal("server/service.go could not be parsed by go/parser into AST")
}
return &astModifier{
fset: fset,
fileAst: fileAst,
funcIndexer: &functionIndexer{
functionIndex: make(map[string]bool),
},
funcRemover: &functionRemover{
functionsToKeep: make(map[string]bool),
},
interfaceRemover: &interfaceRemover{
interfaceToRemove: "",
},
}
}
// String returns the current ast as a string

340
gengokit/gengokit.go Normal file
Просмотреть файл

@ -0,0 +1,340 @@
package gengokit
import (
"bytes"
"go/format"
"os"
"path/filepath"
"strings"
"text/template"
log "github.com/Sirupsen/logrus"
generatego "github.com/golang/protobuf/protoc-gen-go/generator"
"github.com/pkg/errors"
"github.com/TuneLab/gob/gengokit/astmodifier"
"github.com/TuneLab/gob/gengokit/clientarggen"
templateFileAssets "github.com/TuneLab/gob/gengokit/template"
"github.com/TuneLab/gob/gendoc/doctree"
"github.com/TuneLab/gob/truss/truss"
)
func init() {
log.SetLevel(log.InfoLevel)
log.SetOutput(os.Stderr)
log.SetFormatter(&log.TextFormatter{
ForceColors: true,
})
}
type generator struct {
previousFiles []truss.SimpleFile
templateFileNames func() []string
templateFile func(string) ([]byte, error)
templateFuncMap template.FuncMap
templateExec templateExecutor
}
// templateExecutor is passed to templates as the executing struct its fields
// and methods are used to modify the template
type templateExecutor struct {
// Import path for handler package
HandlerImport string
// Import path for generated packages
GeneratedImport string
// GRPC/Protobuff service, with all parameters and return values accessible
Service *doctree.ProtoService
ClientArgs *clientarggen.ClientServiceArgs
}
// GenerateGokit accepts a doctree representing the ast of a group of .proto
// files, a []truss.SimpleFile representing files generated previously, and
// a goImportPath for templating go code imports
// GenerateGoCode returns the a []truss.SimpleFile representing a generated
// gokit microservice file structure
func GenerateGokit(dt doctree.Doctree, previousFiles []truss.SimpleFile, goImportPath string) ([]truss.SimpleFile, error) {
service, err := getProtoService(dt)
if err != nil {
return nil, errors.Wrap(err, "no service found aborting generating gokit microservice")
}
g := newGenerator(service, previousFiles, goImportPath)
files, err := g.GenerateResponseFiles()
if err != nil {
return nil, errors.Wrap(err, "could not generate gokit microservice")
}
return files, nil
}
// getProtoService finds returns the service within a doctree.Doctree
func getProtoService(dt doctree.Doctree) (*doctree.ProtoService, error) {
md := dt.(*doctree.MicroserviceDefinition)
files := md.Files
var service *doctree.ProtoService
for _, file := range files {
if len(file.Services) > 0 {
service = file.Services[0]
}
}
if service == nil {
return nil, errors.New("no service found")
}
return service, nil
}
// New returns a new generator which generates a gokit microservice
func newGenerator(service *doctree.ProtoService, previousFiles []truss.SimpleFile, goImportPath string) *generator {
// import path for server and client handlers
handlerImportString := goImportPath + "/service"
// import path for generated code that user should not edit
generatedImportString := handlerImportString + "/DONOTEDIT"
funcMap := template.FuncMap{
"ToLower": strings.ToLower,
"Title": strings.Title,
"GoName": generatego.CamelCase,
"TrimPrefix": strings.TrimPrefix,
}
return &generator{
previousFiles: previousFiles,
templateFileNames: templateFileAssets.AssetNames,
templateFile: templateFileAssets.Asset,
templateFuncMap: funcMap,
templateExec: templateExecutor{
HandlerImport: handlerImportString,
GeneratedImport: generatedImportString,
Service: service,
ClientArgs: clientarggen.New(service),
},
}
}
// getFileContentByName searches though a []truss.SimpleFile and returns the
// contents of the file with the name n
func getFileContentByName(n string, files []truss.SimpleFile) *string {
for _, f := range files {
if *(f.Name) == n {
return f.Content
}
}
return nil
}
// updateServiceMethods will update the functions within an existing service.go
// file so it contains all the functions within svcFuncs and ONLY those
// functions within svcFuncs
func (g *generator) updateServiceMethods(svcHandler *string, svcFuncs []string) (outCode *bytes.Buffer, err error) {
const svcMethodsTemplPath = "service/partial_template/service.methods"
const svcInterfaceTemplPath = "service/partial_template/service.interface"
astMod := astmodifier.New(svcHandler)
//TODO: Discuss if functions should be removed from the service file, when using truss I did not like that it removed function I wrote myself
//astMod.RemoveFunctionsExecpt(svcFuncs)
astMod.RemoveInterface("Service")
log.WithField("Code", astMod.String()).Debug("Server service handlers before template")
// Index the handler functions, apply handler template for all function in service definition that are not defined in handler
currentFuncs := astMod.IndexFunctions()
code := astMod.Buffer()
err = g.applyTemplateForMissingMeths(svcMethodsTemplPath, currentFuncs, code)
if err != nil {
return nil, errors.Wrap(err, "unable to apply service methods template")
}
// Insert updated Service interface
outBuf, err := g.applyTemplate(svcInterfaceTemplPath, g.templateExec)
if err != nil {
return nil, errors.Wrap(err, "unable to apply service interface template")
}
code.Write(outBuf.Bytes())
return code, nil
}
// updateClientMethods will update the functions within an existing
// client_handler.go file so that it contains exactly the fucntions passed in
// svcFuncs, no more, no less.
func (g *generator) updateClientMethods(clientHandler *string, svcFuncs []string) (outCode *bytes.Buffer, err error) {
const clientMethodsTemplPath = "service/partial_template/client_handler.methods"
astMod := astmodifier.New(clientHandler)
// Remove functions no longer in definition
astMod.RemoveFunctionsExecpt(svcFuncs)
log.WithField("Code", astMod.String()).Debug("Client handlers before template")
// Index handler functions, apply handler template for all function in
// service definition that are not defined in handler
currentFuncs := astMod.IndexFunctions()
code := astMod.Buffer()
err = g.applyTemplateForMissingMeths(clientMethodsTemplPath, currentFuncs, code)
if err != nil {
return nil, errors.Wrap(err, "unable to apply client methods template")
}
return code, nil
}
// GenerateResponseFiles applies all template files for the generated
// microservice and returns a slice containing each templated file as a
// CodeGeneratorResponse_File.
func (g *generator) GenerateResponseFiles() ([]truss.SimpleFile, error) {
const serviceHandlerFilePath = "service/server/service.go"
const clientHandlerFilePath = "service/client/client_handler.go"
var codeGenFiles []truss.SimpleFile
// serviceFunctions is used later as the master list of all service methods
// which should exist within the `server/service.go` and
// `client/client_handler.go` files.
var serviceFunctions []string
for _, meth := range g.templateExec.Service.Methods {
serviceFunctions = append(serviceFunctions, meth.GetName())
}
serviceFunctions = append(serviceFunctions, "NewBasicService")
serviceHandlerFile := getFileContentByName(serviceHandlerFilePath, g.previousFiles)
clientHandlerFile := getFileContentByName(clientHandlerFilePath, g.previousFiles)
for _, templateFilePath := range g.templateFileNames() {
if filepath.Ext(templateFilePath) != ".gotemplate" {
log.WithField("Template file", templateFilePath).Debug("Skipping rendering non-buildable partial template")
continue
}
var generatedFilePath string
var generatedCode *bytes.Buffer
var err error
if templateFilePath == serviceHandlerFilePath+"template" && serviceHandlerFile != nil {
// If there's an existing service file, update its contents
generatedFilePath = serviceHandlerFilePath
generatedCode, err = g.updateServiceMethods(serviceHandlerFile, serviceFunctions)
if err != nil {
return nil, errors.Wrap(err, "could not modifiy service handler file")
}
} else if templateFilePath == clientHandlerFilePath+"template" && clientHandlerFile != nil {
// If there's an existing client_handler file, update its contents
generatedFilePath = clientHandlerFilePath
generatedCode, err = g.updateClientMethods(clientHandlerFile, serviceFunctions)
if err != nil {
return nil, errors.Wrap(err, "could not modifiy client handler file")
}
} else {
generatedFilePath = templateFilePath
// Change file path from .gotemplate to .go
generatedFilePath = strings.TrimSuffix(generatedFilePath, "template")
generatedCode, err = g.applyTemplate(templateFilePath, g.templateExec)
if err != nil {
return nil, errors.Wrap(err, "could not render template")
}
}
// Turn code buffer into string and format it
code := generatedCode.String()
formattedCode := formatCode(code)
resp := truss.SimpleFile{
Name: &generatedFilePath,
Content: &formattedCode,
}
codeGenFiles = append(codeGenFiles, resp)
}
return codeGenFiles, nil
}
// applyTemplateForMissingMeths accepts a funcIndex which represents functions
// already present in a gofile, applyTemplateForMissingMeths compares this map
// to the functions defined in the service and renders the template with the
// path of templPath, and appends this to passed code
func (g *generator) applyTemplateForMissingMeths(templPath string, funcIndex map[string]bool, code *bytes.Buffer) error {
var methodsToTemplate []*doctree.ServiceMethod
for _, meth := range g.templateExec.Service.Methods {
methName := meth.GetName()
if funcIndex[methName] == false {
methodsToTemplate = append(methodsToTemplate, meth)
log.WithField("Method", methName).Info("Rendering template for method")
} else {
log.WithField("Method", methName).Info("Handler method already exists")
}
}
// Create temporary templateExec with only the methods we want to append
// We must also dereference the templateExec's Service and change our newly created
// Service's pointer to it's messages to be methodsToTemplate
templateExecWithOnlyMissingMethods := g.templateExec
tempService := *g.templateExec.Service
tempService.Methods = methodsToTemplate
templateExecWithOnlyMissingMethods.Service = &tempService
// Apply the template and write it to code
templateOut, err := g.applyTemplate(templPath, templateExecWithOnlyMissingMethods)
if err != nil {
return errors.Wrapf(err, "could not apply template for missing methods: %v", templPath)
}
_, err = code.Write(templateOut.Bytes())
if err != nil {
return errors.Wrap(err, "could not append rendered template to code")
}
return nil
}
// applyTemplate accepts a path to a template and an interface to execute on that template
// returns a *bytes.Buffer containing the results of that execution
func (g *generator) applyTemplate(templateFilePath string, executor interface{}) (*bytes.Buffer, error) {
templateBytes, err := g.templateFile(templateFilePath)
if err != nil {
return nil, errors.Wrapf(err, "unable to find template file: %v", templateFilePath)
}
templateString := string(templateBytes)
codeTemplate := template.Must(template.New(templateFilePath).Funcs(g.templateFuncMap).Parse(templateString))
outputBuffer := bytes.NewBuffer(nil)
err = codeTemplate.Execute(outputBuffer, executor)
if err != nil {
return nil, errors.Wrap(err, "template error")
}
return outputBuffer, nil
}
// formatCode takes a string representing golang code and attempts to return a
// formated copy of that code. If formatting fails, a warning is logged and
// the original code is returned.
func formatCode(code string) string {
formatted, err := format.Source([]byte(code))
if err != nil {
log.WithError(err).Warn("Code formatting error, generated service will not build, outputting unformatted code")
// Set formatted to code so at least we get something to examine
formatted = []byte(code)
}
return string(formatted)
}

Просмотреть файл

@ -1,19 +0,0 @@
# `protoc-gen-truss-docs`
A `protoc` plugin which can generate documentation from an annotated Protobuf definition file. Handles http-options.
To run, ensure the program is installed by running `go install github.com/TuneLab/gob/protoc-gen-truss-doc/...`. Once installed, you can use this plugin by compiling a proto file with `protoc` and the the following options:
protoc -I/usr/local/include -I. -I.. \
-I$GOPATH/src/github.com/TuneLab/gob/third_party/googleapis/ \
--truss-doc_out=. {NAME_OF_PROTO_FILE}
This will output a file in the current directory named "docs.md" containing a markdown representation of your documentation.
## Limitations and Bugs
Currently, there are a variety of limitations in the documentation parser.
- Having additional http bindings via the `additional_bindings` directive when declaring http options causes the parser to break.

Просмотреть файл

@ -1,65 +0,0 @@
// Command protoc-gen-truss-doc is a plugin for Google protocol buffer compiler
// to generate markdown documentation for a protobuf definition file.
package main
import (
"flag"
"io"
"io/ioutil"
"os"
"github.com/TuneLab/gob/gendoc/doctree/makedt"
"github.com/golang/protobuf/proto"
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
)
var (
response = string("")
)
// Attempt to parse the incoming CodeGeneratorRequest being written by `protoc`
// to our stdin
func parseReq(r io.Reader) (*plugin.CodeGeneratorRequest, error) {
input, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
req := new(plugin.CodeGeneratorRequest)
if err = proto.Unmarshal(input, req); err != nil {
return nil, err
}
return req, nil
}
func main() {
flag.Parse()
request, err := parseReq(os.Stdin)
if err != nil {
panic(err)
}
// Parse the proto files we've been given, then create the markdown
// documentation for those proto files. All the documentation is written to
// a file named 'docs.md'.
doc, _ := makedt.New(request)
response := doc.Markdown()
out_fname := "service/docs/docs.md"
response_file := str_to_response(response, out_fname)
output_struct := &plugin.CodeGeneratorResponse{File: []*plugin.CodeGeneratorResponse_File{response_file}}
buf, err := proto.Marshal(output_struct)
if _, err := os.Stdout.Write(buf); err != nil {
panic(err)
}
}
func str_to_response(instr string, fname string) *plugin.CodeGeneratorResponse_File {
return &plugin.CodeGeneratorResponse_File{
Name: &fname,
Content: &instr,
}
}

Просмотреть файл

@ -1,291 +0,0 @@
package generator
import (
"bytes"
"go/format"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/TuneLab/gob/protoc-gen-truss-gokit/astmodifier"
templateFileAssets "github.com/TuneLab/gob/protoc-gen-truss-gokit/template"
"github.com/TuneLab/gob/gendoc/doctree"
"github.com/TuneLab/gob/protoc-gen-truss-gokit/generator/clientarggen"
generatego "github.com/golang/protobuf/protoc-gen-go/generator"
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
log "github.com/Sirupsen/logrus"
)
func init() {
log.SetLevel(log.InfoLevel)
log.SetOutput(os.Stderr)
log.SetFormatter(&log.TextFormatter{
ForceColors: true,
})
}
type generator struct {
files []*doctree.ProtoFile
outputDirName string
templateFileNames func() []string
templateFile func(string) ([]byte, error)
templateFuncMap template.FuncMap
templateExec templateExecutor
}
// templateExecutor is passed to templates as the executing struct
// Its fields and methods are used to modify the template
type templateExecutor struct {
// Import path for handler package
HandlerImport string
// Import path for generated packages
GeneratedImport string
// GRPC/Protobuff service, with all parameters and return values accessible
Service *doctree.ProtoService
ClientArgs *clientarggen.ClientServiceArgs
}
// New returns a new generator which generates grpc gateway files.
func New(files []*doctree.ProtoFile, outputDirName string) *generator {
var service *doctree.ProtoService
log.WithField("File Count", len(files)).Info("Files are being processed")
// Find the service to be attached to the templateExecutor
for _, file := range files {
log.WithFields(log.Fields{
"File": file.GetName(),
"Service Count": len(file.Services),
}).Info("File being processed")
if len(file.Services) > 0 {
service = file.Services[0]
log.WithField("Service", service.GetName()).Info("Service Discoved")
}
}
if service == nil {
log.Fatal("No service discovered, aborting...")
}
// Get the working directory and the go import paths for the working directory
// e.g.
// Working directory: (wd)
// /home/adamryman/projects/go/src/github.com/TuneLab/gob/protoc-gen-truss-gokit/
// $GOPATH: (goPath)
// /home/adamryman/projects/go/
// So strings.TrimPrefix(wd, goPath+"/src/") = github.com/TuneLab/gob/protoc-gen-truss-gokit
wd, _ := os.Getwd()
goPath := os.Getenv("GOPATH")
wdImportString := strings.TrimPrefix(wd, goPath+"/src/")
// Then add onto the wdImportString the outputDirName to get our baseImportString
baseImportString := wdImportString + "/" + outputDirName
log.WithField("Output dir", outputDirName).Info("Output directory")
log.WithField("serviceImportPath", baseImportString).Info("Service path")
// import path for generated code with handlers that the user can edit
handlerImportString := baseImportString
// import path for generated code that user should not edit
generatedImportString := baseImportString + "/DONOTEDIT"
funcMap := template.FuncMap{
"ToLower": strings.ToLower,
"Title": strings.Title,
"GoName": generatego.CamelCase,
"TrimPrefix": strings.TrimPrefix,
}
return &generator{
files: files,
outputDirName: outputDirName,
templateFileNames: templateFileAssets.AssetNames,
templateFile: templateFileAssets.Asset,
templateFuncMap: funcMap,
templateExec: templateExecutor{
HandlerImport: handlerImportString,
GeneratedImport: generatedImportString,
Service: service,
ClientArgs: clientarggen.New(service),
},
}
}
// fileExists checks if a file at the given path exists. Returns true if the
// file exists, and false if the file does not exist.
func fileExists(path string) bool {
if _, err := os.Stat(path); err == nil {
return true
}
return false
}
// updateServiceMethods will update the functions within an existing service.go
// file so it contains all the functions within svcFuncs and ONLY those
// functions within svcFuncs
func (g *generator) updateServiceMethods(svcPath string, svcFuncs []string) (outPath string, outCode string) {
log.Info("server/service.go exists")
astMod := astmodifier.New(svcPath)
// Remove functions no longer in definition and remove Service interface
astMod.RemoveFunctionsExecpt(svcFuncs)
astMod.RemoveInterface("Service")
log.WithField("Code", astMod.String()).Debug("Server service handlers before template")
// Index the handler functions, apply handler template for all function in service definition that are not defined in handler
currentFuncs := astMod.IndexFunctions()
code := astMod.Buffer()
code = g.applyTemplateForMissingServiceMethods("service/partial_template/service.methods", currentFuncs, code)
// Insert updated Service interface
outBuf := g.applyTemplate("service/partial_template/service.interface", g.templateExec)
code.WriteString(outBuf)
// Get file ready to write
outPath = "server/service.go"
outCode = formatCode(code.String())
return outPath, outCode
}
// updateClientMethods will update the functions within an existing
// client_handler.go file so that it contains exactly the fucntions passed in
// svcFuncs, no more, no less.
func (g *generator) updateClientMethods(clientPath string, svcFuncs []string) (outPath string, outCode string) {
log.Info("client/client_handler.go exists")
astMod := astmodifier.New(clientPath)
// Remove functions no longer in definition
astMod.RemoveFunctionsExecpt(svcFuncs)
log.WithField("Code", astMod.String()).Debug("Client handlers before template")
// Index handler functions, apply handler template for all function in
// service definition that are not defined in handler
currentFuncs := astMod.IndexFunctions()
code := astMod.Buffer()
code = g.applyTemplateForMissingServiceMethods("service/partial_template/client_handler.methods", currentFuncs, code)
// Get file ready to write
outPath = "client/client_handler.go"
outCode = formatCode(code.String())
return outPath, outCode
}
// GenerateResponseFiles applies all template files for the generated
// microservice and returns a slice containing each templated file as a
// CodeGeneratorResponse_File.
func (g *generator) GenerateResponseFiles() ([]*plugin.CodeGeneratorResponse_File, error) {
var codeGenFiles []*plugin.CodeGeneratorResponse_File
wd, _ := os.Getwd()
clientPath := wd + "/" + g.outputDirName + "/client/client_handler.go"
servicePath := wd + "/" + g.outputDirName + "/server/service.go"
// serviceFunctions is used later as the master list of all service methods
// which should exist within the `server/service.go` and
// `client/client_handler.go` files.
var serviceFunctions []string
for _, meth := range g.templateExec.Service.Methods {
serviceFunctions = append(serviceFunctions, meth.GetName())
}
serviceFunctions = append(serviceFunctions, "NewBasicService")
for _, templateFilePath := range g.templateFileNames() {
if filepath.Ext(templateFilePath) != ".gotemplate" {
log.WithField("Template file", templateFilePath).Debug("Skipping rendering non-buildable partial template")
continue
}
var generatedFilePath string
var generatedCode string
if filepath.Base(templateFilePath) == "service.gotemplate" && fileExists(servicePath) {
// If there's an existing service file, update its contents
generatedFilePath, generatedCode = g.updateServiceMethods(servicePath, serviceFunctions)
} else if filepath.Base(templateFilePath) == "client_handler.gotemplate" && fileExists(clientPath) {
// If there's an existing client_handler file, update its contents
generatedFilePath, generatedCode = g.updateClientMethods(clientPath, serviceFunctions)
} else {
// Remove "template_files/" so that generated files do not include that directory
generatedFilePath = strings.TrimPrefix(templateFilePath, "service/")
// Change file path from .gotemplate to .go
generatedFilePath = strings.TrimSuffix(generatedFilePath, "template")
generatedCode = g.applyTemplate(templateFilePath, g.templateExec)
generatedCode = formatCode(generatedCode)
}
generatedFilePath = g.outputDirName + "/" + generatedFilePath
resp := plugin.CodeGeneratorResponse_File{
Name: &generatedFilePath,
Content: &generatedCode,
}
codeGenFiles = append(codeGenFiles, &resp)
}
return codeGenFiles, nil
}
func (g *generator) applyTemplateForMissingServiceMethods(templateFilePath string, functionIndex map[string]bool, code *bytes.Buffer) *bytes.Buffer {
var methodsToTemplate []*doctree.ServiceMethod
for _, meth := range g.templateExec.Service.Methods {
methName := meth.GetName()
if functionIndex[methName] == false {
methodsToTemplate = append(methodsToTemplate, meth)
log.WithField("Method", methName).Info("Rendering template for method")
} else {
log.WithField("Method", methName).Info("Handler method already exists")
}
}
// Create temporary templateExec with only the methods we want to append
// We must also dereference the templateExec's Service and change our newly created
// Service's pointer to it's messages to be methodsToTemplate
templateExecWithOnlyMissingMethods := g.templateExec
tempService := *g.templateExec.Service
tempService.Methods = methodsToTemplate
templateExecWithOnlyMissingMethods.Service = &tempService
// Apply the template and write it to code
templateOut := g.applyTemplate(templateFilePath, templateExecWithOnlyMissingMethods)
code.WriteString(templateOut)
return code
}
func (g *generator) applyTemplate(templateFilePath string, executor interface{}) string {
templateBytes, _ := g.templateFile(templateFilePath)
templateString := string(templateBytes)
codeTemplate := template.Must(template.New(templateFilePath).Funcs(g.templateFuncMap).Parse(templateString))
outputBuffer := bytes.NewBuffer(nil)
err := codeTemplate.Execute(outputBuffer, executor)
if err != nil {
log.WithError(err).Fatal("Template Error")
}
return outputBuffer.String()
}
// formatCode takes a string representing golang code and attempts to return a
// formated copy of that code. If formatting fails, a warning is logged and
// the original code is returned.
func formatCode(code string) string {
formatted, err := format.Source([]byte(code))
if err != nil {
log.WithError(err).Warn("Code formatting error, generated service will not build, outputting unformatted code")
// Set formatted to code so at least we get something to examine
formatted = []byte(code)
}
return string(formatted)
}

Просмотреть файл

@ -1,51 +0,0 @@
package main
import (
"io"
"io/ioutil"
"os"
"github.com/golang/protobuf/proto"
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
"github.com/TuneLab/gob/gendoc/doctree"
"github.com/TuneLab/gob/gendoc/doctree/makedt"
generator "github.com/TuneLab/gob/protoc-gen-truss-gokit/generator"
)
// parseReq reads io.Reader r into memory and attempts to marshal
// that input into a protobuf plugin CodeGeneratorRequest
func parseReq(r io.Reader) (*plugin.CodeGeneratorRequest, error) {
input, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
req := new(plugin.CodeGeneratorRequest)
if err = proto.Unmarshal(input, req); err != nil {
return nil, err
}
return req, nil
}
func main() {
request, err := parseReq(os.Stdin)
prototree, _ := makedt.New(request)
prototreeDefinition := prototree.(*doctree.MicroserviceDefinition)
g := generator.New(prototreeDefinition.Files, "service")
codeGenFiles, _ := g.GenerateResponseFiles()
output := &plugin.CodeGeneratorResponse{
File: codeGenFiles,
}
buf, err := proto.Marshal(output)
_ = err
if _, err := os.Stdout.Write(buf); err != nil {
os.Exit(1)
}
}

Просмотреть файл

@ -0,0 +1,48 @@
package main
import (
"fmt"
"io/ioutil"
"os"
//"path"
"github.com/golang/protobuf/proto"
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
)
func main() {
input, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
req := new(plugin.CodeGeneratorRequest)
if err = proto.Unmarshal(input, req); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
filesOut := req.GetFileToGenerate()
//fmt.Fprintln(os.Stderr, "test")
fileName := filesOut[0]
protocOut := string(input)
codeGenFile := plugin.CodeGeneratorResponse_File{
Name: &fileName,
Content: &protocOut,
}
output := &plugin.CodeGeneratorResponse{
File: []*plugin.CodeGeneratorResponse_File{
&codeGenFile,
},
}
buf, err := proto.Marshal(output)
if _, err := os.Stdout.Write(buf); err != nil {
os.Exit(1)
}
}

Просмотреть файл

@ -1,191 +0,0 @@
package generator
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
log "github.com/Sirupsen/logrus"
templates "github.com/TuneLab/gob/truss/template"
)
func init() {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.TextFormatter{
ForceColors: true,
})
}
// GenerateMicroservice takes a golang importPath, a path to .proto definition files workingDirectory, and a slice of
// definition files DefinitionFiles and outputs a ./service direcotry in workingDirectory with a generated and built golang microservice
func GenerateMicroservice(importPath string, workingDirectory string, definitionFiles []string) {
done := make(chan bool)
// Stage 1
buildDirectories(workingDirectory)
outputGoogleImport(workingDirectory)
// Stage 2, 3, 4
generatePbGoCmd := "--go_out=Mgoogle/api/annotations.proto=" + importPath + "/service/DONOTEDIT/third_party/googleapis/google/api,plugins=grpc:./service/DONOTEDIT/pb"
const generateDocsCmd = "--truss-doc_out=."
const generateGoKitCmd = "--truss-gokit_out=."
go protoc(workingDirectory, definitionFiles, generatePbGoCmd, done)
go protoc(workingDirectory, definitionFiles, generateDocsCmd, done)
go protoc(workingDirectory, definitionFiles, generateGoKitCmd, done)
<-done
<-done
<-done
// Stage 5
go goBuild("server", importPath+"/service/DONOTEDIT/cmd/svc/...", done)
go goBuild("cliclient", importPath+"/service/DONOTEDIT/cmd/cliclient/...", done)
<-done
<-done
}
// buildDirectories puts the following directories in place
// .
// └── service
// ├── bin
// └── DONOTEDIT
// ├── pb
// └── third_party
// └── googleapis
// └── google
// └── api
func buildDirectories(workingDirectory string) {
// third_party created by going through assets in template
// and creating directoires that are not there
for _, filePath := range templates.AssetNames() {
fullPath := workingDirectory + "/" + filePath
dirPath := filepath.Dir(fullPath)
err := os.MkdirAll(dirPath, 0777)
if err != nil {
log.WithField("DirPath", dirPath).WithError(err).Fatal("Cannot create directories")
}
}
// Create the directory where protoc will store the compiled .pb.go files
err := os.MkdirAll(workingDirectory+"/service/DONOTEDIT/pb", 0777)
if err != nil {
log.WithField("DirPath", "service/DONOTEDIT/pb").WithError(err).Fatal("Cannot create directories")
}
// Create the directory where go build will put the compiled binaries
err = os.MkdirAll(workingDirectory+"/service/bin", 0777)
if err != nil {
log.WithField("DirPath", "service/bin").WithError(err).Fatal("Cannot create directories")
}
}
// outputGoogleImport places imported and required google.api.http protobuf option files
// into their required directories as part of stage one generation
func outputGoogleImport(workingDirectory string) {
// Output files that are stored in template package
for _, filePath := range templates.AssetNames() {
fileBytes, _ := templates.Asset(filePath)
fullPath := workingDirectory + "/" + filePath
// Rename .gotemplate to .go
if strings.HasSuffix(fullPath, ".gotemplate") {
fullPath = strings.TrimSuffix(fullPath, "template")
}
err := ioutil.WriteFile(fullPath, fileBytes, 0666)
if err != nil {
log.WithField("FilePath", fullPath).WithError(err).Fatal("Cannot create ")
}
}
}
// goBuild calls the `$ go get ` to install dependenices
// and then calls `$ go build service/bin/$name $path`
// to put the iterating binaries in the correct place
func goBuild(name string, path string, done chan bool) {
// $ go get
goGetExec := exec.Command(
"go",
"get",
"-d",
"-v",
path,
)
goGetExec.Stderr = os.Stderr
log.WithField("cmd", strings.Join(goGetExec.Args, " ")).Info("go get")
val, err := goGetExec.Output()
if err != nil {
log.WithFields(log.Fields{
"output": string(val),
"input": goGetExec.Args,
}).WithError(err).Warn("go get failed")
}
// $ go build
goBuildExec := exec.Command(
"go",
"build",
"-o",
"service/bin/"+name,
path,
)
goBuildExec.Stderr = os.Stderr
log.WithField("cmd", strings.Join(goBuildExec.Args, " ")).Info("go build")
val, err = goBuildExec.Output()
if err != nil {
log.WithFields(log.Fields{
"output": string(val),
"input": goBuildExec.Args,
}).WithError(err).Fatal("go build failed")
}
done <- true
}
func protoc(workingDirectory string, definitionPaths []string, command string, done chan bool) {
const googleApiHttpImportPath = "/service/DONOTEDIT/third_party/googleapis"
cmdArgs := []string{
"-I.",
"-I" + workingDirectory + googleApiHttpImportPath,
command,
}
// Append each definition file path to the end of that command args
cmdArgs = append(cmdArgs, definitionPaths...)
protocExec := exec.Command(
"protoc",
cmdArgs...,
)
protocExec.Stderr = os.Stderr
log.WithField("cmd", strings.Join(protocExec.Args, " ")).Info("protoc")
val, err := protocExec.Output()
if err != nil {
log.WithFields(log.Fields{
"output": string(val),
"input": protocExec.Args,
}).WithError(err).Fatal("Protoc call failed")
}
done <- true
}

Просмотреть файл

@ -2,44 +2,111 @@ package main
import (
"flag"
//"fmt"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/TuneLab/gob/truss/generator"
"github.com/pkg/errors"
log "github.com/Sirupsen/logrus"
"github.com/TuneLab/gob/truss/protostage"
"github.com/TuneLab/gob/truss/truss"
"github.com/TuneLab/gob/gendoc"
"github.com/TuneLab/gob/gendoc/doctree/makedt"
"github.com/TuneLab/gob/gengokit"
)
func init() {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.TextFormatter{
ForceColors: true,
})
}
// Stages are documented in README.md
func main() {
noBuild := flag.Bool("nobuild", false, "Set -nobuild to generate code without building")
flag.Parse()
if len(flag.Args()) == 0 {
log.Fatal("No proto files passed")
os.Exit(1)
exitIfError(errors.New("no arguments passed"))
}
rawDefinitionPaths := flag.Args()
protoDir, definitionFiles, err := cleanProtofilePath(rawDefinitionPaths)
var files []*os.File
for _, f := range definitionFiles {
protoF, err := os.Open(f)
exitIfError(errors.Wrapf(err, "could not open %v", protoF))
files = append(files, protoF)
}
// Check truss is running in $GOPATH
goPath := os.Getenv("GOPATH")
if !strings.HasPrefix(protoDir, goPath) {
exitIfError(errors.New("truss envoked on files outside of $GOPATH"))
}
// Stage directories and files needed on disk
err = protostage.Stage(protoDir)
exitIfError(err)
// Generate the .pb.go files containing the golang data structures
// From `$GOPATH/src/org/user/thing` get `org/user/thing` for importing in golang
goImportPath := strings.TrimPrefix(protoDir, goPath+"/src/")
err = protostage.GeneratePBDataStructures(definitionFiles, protoDir, goImportPath)
// Compose protocOut and service file to make a doctree
protocOut, serviceFile, err := protostage.Compose(definitionFiles, protoDir)
exitIfError(err)
// Make a doctree
dt, err := makedt.New(protocOut, serviceFile)
exitIfError(err)
prevGen, err := readPreviousGeneration(protoDir)
exitIfError(err)
// generate docs
genDocFiles := gendoc.GenerateDocs(dt)
// generate gokit microservice
genFiles, err := gengokit.GenerateGokit(dt, prevGen, goImportPath)
exitIfError(err)
// append files together
genFiles = append(genFiles, genDocFiles...)
// Write files to disk
for _, f := range genFiles {
name := *(f.Name)
content := *(f.Content)
mkdir(name)
err = ioutil.WriteFile(name, []byte(content), 0666)
exitIfError(errors.Wrapf(err, "could not write to %v", name))
}
if !*noBuild {
err := buildMicroservice()
exitIfError(errors.Wrap(err, "could not build microservice"))
}
}
// cleanProtofilePath takes a slice of file paths and returns the
// absolute directory that contains the file paths, an array of the basename
// of the files, or an error if the files are not in the same directory
func cleanProtofilePath(rawPaths []string) (wd string, definitionFiles []string, err error) {
execWd, err := os.Getwd()
if err != nil {
log.WithError(err).Fatal("Cannot get working directory")
return "", nil, errors.Wrap(err, "could not get working directoru of truss")
}
var workingDirectory string
var definitionFiles []string
// Parsed passed file paths
for _, def := range rawDefinitionPaths {
for _, def := range rawPaths {
// If the definition file path is not absolute, then make it absolute using trusses working directory
if !path.IsAbs(def) {
def = path.Clean(def)
@ -47,29 +114,175 @@ func main() {
}
// The working direcotry for this definition file
wd := path.Dir(def)
dir := path.Dir(def)
// Add the base name of definition file to the slice
definitionFiles = append(definitionFiles, path.Base(def))
// If the working directory has not beenset before set it
if workingDirectory == "" {
workingDirectory = wd
workingDirectory = dir
} else {
// If the working directory for this definition file is different than the previous
if wd != workingDirectory {
log.Fatal("Passed protofiles reside in different directories")
if workingDirectory != dir {
return "", nil,
errors.Errorf(
"all .proto files must reside in the same directory\n"+
"these two differ: \n%v\n%v",
wd,
workingDirectory)
}
}
}
goPath := os.Getenv("GOPATH")
return workingDirectory, definitionFiles, nil
}
if !strings.HasPrefix(workingDirectory, goPath) {
log.Fatal("truss envoked from outside of $GOPATH")
// mkdir acts like $ mkdir -p path
func mkdir(path string) error {
dir := filepath.Dir(path)
// 0775 is the file mode that $ mkdir uses when creating a directoru
err := os.MkdirAll(dir, 0775)
return err
}
func exitIfError(err error) {
if errors.Cause(err) != nil {
defer os.Exit(1)
fmt.Printf("%v\n", err)
}
}
func buildMicroservice() (err error) {
const serverPath = "./service/DONOTEDIT/cmd/svc/..."
const clientPath = "./service/DONOTEDIT/cmd/svc/..."
// Build server and client
errChan := make(chan error)
go goBuild("server", serverPath, errChan)
go goBuild("cliclient", clientPath, errChan)
err = <-errChan
if err != nil {
return err
}
// From `$GOPATH/src/org/user/thing` get `org/user/thing` for importing in golang
genImportPath := strings.TrimPrefix(workingDirectory, goPath+"/src/")
err = <-errChan
if err != nil {
return err
}
generator.GenerateMicroservice(genImportPath, workingDirectory, definitionFiles)
return nil
}
// goBuild calls the `$ go get ` to install dependenices
// and then calls `$ go build service/bin/$name $path`
// to put the iterating binaries in the correct place
func goBuild(name string, path string, errChan chan error) {
// $ go get
goGetExec := exec.Command(
"go",
"get",
"-d",
"-v",
path,
)
goGetExec.Stderr = os.Stderr
err := goGetExec.Run()
if err != nil {
errChan <- errors.Wrapf(err, "could not $ go get %v", path)
return
}
// $ go build
goBuildExec := exec.Command(
"go",
"build",
"-o",
"service/bin/"+name,
path,
)
goBuildExec.Stderr = os.Stderr
err = goBuildExec.Run()
if err != nil {
errChan <- errors.Wrapf(err, "could not $ go build %v", path)
return
}
errChan <- nil
}
// readPreviousGeneration accepts the path to the directory where the inputed .proto files are stored, protoDir,
// it returns a []truss.SimpleFile for all files in the service/ dir in protoDir
func readPreviousGeneration(protoDir string) ([]truss.SimpleFile, error) {
if fileExists(protoDir+"/service") != true {
return nil, nil
}
var files []truss.SimpleFile
sfs := simpleFileConstructor{
protoDir: protoDir,
files: files,
}
err := filepath.Walk(protoDir+"/service", sfs.makeSimpleFile)
if err != nil {
return nil, errors.Wrapf(err, "could not fully walk directory %v/service", protoDir)
}
return sfs.files, nil
}
// simpleFileConstructor has the function makeSimpleFile which is of type filepath.WalkFunc
// This allows for filepath.Walk to be called with makeSimpleFile and build a truss.SimpleFile
// for all files in a direcotry
type simpleFileConstructor struct {
protoDir string
files []truss.SimpleFile
}
// makeSimpleFile is of type filepath.WalkFunc
// makeSimpleFile constructs a truss.SimpleFile and stores it in SimpleFileConstructor.files
func (sfs *simpleFileConstructor) makeSimpleFile(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
byteContent, ioErr := ioutil.ReadFile(path)
if ioErr != nil {
return errors.Wrapf(ioErr, "could not read file: %v", path)
}
// name will be in the always start with "service/"
// trim the prefix of the path to the proto files from the full path to the file
name := strings.TrimPrefix(path, sfs.protoDir+"/")
content := string(byteContent)
file := truss.SimpleFile{
Name: &name,
Content: &content,
}
sfs.files = append(sfs.files, file)
return nil
}
// fileExists checks if a file at the given path exists. Returns true if the
// file exists, and false if the file does not exist.
func fileExists(path string) bool {
if _, err := os.Stat(path); err == nil {
return true
}
return false
}

Просмотреть файл

@ -0,0 +1,276 @@
package protostage
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/golang/protobuf/proto"
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
"github.com/pkg/errors"
templates "github.com/TuneLab/gob/truss/template"
)
// Stage outputs the following files and directories in place
// that are required for protoc imports and go build.
// .
// └── service
// ├── bin
// └── DONOTEDIT
// ├── pb
// └── third_party
// └── googleapis
// └── google
// └── api
// ├── annotations.pb.go
// ├── annotations.proto
// ├── http.pb.go
// └── http.proto
func Stage(protoDir string) error {
err := buildDirectories(protoDir)
if err != nil {
return err
}
err = outputGoogleImport(protoDir)
if err != nil {
return err
}
return nil
}
// GeneratePBDataStructures calls $ protoc with the protoc-gen-go to output
// .pb.go files in ./service/DONOTEDIT/pb which contain the golang
// datastructures represened in the .proto files
func GeneratePBDataStructures(protoFiles []string, protoDir, importPath string) error {
const pbDataStructureDir = "/service/DONOTEDIT/pb"
genGoCode := "--go_out=Mgoogle/api/annotations.proto=" +
importPath +
"/service/DONOTEDIT/third_party/googleapis/google/api," +
"plugins=grpc:" +
protoDir +
"/service/DONOTEDIT/pb"
_, err := exec.LookPath("protoc-gen-go")
if err != nil {
return errors.Wrap(err, "protoc-gen-go not exist in $PATH")
}
err = protoc(protoFiles, protoDir, protoDir+pbDataStructureDir, genGoCode)
if err != nil {
return errors.Wrap(err, "could not generate go code from .proto files")
}
return nil
}
// Compose gets the parsed output from protoc, marshals that output to a
// CodeGeneratorRequest. Then Compose finds the .proto file containing the
// service definition and returns the CodeGeneratorRequest and the service
// definition file
func Compose(protoFiles []string, protoDir string) (*plugin.CodeGeneratorRequest, *os.File, error) {
protocOut, err := getProtocOutput(protoFiles, protoDir)
if err != nil {
return nil, nil, errors.Wrap(err, "could not get output from protoc")
}
req := new(plugin.CodeGeneratorRequest)
if err = proto.Unmarshal(protocOut, req); err != nil {
return nil, nil, errors.Wrap(err, "could not marshal protoc ouput to code generator request")
}
svcFile, err := findServiceFile(req, protoDir)
if err != nil {
return nil, nil, errors.Wrap(err, "unable to find which input file contains a service")
}
return req, svcFile, nil
}
// absoluteDir takes a path to file or directory if path is to a file return
// the absolute path to the directory containing the file if path is to a
// directory return the absolute path of that directory
func absoluteDir(path string) (string, error) {
absP, err := filepath.Abs(filepath.Dir(path))
if err != nil {
return "", errors.Wrapf(err, "cannot find absolute path of %v", path)
}
return absP, nil
}
// getProtocOutput calls exec's $ protoc with the passed protofiles and the
// protoc-gen-truss-protocast plugin and returns the output of protoc
func getProtocOutput(protoFiles []string, protoFileDir string) ([]byte, error) {
_, err := exec.LookPath("protoc-gen-truss-protocast")
if err != nil {
return nil, errors.Wrap(err, "protoc-gen-truss-protocast does not exist in $PATH")
}
protocOutDir, err := ioutil.TempDir("", "truss-")
defer os.RemoveAll(protocOutDir)
log.WithField("Protoc output dir", protocOutDir).Debug("Protoc output directory created")
const plugin = "--truss-protocast_out=."
err = protoc(protoFiles, protoFileDir, protocOutDir, plugin)
if err != nil {
return nil, errors.Wrap(err, "protoc failed")
}
fileInfo, err := ioutil.ReadDir(protocOutDir)
if err != nil {
return nil, errors.Wrapf(err, "could not read directory: %v", protocOutDir)
}
var protocOut []byte
if len(fileInfo) > 0 {
fileName := fileInfo[0].Name()
filePath := protocOutDir + "/" + fileName
protocOut, err = ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "cannot read file: %v", filePath)
}
} else {
return nil, errors.Errorf("no protoc output file found in: %v", protocOutDir)
}
return protocOut, nil
}
// protoc exec's $ protoc on protoFiles, on their full path which is created with protoDir
func protoc(protoFiles []string, protoDir, outDir, plugin string) error {
const googleAPIHTTPImportPath = "/service/DONOTEDIT/third_party/googleapis"
var fullPaths []string
for _, f := range protoFiles {
fullPaths = append(fullPaths, protoDir+"/"+f)
}
cmdArgs := []string{
//"-I.",
"-I" + protoDir + googleAPIHTTPImportPath,
"--proto_path=" + protoDir,
plugin,
}
// Append each definition file path to the end of that command args
cmdArgs = append(cmdArgs, fullPaths...)
protocExec := exec.Command(
"protoc",
cmdArgs...,
)
log.Debug(protocExec.Args)
protocExec.Dir = outDir
outBytes, err := protocExec.CombinedOutput()
if err != nil {
return errors.Wrapf(err,
"protoc exec failed.\nprotoc output:\n\n%v\nprotoc arguments:\n\n%v\n\n",
string(outBytes), protocExec.Args)
}
return nil
}
// findServiceFile Searches through the files in the request and returns the
// path to the first one which contains a service declaration. If no file in
// the request contains a service, returns an empty string.
func findServiceFile(req *plugin.CodeGeneratorRequest, protoFileDir string) (*os.File, error) {
var svcFileName string
for _, file := range req.GetProtoFile() {
if len(file.GetService()) > 0 {
svcFileName = file.GetName()
}
}
if svcFileName == "" {
return nil, errors.New("passed protofiles contain no service")
}
svc, err := os.Open(protoFileDir + "/" + svcFileName)
if err != nil {
return nil, errors.Wrapf(err, "could not open service file: %v\n in path: %v",
protoFileDir, svcFileName)
}
return svc, nil
}
// buildDirectories puts the following directories in place
// .
// └── service
// ├── bin
// └── DONOTEDIT
// ├── pb
// └── third_party
// └── googleapis
// └── google
// └── api
func buildDirectories(protoDir string) error {
// third_party created by going through assets in template
// and creating directoires that are not there
for _, fp := range templates.AssetNames() {
dir := filepath.Dir(protoDir + "/" + fp)
err := mkdir(dir)
if err != nil {
return errors.Wrapf(err, "unable to create directory for %v", dir)
}
}
// Create the directory where protoc will store the compiled .pb.go files
p := protoDir + "/service/DONOTEDIT/pb"
err := mkdir(p)
if err != nil {
return errors.Wrapf(err, "unable to create directory for %v", p)
}
// Create the directory where go build will put the compiled binaries
p = protoDir + "/service/bin"
err = mkdir(p)
if err != nil {
return errors.Wrapf(err, "unable to create directory for %v", p)
}
return nil
}
// mkdir acts like $ mkdir -p path
func mkdir(path string) error {
// 0775 is the file mode that $ mkdir uses when creating a directoru
err := os.MkdirAll(path, 0775)
return err
}
// outputGoogleImport places imported and required google.api.http protobuf option files
// into their required directories as part of stage one generation
func outputGoogleImport(workingDirectory string) error {
// Output files that are stored in template package
for _, filePath := range templates.AssetNames() {
fileBytes, _ := templates.Asset(filePath)
fullPath := workingDirectory + "/" + filePath
// Rename .gotemplate to .go
if strings.HasSuffix(fullPath, ".gotemplate") {
fullPath = strings.TrimSuffix(fullPath, "template")
}
err := ioutil.WriteFile(fullPath, fileBytes, 0666)
if err != nil {
return errors.Wrapf(err, "cannot create template file at path %v", fullPath)
}
}
return nil
}

Просмотреть файл

@ -86,7 +86,7 @@ func serviceDonoteditThird_partyGoogleapisGoogleApiAnnotationsPbGotemplate() (*a
return nil, err
}
info := bindataFileInfo{name: "service/DONOTEDIT/third_party/googleapis/google/api/annotations.pb.gotemplate", size: 2514, mode: os.FileMode(436), modTime: time.Unix(1470178137, 0)}
info := bindataFileInfo{name: "service/DONOTEDIT/third_party/googleapis/google/api/annotations.pb.gotemplate", size: 2514, mode: os.FileMode(436), modTime: time.Unix(1470266210, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -126,7 +126,7 @@ func serviceDonoteditThird_partyGoogleapisGoogleApiHttpPbGotemplate() (*asset, e
return nil, err
}
info := bindataFileInfo{name: "service/DONOTEDIT/third_party/googleapis/google/api/http.pb.gotemplate", size: 18398, mode: os.FileMode(436), modTime: time.Unix(1470178137, 0)}
info := bindataFileInfo{name: "service/DONOTEDIT/third_party/googleapis/google/api/http.pb.gotemplate", size: 18398, mode: os.FileMode(436), modTime: time.Unix(1470266210, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

11
truss/truss/truss.go Normal file
Просмотреть файл

@ -0,0 +1,11 @@
// Package truss contains the relative file tree data structure that represents
// the paths and contents of generated files
package truss
// SimpleFile stores a file name and that file's content
// Name is a path relative to the directory containing the .proto files
// Name should start with "service/" for all generated and read in files
type SimpleFile struct {
Name *string
Content *string
}