Major truss refactor
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:
Родитель
e8e3e8ec8c
Коммит
78d2ce7eaa
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
265
truss/main.go
265
truss/main.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Загрузка…
Ссылка в новой задаче