Merge pull request #70 from hasAdamr/refactor-protostage

Add -pbout flag for .pb.go out directory; Large refactor for pipeline changes.
This commit is contained in:
Adam Ryman 2016-10-13 10:46:59 -07:00 коммит произвёл GitHub
Родитель a2bd9bc548 30bde78a55
Коммит d3caffcf87
22 изменённых файлов: 563 добавлений и 527 удалений

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

@ -3,8 +3,10 @@
## Dependencies
1. Everything required to install `truss`
2. go-bindata for compiling templates into binary `$ go get
github.com/jteeuwen/go-bindata`
2. go-bindata for compiling templates into binary
```
$ go get github.com/jteeuwen/go-bindata/...
```
## Building
@ -15,7 +17,7 @@ this is done with:
$ go generate github.com/TuneLab/go-truss/...
```
Then to build truss and it's two protoc plugins to your $GOPATH/bin directory:
Then to build truss and its protoc plugin to your $GOPATH/bin directory:
```
$ go install github.com/TuneLab/go-truss/...
@ -66,13 +68,3 @@ Additional internal packages of note used by these programs are:
- `deftree`, located in `deftree/`, which makes sense of the protobuf file
passed to it by `protoc`, and is used by `gengokit` and
`gendoc`
## Docker
BETA
To build the docker image `$ docker build -t tunelab/gob/truss .`
To use the docker image as `truss` on .proto files `$ docker run -it --rm
--name test -v $PWD:/gopath/src/microservice -w /gopath/src/microservice
tunelab/gob/truss *.proto`

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

@ -19,7 +19,7 @@ import (
"github.com/pkg/errors"
"github.com/TuneLab/go-truss/deftree/svcparse"
"github.com/TuneLab/go-truss/truss/protostage"
"github.com/TuneLab/go-truss/truss/execprotoc"
)
var gengo *generator.Generator
@ -101,24 +101,21 @@ func NewFromString(def string) (Deftree, error) {
}
defer os.RemoveAll(protoDir)
err = ioutil.WriteFile(filepath.Join(protoDir, defFileName), []byte(def), 0666)
defPath := filepath.Join(protoDir, defFileName)
err = ioutil.WriteFile(defPath, []byte(def), 0666)
if err != nil {
return nil, errors.Wrap(err, "could not write proto definition to file")
}
err = protostage.Stage(protoDir)
req, err := execprotoc.CodeGeneratorRequest([]string{defPath})
if err != nil {
return nil, errors.Wrap(err, "unable to prepare filesystem for generating deftree")
return nil, errors.Wrap(err, "unable to create a proto CodeGeneratorRequest")
}
req, svcFile, err := protostage.Compose([]string{defFileName}, protoDir)
deftree, err := New(req, strings.NewReader(def))
if err != nil {
return nil, errors.Wrap(err, "unable to get CodeGeneratorRequest for generating deftree")
}
deftree, err := New(req, svcFile)
if err != nil {
return nil, errors.Wrap(err, "can not create new deftree")
return nil, errors.Wrap(err, "could not create new deftree from CodeGeneratorRequest and definition")
}
return deftree, nil

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

@ -31,11 +31,13 @@ func init() {
})
}
// templateExecutor is passed to templates as the executing struct its fields
// 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 the directory containing the definition .proto files
ImportPath string
// import path for .pb.go files containing service structs
PBImportPath string
// PackageName is the name of the package containing the service definition
PackageName string
// GRPC/Protobuff service, with all parameters and return values accessible
@ -46,15 +48,12 @@ type templateExecutor struct {
funcMap template.FuncMap
}
// newTemplateExecutor accepts a deftree and a goImportPath to construct a
// templateExecutor for templating a service
func newTemplateExecutor(dt deftree.Deftree, goImportPath string) (*templateExecutor, error) {
func newTemplateExecutor(dt deftree.Deftree, goPackage, goPBPackage string) (*templateExecutor, error) {
service, err := getProtoService(dt)
if err != nil {
return nil, errors.Wrap(err, "no service found; aborting generating gokit service")
}
importPath := goImportPath + "/" + dt.GetName() + "-service"
funcMap := template.FuncMap{
"ToLower": strings.ToLower,
"Title": strings.Title,
@ -62,28 +61,29 @@ func newTemplateExecutor(dt deftree.Deftree, goImportPath string) (*templateExec
"TrimPrefix": strings.TrimPrefix,
}
return &templateExecutor{
ImportPath: importPath,
PackageName: dt.GetName(),
Service: service,
ClientArgs: clientarggen.New(service),
HTTPHelper: httptransport.NewHelper(service),
funcMap: funcMap,
ImportPath: goPackage,
PBImportPath: goPBPackage,
PackageName: dt.GetName(),
Service: service,
ClientArgs: clientarggen.New(service),
HTTPHelper: httptransport.NewHelper(service),
funcMap: funcMap,
}, nil
}
// GenerateGokit accepts a deftree representing the ast of a group of .proto
// files, a []truss.NamedReadWriter representing files generated previously
// (which may be nil for no previously generated files), and a goImportPath for
// templating go code imports. GenerateGokit returns the a
// []truss.NamedReadWriter representing a generated gokit service file
// structure
func GenerateGokit(dt deftree.Deftree, previousFiles []truss.NamedReadWriter, goImportPath string) ([]truss.NamedReadWriter, error) {
te, err := newTemplateExecutor(dt, goImportPath)
// GenerateGokit returns a gokit service generated from a service definition (deftree),
// the package to the root of the generated service goPackage, the package
// to the .pb.go service struct files (goPBPackage) and any prevously generated files.
func GenerateGokit(dt deftree.Deftree, goPackage, goPBPackage string, previousFiles []truss.NamedReadWriter) ([]truss.NamedReadWriter, error) {
te, err := newTemplateExecutor(dt, goPackage, goPBPackage)
if err != nil {
return nil, errors.Wrap(err, "could not create template executor")
}
fpm := filesToFilepathMap(previousFiles)
fpm := make(map[string]io.Reader, len(previousFiles))
for _, f := range previousFiles {
fpm[f.Name()] = f
}
var codeGenFiles []truss.NamedReadWriter
@ -192,7 +192,7 @@ func generateResponseFile(templFP string, te *templateExecutor, prevGenMap map[s
// disk
func templatePathToActual(templFilePath, packageName string) string {
// Switch "NAME" in path with packageName.
//i.e. for packageName = addsvc; /NAME-service/NAME-server -> /addsvc-service/addsvc-server
// i.e. for packageName = addsvc; /NAME-service/NAME-server -> /addsvc-service/addsvc-server
actual := strings.Replace(templFilePath, "NAME", packageName, -1)
actual = strings.TrimSuffix(actual, "template")
@ -200,16 +200,6 @@ func templatePathToActual(templFilePath, packageName string) string {
return actual
}
// filesTofilepathMap accepts a slice of truss.NamedReadWriters and returns a map of filepaths
// as strings to files as io.Reader
func filesToFilepathMap(previousFiles []truss.NamedReadWriter) map[string]io.Reader {
pathToFile := make(map[string]io.Reader, len(previousFiles))
for _, f := range previousFiles {
pathToFile[f.Name()] = f
}
return pathToFile
}
// updateServerMethods accepts NAME-service/handlers/server/server_handlers.go
// as an io.Reader and modifies it to contains only functions returned by
// serviceFunctionNames.
@ -320,8 +310,7 @@ func (te templateExecutor) trimServiceFuncs(funcsInFile map[string]bool) *templa
return &te
}
// applyTemplateFromPath accepts a path to a template and a templateExecutor and calls
// applyTemplate with the template at that template path and returns the result
// applyTemplateFromPath calls applyTemplate with the template at templFilePath
func applyTemplateFromPath(templFilePath string, executor *templateExecutor) (io.Reader, error) {
templBytes, err := templateFileAssets.Asset(templFilePath)
@ -332,9 +321,6 @@ func applyTemplateFromPath(templFilePath string, executor *templateExecutor) (io
return applyTemplate(templBytes, templFilePath, executor)
}
// applyTemplate accepts a []byte, and a string representing the content and
// name of a template and a templateExecutor to execute on that template. Returns a
// io.Reader containing the results of that execution
func applyTemplate(templBytes []byte, templName string, executor *templateExecutor) (io.Reader, error) {
templateString := string(templBytes)

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

@ -69,17 +69,14 @@ func TestNewTemplateExecutor(t *testing.T) {
t.Fatal(err)
}
const goImportPath = "github.com/TuneLab/go-truss/gengokit"
const goPackage = "github.com/TuneLab/go-truss/gengokit/general-service"
const goPBPackage = "github.com/TuneLab/go-truss/gengokit/general-service"
te, err := newTemplateExecutor(dt, goImportPath)
te, err := newTemplateExecutor(dt, goPackage, goPBPackage)
if err != nil {
t.Fatal(err)
}
if got, want := te.ImportPath, goImportPath+"/general-service"; got != want {
t.Fatalf("\n`%v` was ImportPath\n`%v` was wanted", got, want)
}
if got, want := te.PackageName, dt.GetName(); got != want {
t.Fatalf("\n`%v` was PackageName\n`%v` was wanted", got, want)
}
@ -165,14 +162,15 @@ func TestApplyTemplateFromPath(t *testing.T) {
}
`
const goImportPath = "github.com/TuneLab/go-truss/gengokit"
const goPackage = "github.com/TuneLab/go-truss/gengokit"
const goPBPackage = "github.com/TuneLab/go-truss/gengokit/general-service"
dt, err := deftree.NewFromString(def)
if err != nil {
t.Fatal(err)
}
te, err := newTemplateExecutor(dt, goImportPath)
te, err := newTemplateExecutor(dt, goPackage, goPBPackage)
if err != nil {
t.Fatal(err)
}
@ -195,7 +193,7 @@ func TestApplyTemplateFromPath(t *testing.T) {
}
func TestTrimTemplateExecutorServiceFuncs(t *testing.T) {
const goImportPath = "github.com/TuneLab/go-truss/gengokit"
const goPackage = "github.com/TuneLab/go-truss/gengokit"
const def = `
syntax = "proto3";
@ -247,7 +245,7 @@ func TestTrimTemplateExecutorServiceFuncs(t *testing.T) {
"ProtoMethodAgainAgain": true,
}
te, err := stringToTemplateExector(def, goImportPath)
te, err := stringToTemplateExector(def, goPackage)
if err != nil {
t.Fatal(err)
}
@ -297,7 +295,7 @@ func stringToTemplateExector(def, importPath string) (*templateExecutor, error)
return nil, err
}
te, err := newTemplateExecutor(dt, importPath)
te, err := newTemplateExecutor(dt, importPath, importPath)
if err != nil {
return nil, err
}
@ -305,6 +303,7 @@ func stringToTemplateExector(def, importPath string) (*templateExecutor, error)
return te, nil
}
func TestUpdateServerMethods(t *testing.T) {
const def = `
syntax = "proto3";
@ -336,14 +335,15 @@ func TestUpdateServerMethods(t *testing.T) {
}
`
const goImportPath = "github.com/TuneLab/go-truss/gengokit"
const goPackage = "github.com/TuneLab/go-truss/gengokit"
const goPBPackage = "github.com/TuneLab/go-truss/gengokit/general-service"
dt, err := deftree.NewFromString(def)
if err != nil {
t.Fatal(err)
}
te, err := newTemplateExecutor(dt, goImportPath)
te, err := newTemplateExecutor(dt, goPackage, goPBPackage)
if err != nil {
t.Fatal(err)
}
@ -392,7 +392,8 @@ func TestUpdateServerMethods(t *testing.T) {
}
func TestAllTemplates(t *testing.T) {
const goImportPath = "github.com/TuneLab/go-truss/gengokit"
const goPackage = "github.com/TuneLab/go-truss/gengokit"
const goPBPackage = "github.com/TuneLab/go-truss/gengokit/general-service"
const def = `
syntax = "proto3";
@ -466,7 +467,7 @@ func TestAllTemplates(t *testing.T) {
t.Fatal(err)
}
te, err := newTemplateExecutor(dt, goImportPath)
te, err := newTemplateExecutor(dt, goPackage, goPBPackage)
if err != nil {
t.Fatal(err)
}
@ -476,7 +477,7 @@ func TestAllTemplates(t *testing.T) {
t.Fatal(err)
}
te2, err := newTemplateExecutor(dt2, goImportPath)
te2, err := newTemplateExecutor(dt2, goPackage, goPBPackage)
if err != nil {
t.Fatal(err)
}

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

@ -27,7 +27,7 @@ import (
clientHandler "{{$templateExecutor.ImportPath -}} /handlers/client"
grpcclient "{{$templateExecutor.ImportPath -}} /generated/client/grpc"
httpclient "{{$templateExecutor.ImportPath -}} /generated/client/http"
pb "{{$templateExecutor.ImportPath -}}"
pb "{{$templateExecutor.PBImportPath -}}"
)
var (

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

@ -33,7 +33,7 @@ import (
// This Service
handler "{{$templateExecutor.ImportPath -}} /handlers/server"
svc "{{$templateExecutor.ImportPath -}} /generated"
pb "{{$templateExecutor.ImportPath -}}"
pb "{{$templateExecutor.PBImportPath -}}"
)

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

@ -20,7 +20,7 @@ import (
// This Service
handler "{{$templateExecutor.ImportPath -}} /handlers/server"
svc "{{$templateExecutor.ImportPath -}} /generated"
pb "{{$templateExecutor.ImportPath -}}"
pb "{{$templateExecutor.PBImportPath -}}"
)
// New returns an AddService backed by a gRPC client connection. It is the

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

@ -16,7 +16,7 @@ import (
//_ "github.com/go-kit/kit/metrics"
handler "{{.ImportPath -}} /handlers/server"
pb "{{.ImportPath -}}"
pb "{{.PBImportPath -}}"
)
// Endpoints collects all of the endpoints that compose an add service. It's

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

@ -12,7 +12,7 @@ import (
//_ "github.com/go-kit/kit/log"
//_ "github.com/go-kit/kit/metrics"
//pb "{{.ImportPath -}} "
//pb "{{.PBImportPath -}} "
)
// Middleware describes a service (as opposed to endpoint) middleware.

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

@ -13,7 +13,7 @@ import (
grpctransport "github.com/go-kit/kit/transport/grpc"
// This Service
pb "{{$templateExecutor.ImportPath -}}"
pb "{{$templateExecutor.PBImportPath -}}"
)

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

@ -25,7 +25,7 @@ import (
httptransport "github.com/go-kit/kit/transport/http"
// This service
pb "{{$templateExecutor.ImportPath -}}"
pb "{{$templateExecutor.PBImportPath -}}"
)
var (

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

@ -2,7 +2,7 @@
package clienthandler
import (
pb "{{$templateExecutor.ImportPath -}}"
pb "{{$templateExecutor.PBImportPath -}}"
)

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

@ -12,7 +12,7 @@ import (
_ "github.com/go-kit/kit/log"
_ "github.com/go-kit/kit/metrics"
pb "{{.ImportPath -}}"
pb "{{.PBImportPath -}}"
)

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -7,17 +7,20 @@ HTTPTEST_MSG=\n$(OK_COLOR)Starting http end to end test:$(END_COLOR_LINE)
CLITEST_MSG=\n$(OK_COLOR)Start server and cliclient generate, build, and run test:$(END_COLOR_LINE)
all: test
test: clean httptest clitest
test: clean test-http test-cli
httptest:
@echo -e '$(HTTPTEST_MSG)'
test-http:
@printf '$(HTTPTEST_MSG)'
$(MAKE) -C http
clitest:
@echo -e '$(CLITEST_MSG)'
test-cli:
@printf '$(CLITEST_MSG)'
go test -v ./cli
clean:
go test ./cli -clean
$(MAKE) -C http clean

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

@ -35,6 +35,12 @@ func TestBasicTypes(t *testing.T) {
testEndToEnd("1-basic", t)
}
func TestBasicTypesWithPBOutFlag(t *testing.T) {
testEndToEnd("1-basic", t,
"-pbout",
"github.com/TuneLab/go-truss/truss/_integration-tests/cli/test-service-definitions/1-basic/pbout")
}
// Disabled until repeated types are implemented for cliclient
func _TestRepeatedTypes(t *testing.T) {
testEndToEnd("2-repeated", t)
@ -62,13 +68,16 @@ type runReference struct {
serverOutput string
}
func testEndToEnd(defDir string, t *testing.T) {
func testEndToEnd(defDir string, t *testing.T, trussOptions ...string) {
port := 45360
wd, _ := os.Getwd()
fullpath := filepath.Join(wd, definitionDirectory, defDir)
trussOut, err := truss(fullpath)
// Remove tests if they exists
removeTestFiles(fullpath)
trussOut, err := truss(fullpath, trussOptions...)
// If truss fails, test error and skip communication
if err != nil {
@ -78,7 +87,7 @@ func testEndToEnd(defDir string, t *testing.T) {
// Build the service to be tested
err = buildTestService(fullpath)
if err != nil {
t.Fatalf("Could not buld service. Error: %v", err)
t.Fatalf("Could not build service. Error: %v", err)
}
// Run them save a reference to each run
@ -97,7 +106,7 @@ func testEndToEnd(defDir string, t *testing.T) {
// truss calls truss on *.proto in path
// Truss logs to Stdout when generation passes or fails
func truss(path string) (string, error) {
func truss(path string, options ...string) (string, error) {
var protofiles []string
files, err := ioutil.ReadDir(path)
for _, f := range files {
@ -109,9 +118,11 @@ func truss(path string) (string, error) {
}
}
args := append(options, protofiles...)
trussExec := exec.Command(
"truss",
protofiles...,
args...,
)
trussExec.Dir = path
@ -136,7 +147,7 @@ func buildTestService(serviceDir string) (err error) {
binDir := serviceDir + "/bin"
err = mkdir(binDir)
err = os.MkdirAll(binDir, 0777)
if err != nil {
return err
}
@ -203,16 +214,6 @@ func goBuild(name, outputPath, relCodePath string, errChan chan error) {
errChan <- nil
}
// 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
}
// runServerAndClient execs a TEST-server and TEST-client and puts a
// runReference to their interaction on the runRefs channel
func runServerAndClient(path string, port int, debugPort int) runReference {
@ -342,16 +343,15 @@ func cleanTests(servicesDir string) {
if !d.IsDir() {
continue
}
removeTestFiles(servicesDir + "/" + d.Name())
removeTestFiles(filepath.Join(servicesDir, d.Name()))
}
}
// removeTestFiles removes all files created by running truss and building the
// service from a single definition directory
func removeTestFiles(defDir string) {
if fileExists(defDir + "/TEST-service") {
os.RemoveAll(defDir + "/TEST-service")
os.RemoveAll(defDir + "/third_party")
os.RemoveAll(defDir + "/bin")
}
os.RemoveAll(filepath.Join(defDir, "TEST-service"))
os.RemoveAll(filepath.Join(defDir, "bin"))
os.RemoveAll(filepath.Join(defDir, "pbout"))
os.MkdirAll(filepath.Join(defDir, "pbout"), 0777)
}

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

@ -0,0 +1,188 @@
// Package execprotoc provides an interface for interacting with protoc
// requiring only paths to files on disk
package execprotoc
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/golang/protobuf/proto"
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
"github.com/pkg/errors"
assets "github.com/TuneLab/go-truss/truss/template"
)
// GeneratePBDotGo creates .pb.go files from the passed protoPaths and writes
// them to outDir. These files import Google api's which will be created at
// importDir.
func GeneratePBDotGo(protoPaths []string, importDir, outDir string) error {
err := outputGoogleImport(importDir)
if err != nil {
return err
}
goImportPath, err := filepath.Rel(filepath.Join(os.Getenv("GOPATH"), "src"), importDir)
if err != nil {
return err
}
genGoCode := "--go_out=Mgoogle/api/annotations.proto=" +
goImportPath + "/third_party/googleapis/google/api," +
"plugins=grpc:" +
outDir
_, err = exec.LookPath("protoc-gen-go")
if err != nil {
return errors.Wrap(err, "cannot find protoc-gen-go in PATH")
}
err = protoc(protoPaths, importDir, genGoCode)
if err != nil {
return errors.Wrap(err, "cannot exec protoc with protoc-gen-go")
}
return nil
}
// CodeGeneratorRequest returns a protoc CodeGeneratorRequest from running
// protoc on protoPaths
func CodeGeneratorRequest(protoPaths []string) (*plugin.CodeGeneratorRequest, error) {
protocOut, err := getProtocOutput(protoPaths)
if err != nil {
return nil, errors.Wrap(err, "cannot get output from protoc")
}
req := new(plugin.CodeGeneratorRequest)
if err = proto.Unmarshal(protocOut, req); err != nil {
return nil, errors.Wrap(err, "cannot marshal protoc ouput to code generator request")
}
return req, nil
}
// ServiceFile returns the file in req that contains a service declaration.
func ServiceFile(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(filepath.Join(protoFileDir, svcFileName))
if err != nil {
return nil, errors.Wrapf(err, "cannot open service file: %v\n in path: %v",
protoFileDir, svcFileName)
}
return svc, nil
}
// getProtocOutput executes protoc with the passed protofiles and the
// protoc-gen-truss-protocast plugin and returns the output of protoc
func getProtocOutput(protoPaths []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-")
if err != nil {
return nil, errors.Wrap(err, "cannot create temp directory")
}
defer os.RemoveAll(protocOutDir)
err = outputGoogleImport(protocOutDir)
if err != nil {
return nil, errors.Wrapf(err, "cannot write protoc imports to dir: %s", protocOutDir)
}
pluginCall := filepath.Join("--truss-protocast_out=", protocOutDir)
err = protoc(protoPaths, protocOutDir, pluginCall)
if err != nil {
return nil, errors.Wrap(err, "protoc failed")
}
fileInfo, err := ioutil.ReadDir(protocOutDir)
if err != nil {
return nil, errors.Wrapf(err, "cannot read directory: %v", protocOutDir)
}
for _, f := range fileInfo {
if f.IsDir() {
continue
}
fPath := filepath.Join(protocOutDir, f.Name())
protocOut, err := ioutil.ReadFile(fPath)
if err != nil {
return nil, errors.Wrapf(err, "cannot read file: %v", fPath)
}
return protocOut, nil
}
return nil, errors.Errorf("no protoc output file found in: %v", protocOutDir)
}
// protoc executes protoc on protoPaths
func protoc(protoPaths []string, importDir, plugin string) error {
const googleAPIImportPath = "/third_party/googleapis"
cmdArgs := []string{
"-I" + filepath.Join(importDir, googleAPIImportPath),
"--proto_path=" + filepath.Dir(protoPaths[0]),
plugin,
}
// Append each definition file path to the end of that command args
cmdArgs = append(cmdArgs, protoPaths...)
protocExec := exec.Command(
"protoc",
cmdArgs...,
)
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
}
// outputGoogleImport places imported and required google.api.http protobuf
// option files
func outputGoogleImport(dir string) error {
// Output files that are stored in template package
for _, assetPath := range assets.AssetNames() {
fileBytes, _ := assets.Asset(assetPath)
fullPath := filepath.Join(dir, assetPath)
// Rename .gotemplate to .go
if strings.HasSuffix(fullPath, ".gotemplate") {
fullPath = strings.TrimSuffix(fullPath, "template")
}
err := os.MkdirAll(filepath.Dir(fullPath), 0777)
if err != nil {
return errors.Wrapf(err, "cannot create directory %v", filepath.Dir(fullPath))
}
err = ioutil.WriteFile(fullPath, fileBytes, 0666)
if err != nil {
return errors.Wrapf(err, "cannot create template file at path %v", fullPath)
}
}
return nil
}

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

@ -6,13 +6,12 @@ import (
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/TuneLab/go-truss/truss/protostage"
"github.com/TuneLab/go-truss/truss/execprotoc"
"github.com/TuneLab/go-truss/truss/truss"
"github.com/TuneLab/go-truss/deftree"
@ -20,131 +19,207 @@ import (
"github.com/TuneLab/go-truss/gengokit"
)
func main() {
var (
pbPackageFlag = flag.String("pbout", "", "The go package path where the protoc-gen-go .pb.go structs will be written.")
)
func init() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [OPTION]... [*.proto]...\n", filepath.Base(os.Args[0]))
flag.PrintDefaults()
}
flag.Parse()
if len(flag.Args()) == 0 {
exitIfError(errors.New("no arguments passed"))
fmt.Fprintf(os.Stderr, "%s: missing .proto file(s)\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, "Try '%s --help' for more information.\n", filepath.Base(os.Args[0]))
os.Exit(1)
}
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)
// Compose protocOut and service file to make a deftree
protocOut, serviceFile, err := protostage.Compose(definitionFiles, protoDir)
exitIfError(err)
// Make a deftree
dt, err := deftree.New(protocOut, serviceFile)
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
mkdir(protoDir + "/" + dt.GetName() + "-service/")
goImportPath := strings.TrimPrefix(protoDir, goPath+"/src/")
err = protostage.GeneratePBDataStructures(definitionFiles, protoDir, goImportPath, dt.GetName())
exitIfError(err)
prevGen, err := readPreviousGeneration(protoDir, dt.GetName())
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()
mkdir(name)
file, err := os.Create(name)
exitIfError(errors.Wrapf(err, "could create file %v", name))
_, err = io.Copy(file, f)
exitIfError(errors.Wrapf(err, "could not write to %v", name))
}
}
// 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()
func main() {
cfg, err := parseInput()
exitIfError(errors.Wrap(err, "cannot parse input"))
dt, err := parseServiceDefinition(cfg.DefPaths)
exitIfError(errors.Wrap(err, "cannot parse input definition proto files"))
err = updateConfigWithService(cfg, dt)
exitIfError(err)
genFiles, err := generateCode(cfg, dt)
exitIfError(errors.Wrap(err, "cannot generate service"))
for _, f := range genFiles {
err := writeGenFile(f, cfg.ServicePath())
if err != nil {
exitIfError(errors.Wrap(err, "cannot to write output"))
}
}
}
// parseInput constructs a *truss.Config with all values needed to parse
// service definition files.
func parseInput() (*truss.Config, error) {
var cfg truss.Config
// GOPATH
goPaths := filepath.SplitList(os.Getenv("GOPATH"))
if goPaths == nil {
return nil, errors.New("GOPATH not set")
}
cfg.GOPATH = goPaths[0]
// DefPaths
var err error
rawDefinitionPaths := flag.Args()
cfg.DefPaths, err = cleanProtofilePath(rawDefinitionPaths)
if err != nil {
return "", nil, errors.Wrap(err, "could not get working directoru of truss")
return nil, errors.Wrap(err, "cannot parse input arguments")
}
var workingDirectory string
// PBGoPackage
if *pbPackageFlag == "" {
return &cfg, nil
}
cfg.PBPackage = *pbPackageFlag
if !fileExists(
filepath.Join(cfg.GOPATH, "src", cfg.PBPackage)) {
return nil, errors.Errorf(".pb.go output package directory does not exist: %q", cfg.PBPackage)
}
return &cfg, nil
}
// parseServiceDefinition returns a deftree which contains all needed for all
// generating a truss service and documentation
func parseServiceDefinition(definitionPaths []string) (deftree.Deftree, error) {
protocOut, err := execprotoc.CodeGeneratorRequest(definitionPaths)
if err != nil {
return nil, errors.Wrap(err, "cannot use parse input files with protoc")
}
svcFile, err := execprotoc.ServiceFile(protocOut, filepath.Dir(definitionPaths[0]))
if err != nil {
return nil, errors.Wrap(err, "cannot find service definition file")
}
dt, err := deftree.New(protocOut, svcFile)
if err != nil {
return nil, errors.Wrap(err, "cannot to construct service definition")
}
return dt, nil
}
// updateConfigWithService updates the config with all information needed to
// generate a truss service using the parsedServiceDefinition deftree
func updateConfigWithService(cfg *truss.Config, dt deftree.Deftree) error {
var err error
// Service Path
svcName := dt.GetName() + "-service"
svcPath := filepath.Join(filepath.Dir(cfg.DefPaths[0]), svcName)
cfg.ServicePackage, err = filepath.Rel(filepath.Join(cfg.GOPATH, "src"), svcPath)
if err != nil {
return errors.Wrap(err, "service path is not in GOPATH")
}
// PrevGen
cfg.PrevGen, err = readPreviousGeneration(cfg.ServicePath())
if err != nil {
return errors.Wrap(err, "cannot read previously generated files")
}
// PBGoPath
if cfg.PBPackage == "" {
cfg.PBPackage = cfg.ServicePackage
}
return nil
}
// generateCode returns a []truss.NamedReadWriter that represents a gokit
// service with documentation
func generateCode(cfg *truss.Config, dt deftree.Deftree) ([]truss.NamedReadWriter, error) {
if cfg.PrevGen == nil {
err := os.Mkdir(cfg.ServicePath(), 0777)
if err != nil {
return nil, errors.Wrap(err, "cannot create service directory")
}
}
err := execprotoc.GeneratePBDotGo(cfg.DefPaths, cfg.ServicePath(), cfg.PBPath())
if err != nil {
return nil, errors.Wrap(err, "cannot create .pb.go files")
}
genGokitFiles, err := gengokit.GenerateGokit(dt, cfg.ServicePackage, cfg.PBPackage, cfg.PrevGen)
if err != nil {
return nil, errors.Wrap(err, "cannot generate gokit service")
}
genDocFiles := gendoc.GenerateDocs(dt)
genFiles := append(genGokitFiles, genDocFiles...)
return genFiles, nil
}
// writeGenFile writes a truss.NamedReadWriter to the filesystem
// to be contained within serviceDir
func writeGenFile(f truss.NamedReadWriter, serviceDir string) error {
// the serviceDir contains /NAME-service so we want to write to the
// directory above
outDir := filepath.Dir(serviceDir)
// i.e. NAME-service/generated/endpoint.go
name := f.Name()
fullPath := filepath.Join(outDir, name)
err := os.MkdirAll(filepath.Dir(fullPath), 0777)
if err != nil {
return err
}
file, err := os.Create(fullPath)
if err != nil {
return errors.Wrapf(err, "cannot create file %v", fullPath)
}
_, err = io.Copy(file, f)
if err != nil {
return errors.Wrapf(err, "cannot write to %v", fullPath)
}
return nil
}
// cleanProtofilePath returns the absolute filepath of a group of files
// of the files, or an error if the files are not in the same directory
func cleanProtofilePath(rawPaths []string) ([]string, error) {
var fullPaths []string
// Parsed passed file paths
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)
def = path.Join(execWd, def)
full, err := filepath.Abs(def)
if err != nil {
return nil, errors.Wrap(err, "cannot get working directory of truss")
}
// The working direcotry for this definition file
dir := path.Dir(def)
// Add the base name of definition file to the slice
definitionFiles = append(definitionFiles, path.Base(def))
fullPaths = append(fullPaths, full)
// If the working directory has not beenset before set it
if workingDirectory == "" {
workingDirectory = dir
} else {
// If the working directory for this definition file is different than the previous
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)
}
if filepath.Dir(fullPaths[0]) != filepath.Dir(full) {
return nil, errors.Errorf("passed .proto files in different directories")
}
}
return workingDirectory, definitionFiles, nil
}
// 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
return fullPaths, nil
}
// exitIfError will print the error message and exit 1 if the passed error is
// non-nil
func exitIfError(err error) {
if errors.Cause(err) != nil {
defer os.Exit(1)
@ -152,33 +227,32 @@ func exitIfError(err error) {
}
}
// readPreviousGeneration accepts the path to the directory where the inputed .proto files are stored, protoDir,
// it returns a []truss.NamedReadWriter for all files in the service/ dir in protoDir
func readPreviousGeneration(protoDir, packageName string) ([]truss.NamedReadWriter, error) {
dir := protoDir + "/" + packageName + "-service"
if fileExists(dir) != true {
// readPreviousGeneration returns a []truss.NamedReadWriter for all files serviceDir
func readPreviousGeneration(serviceDir string) ([]truss.NamedReadWriter, error) {
if fileExists(serviceDir) != true {
return nil, nil
}
var files []truss.NamedReadWriter
dir, _ := filepath.Split(serviceDir)
sfs := simpleFileConstructor{
protoDir: protoDir,
files: files,
dir: dir,
files: files,
}
err := filepath.Walk(dir, sfs.makeSimpleFile)
err := filepath.Walk(serviceDir, sfs.makeSimpleFile)
if err != nil {
return nil, errors.Wrapf(err, "could not fully walk directory %v", protoDir)
return nil, errors.Wrapf(err, "cannot fully walk directory %v", sfs.dir)
}
return sfs.files, nil
}
// simpleFileConstructor has the function makeSimpleFile which is of type filepath.WalkFunc
// simpleFileConstructor has function makeSimpleFile 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.NamedReadWriter
dir string
files []truss.NamedReadWriter
}
// makeSimpleFile is of type filepath.WalkFunc
@ -191,12 +265,11 @@ func (sfs *simpleFileConstructor) makeSimpleFile(path string, info os.FileInfo,
byteContent, ioErr := ioutil.ReadFile(path)
if ioErr != nil {
return errors.Wrapf(ioErr, "could not read file: %v", path)
return errors.Wrapf(ioErr, "cannot 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+"/")
name := strings.TrimPrefix(path, sfs.dir)
var file truss.SimpleFile
file.Path = name
file.Write(byteContent)

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

@ -1,240 +0,0 @@
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/go-truss/truss/template"
)
// Stage outputs the third_party imports that are required for protoc imports
// and go build.
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, packageName string) error {
pbDataStructureDir := "/" + packageName + "-service/"
genGoCode := "--go_out=Mgoogle/api/annotations.proto=" +
importPath +
"/third_party/googleapis/google/api," +
"plugins=grpc:" +
protoDir +
"/" + packageName + "-service"
_, 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 = "/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 outputs the directories of the third_party imports
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)
}
}
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
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -0,0 +1,35 @@
package truss
import (
"path/filepath"
)
// Config defines the inputs to a truss service generation
type Config struct {
// The first path in $GOPATH
GOPATH string
// The go packge where .pb.go files protoc-gen-go creates will be written
PBPackage string
// The go package where the service code will be written
ServicePackage string
// The paths to each of the .proto files truss is being run against
DefPaths []string
// The files of a previously generated service, may be nil
PrevGen []NamedReadWriter
}
// ServicePath returns the full path to Config.ServicePackage
func (c *Config) ServicePath() string {
goSvcPath := filepath.Join(c.GOPATH, "src", c.ServicePackage)
return goSvcPath
}
// PBPath returns the full paht to Config.PBPackage
func (c *Config) PBPath() string {
pbPath := filepath.Join(c.GOPATH, "src", c.PBPackage)
return pbPath
}

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

@ -8,11 +8,12 @@ import (
)
// NamedReadWriter represents 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
// os.file fulfills the NamedReadWriter interface
type NamedReadWriter interface {
io.ReadWriter
// Name() is a path relative to the directory containing the .proto
// files. Name() should start with "NAME-service/" for all files which have
// been generated and read in. Note that os.file fulfills the
// NamedReadWriter interface.
Name() string
}