Skip to content

Commit

Permalink
Merge pull request #23 from timburks/master
Browse files Browse the repository at this point in the history
Add Go code generator plugin
  • Loading branch information
timburks committed Feb 2, 2017
2 parents 68774c3 + aa6e028 commit 72ada8f
Show file tree
Hide file tree
Showing 31 changed files with 2,701 additions and 476 deletions.
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ install:
- go get github.com/googleapis/openapi-compiler/openapic
- go get github.com/googleapis/openapi-compiler/plugins/go/openapi_go_sample

script: go test . -v
script:
- go test . -v
- cd plugins/go/openapi_go_generator/examples/bookstore
- make test
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

build:
go install github.com/googleapis/openapi-compiler/openapic
go install github.com/googleapis/openapi-compiler/apps/report
go install github.com/googleapis/openapi-compiler/plugins/go/openapi_go_sample
go install github.com/googleapis/openapi-compiler/plugins/go/openapi_go_generator/encode_templates
go generate github.com/googleapis/openapi-compiler/plugins/go/openapi_go_generator
go install github.com/googleapis/openapi-compiler/plugins/go/openapi_go_generator
rm -f $(GOPATH)/bin/openapi_go_client $(GOPATH)/bin/openapi_go_server
ln -s $(GOPATH)/bin/openapi_go_generator $(GOPATH)/bin/openapi_go_client
ln -s $(GOPATH)/bin/openapi_go_generator $(GOPATH)/bin/openapi_go_server

136 changes: 102 additions & 34 deletions openapic/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/golang/protobuf/proto"
"github.com/googleapis/openapi-compiler/OpenAPIv2"
Expand All @@ -33,14 +35,51 @@ import (
)

type PluginCall struct {
Name string
Output string
Name string
Invocation string
}

func (pluginCall *PluginCall) perform(document *openapi_v2.Document, sourceName string) {
func (pluginCall *PluginCall) perform(document *openapi_v2.Document, sourceName string) error {
if pluginCall.Name != "" {
request := &plugins.PluginRequest{}
request.Parameter = ""
request := &plugins.Request{}

// Infer the name of the executable by adding the prefix.
executableName := "openapi_" + pluginCall.Name

// validate invocation string with regular expression
invocation := pluginCall.Invocation

//
// Plugin invocations must consist of
// zero or more comma-separated key=value pairs followed by a path.
// If pairs are present, a colon separates them from the path.
// Keys and values must be alphanumeric strings and may contain
// dashes, underscores, periods, or forward slashes.
// A path can contain any characters other than the separators ',', ':', and '='.
//
invocation_regex := regexp.MustCompile(`^([\w-_\/\.]+=[\w-_\/\.]+(,[\w-_\/\.]+=[\w-_\/\.]+)*:)?[^,:=]+$`)
if !invocation_regex.Match([]byte(pluginCall.Invocation)) {
return errors.New(fmt.Sprintf("Invalid invocation of %s: %s", executableName, invocation))
}

invocationParts := strings.Split(pluginCall.Invocation, ":")
var outputLocation string
switch len(invocationParts) {
case 1:
outputLocation = invocationParts[0]
case 2:
parameters := strings.Split(invocationParts[0], ",")
for _, keyvalue := range parameters {
pair := strings.Split(keyvalue, "=")
if len(pair) == 2 {
request.Parameters = append(request.Parameters, &plugins.Parameter{Name: pair[0], Value: pair[1]})
}
}
outputLocation = invocationParts[1]
default:
// badly-formed request
outputLocation = invocationParts[len(invocationParts)-1]
}

version := &plugins.Version{}
version.Major = 0
Expand All @@ -53,34 +92,55 @@ func (pluginCall *PluginCall) perform(document *openapi_v2.Document, sourceName
wrapper.Version = "v2"
protoBytes, _ := proto.Marshal(document)
wrapper.Value = protoBytes
request.Wrapper = []*plugins.Wrapper{wrapper}
request.Wrapper = wrapper
requestBytes, _ := proto.Marshal(request)

cmd := exec.Command("openapi_" + pluginCall.Name)
cmd := exec.Command(executableName)
cmd.Stdin = bytes.NewReader(requestBytes)
cmd.Stderr = os.Stderr
output, err := cmd.Output()
if err != nil {
fmt.Printf("Error: %+v\n", err)
return err
}
response := &plugins.PluginResponse{}
response := &plugins.Response{}
err = proto.Unmarshal(output, response)
if err != nil {
fmt.Printf("Error: %+v\n", err)
fmt.Printf("%s\n", string(output))
return err
}

// write files to the specified directory
var writer io.Writer
if pluginCall.Output == "-" {
if outputLocation == "!" {
// write nothing
} else if outputLocation == "-" {
writer = os.Stdout
for _, file := range response.Files {
writer.Write([]byte("\n\n" + file.Name + " -------------------- \n"))
writer.Write(file.Data)
}
} else if isFile(outputLocation) {
return errors.New(fmt.Sprintf("Error, unable to overwrite %s\n", outputLocation))
} else {
file, _ := os.Create(pluginCall.Output)
defer file.Close()
writer = file
}
for _, text := range response.Text {
writer.Write([]byte(text))
if !isDirectory(outputLocation) {
os.Mkdir(outputLocation, 0755)
}
for _, file := range response.Files {
path := outputLocation + "/" + file.Name
f, _ := os.Create(path)
defer f.Close()
f.Write(file.Data)
}
}
}
return nil
}

func isFile(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return !fileInfo.IsDir()
}

func isDirectory(path string) bool {
Expand All @@ -93,8 +153,12 @@ func isDirectory(path string) bool {

func writeFile(name string, bytes []byte, source string, extension string) {
var writer io.Writer
if name == "-" {
if name == "!" {
return
} else if name == "-" {
writer = os.Stdout
} else if name == "=" {
writer = os.Stderr
} else if isDirectory(name) {
base := filepath.Base(source)
// remove the original source extension
Expand All @@ -110,7 +174,7 @@ func writeFile(name string, bytes []byte, source string, extension string) {
writer = file
}
writer.Write(bytes)
if name == "-" {
if name == "-" || name == "=" {
writer.Write([]byte("\n"))
}
}
Expand All @@ -137,7 +201,7 @@ Options:
resolveReferences := false

// arg processing matches patterns of the form "--PLUGIN_out=PATH"
plugin_regex, err := regexp.Compile("--(.+)_out=(.+)")
plugin_regex := regexp.MustCompile("--(.+)_out=(.+)")

for i, arg := range os.Args {
if i == 0 {
Expand All @@ -146,24 +210,24 @@ Options:
var m [][]byte
if m = plugin_regex.FindSubmatch([]byte(arg)); m != nil {
pluginName := string(m[1])
outputName := string(m[2])
invocation := string(m[2])
switch pluginName {
case "pb":
binaryProtoPath = outputName
binaryProtoPath = invocation
case "json":
jsonProtoPath = outputName
jsonProtoPath = invocation
case "text":
textProtoPath = outputName
textProtoPath = invocation
case "errors":
errorPath = outputName
errorPath = invocation
default:
pluginCall := &PluginCall{Name: pluginName, Output: outputName}
pluginCall := &PluginCall{Name: pluginName, Invocation: invocation}
pluginCalls = append(pluginCalls, pluginCall)
}
} else if arg == "--resolve_refs" {
resolveReferences = true
} else if arg[0] == '-' {
fmt.Printf("Unknown option: %s.\n%s\n", arg, usage)
fmt.Fprintf(os.Stderr, "Unknown option: %s.\n%s\n", arg, usage)
os.Exit(-1)
} else {
sourceName = arg
Expand All @@ -175,24 +239,24 @@ Options:
textProtoPath == "" &&
errorPath == "" &&
len(pluginCalls) == 0 {
fmt.Printf("Missing output directives.\n%s\n", usage)
fmt.Fprintf(os.Stderr, "Missing output directives.\n%s\n", usage)
os.Exit(-1)
}

if sourceName == "" {
fmt.Printf("No input specified.\n%s\n", usage)
fmt.Fprintf(os.Stderr, "No input specified.\n%s\n", usage)
os.Exit(-1)
}

// If we get here and the error output is unspecified, write errors to stdout.
// If we get here and the error output is unspecified, write errors to stderr.
if errorPath == "" {
errorPath = "-"
errorPath = "="
}

// read and compile the OpenAPI source
info, err := compiler.ReadInfoForFile(sourceName)
if err != nil {
fmt.Printf("Error: %+v\n", err)
writeFile(errorPath, []byte(err.Error()), sourceName, "errors")
os.Exit(-1)
}
document, err := openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
Expand Down Expand Up @@ -227,6 +291,10 @@ Options:
writeFile(textProtoPath, bytes, sourceName, "text")
}
for _, pluginCall := range pluginCalls {
pluginCall.perform(document, sourceName)
err = pluginCall.perform(document, sourceName)
if err != nil {
writeFile(errorPath, []byte(err.Error()), sourceName, "errors")
defer os.Exit(-1)
}
}
}
57 changes: 57 additions & 0 deletions openapic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,60 @@ func TestSamplePluginWithPetstore(t *testing.T) {
"sample-petstore.out",
"test/sample-petstore.out")
}

func TestErrorInvalidPluginInvocations(t *testing.T) {
var err error
output, err := exec.Command(
"openapic",
"examples/petstore.yaml",
"--errors_out=-",
"--plugin_out=foo=bar,:abc",
"--plugin_out=,foo=bar:abc",
"--plugin_out=foo=:abc",
"--plugin_out==bar:abc",
"--plugin_out=,,:abc",
"--plugin_out=foo=bar=baz:abc",
).Output()
if err == nil {
t.Logf("Invalid invocations were accepted")
t.FailNow()
}
output_file := "invalid-plugin-invocation.errors"
_ = ioutil.WriteFile(output_file, output, 0644)
err = exec.Command("diff", output_file, "test/errors/invalid-plugin-invocation.errors").Run()
if err != nil {
t.Logf("Diff failed: %+v", err)
t.FailNow()
} else {
// if the test succeeded, clean up
os.Remove(output_file)
}
}

func TestValidPluginInvocations(t *testing.T) {
var err error
output, err := exec.Command(
"openapic",
"examples/petstore.yaml",
"--errors_out=-",
// verify an invocation with no parameters
"--go_sample_out=!", // "!" indicates that no output should be generated
// verify single pair of parameters
"--go_sample_out=a=b:!",
// verify multiple parameters
"--go_sample_out=a=b,c=123,xyz=alphabetagammadelta:!",
// verify that special characters / . - _ can be included in parameter keys and values
"--go_sample_out=a/b/c=x/y/z:!",
"--go_sample_out=a.b.c=x.y.z:!",
"--go_sample_out=a-b-c=x-y-z:!",
"--go_sample_out=a_b_c=x_y_z:!",
).Output()
if len(output) != 0 {
t.Logf("Valid invocations generated invalid errors\n%s", string(output))
t.FailNow()
}
if err != nil {
t.Logf("Valid invocations were not accepted")
t.FailNow()
}
}
10 changes: 10 additions & 0 deletions plugins/go/openapi_go_generator/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

build:
go install github.com/googleapis/openapi-compiler/openapic
go install github.com/googleapis/openapi-compiler/plugins/go/openapi_go_generator/encode_templates
go generate github.com/googleapis/openapi-compiler/plugins/go/openapi_go_generator
go install github.com/googleapis/openapi-compiler/plugins/go/openapi_go_generator
rm -f $(GOPATH)/bin/openapi_go_client $(GOPATH)/bin/openapi_go_server
ln -s $(GOPATH)/bin/openapi_go_generator $(GOPATH)/bin/openapi_go_client
ln -s $(GOPATH)/bin/openapi_go_generator $(GOPATH)/bin/openapi_go_server

Loading

0 comments on commit 72ada8f

Please sign in to comment.