blob: 79a0867f2f871d14c483737386475e003348504b [file] [log] [blame]
package gnostic_plugin_v1
import (
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes/any"
openapiv2 "github.com/googleapis/gnostic/OpenAPIv2"
openapiv3 "github.com/googleapis/gnostic/OpenAPIv3"
discovery "github.com/googleapis/gnostic/discovery"
surface "github.com/googleapis/gnostic/surface"
)
// Environment contains the environment of a plugin call.
type Environment struct {
Request *Request // plugin request object
Response *Response // response message
Invocation string // string representation of call
RunningAsPlugin bool // true if app is being run as a plugin
}
// NewEnvironment creates a plugin context from arguments and standard input.
func NewEnvironment() (env *Environment, err error) {
env = &Environment{
Invocation: os.Args[0],
Response: &Response{},
}
input := flag.String("input", "", "API description (in binary protocol buffer form)")
output := flag.String("output", "-", "Output file or directory")
plugin := flag.Bool("plugin", false, "Run as a gnostic plugin (other flags are ignored).")
flag.Parse()
env.RunningAsPlugin = *plugin
programName := path.Base(os.Args[0])
if (*input == "") && !*plugin {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, programName+" is a gnostic plugin.\n")
fmt.Fprintf(os.Stderr, `
When it is run from gnostic, the -plugin option is specified and gnostic
writes a binary request to stdin and waits for a binary response on stdout.
This program can also be run standalone using the other flags listed below.
When the -plugin option is specified, these flags are ignored.`)
fmt.Fprintf(os.Stderr, "\n\nUsage:\n")
flag.PrintDefaults()
}
flag.Usage()
os.Exit(0)
}
if env.RunningAsPlugin {
// Handle invocation as a plugin.
// Read the plugin input.
pluginData, err := ioutil.ReadAll(os.Stdin)
env.RespondAndExitIfError(err)
if len(pluginData) == 0 {
env.RespondAndExitIfError(fmt.Errorf("no input data"))
}
// Deserialize the request from the input.
request := &Request{}
err = proto.Unmarshal(pluginData, request)
env.RespondAndExitIfError(err)
// Collect parameters passed to the plugin.
parameters := request.Parameters
for _, parameter := range parameters {
env.Invocation += " " + parameter.Name + "=" + parameter.Value
}
// Log the invocation.
//log.Printf("Running plugin %s", env.Invocation)
env.Request = request
} else {
// Handle invocation from the command line.
// Read the input document.
apiData, err := ioutil.ReadFile(*input)
if len(apiData) == 0 {
env.RespondAndExitIfError(fmt.Errorf("no input data"))
}
env.Request = &Request{}
env.Request.OutputPath = *output
env.Request.SourceName = path.Base(*input)
// First try to unmarshal OpenAPI v2.
documentv2 := &openapiv2.Document{}
err = proto.Unmarshal(apiData, documentv2)
if err == nil {
env.Request.AddModel("openapi.v2.Document", documentv2)
// include experimental API surface model
surfaceModel, err := surface.NewModelFromOpenAPI2(documentv2)
if err != nil {
env.Request.AddModel("surface.v1.Model", surfaceModel)
}
return env, err
}
// If that failed, ignore deserialization errors and try to unmarshal OpenAPI v3.
documentv3 := &openapiv3.Document{}
err = proto.Unmarshal(apiData, documentv3)
if err == nil {
env.Request.AddModel("openapi.v3.Document", documentv3)
// include experimental API surface model
surfaceModel, err := surface.NewModelFromOpenAPI3(documentv3)
if err != nil {
env.Request.AddModel("surface.v1.Model", surfaceModel)
}
return env, err
}
// If that failed, ignore deserialization errors and try to unmarshal a Discovery document.
discoveryDocument := &discovery.Document{}
err = proto.Unmarshal(apiData, discoveryDocument)
if err == nil {
env.Request.AddModel("discovery.v1.Document", discoveryDocument)
return env, err
}
// If we get here, we don't know what we got
err = errors.New("Unrecognized format for input")
return env, err
}
return env, err
}
// RespondAndExitIfError checks an error and if it is non-nil, records it and serializes and returns the response and then exits.
func (env *Environment) RespondAndExitIfError(err error) {
if err != nil {
env.Response.Errors = append(env.Response.Errors, err.Error())
env.RespondAndExit()
}
}
// RespondAndExit serializes and returns the plugin response and then exits.
func (env *Environment) RespondAndExit() {
if env.RunningAsPlugin {
responseBytes, _ := proto.Marshal(env.Response)
os.Stdout.Write(responseBytes)
} else {
err := HandleResponse(env.Response, env.Request.OutputPath)
if err != nil {
log.Printf("%s", err.Error())
}
}
os.Exit(0)
}
func HandleResponse(response *Response, outputLocation string) error {
if response.Errors != nil {
return fmt.Errorf("Plugin error: %+v", response.Errors)
}
// Write files to the specified directory.
var writer io.Writer
switch {
case outputLocation == "!":
// Write nothing.
case outputLocation == "-":
writer = os.Stdout
for _, file := range response.Files {
writer.Write([]byte("\n\n" + file.Name + " -------------------- \n"))
writer.Write(file.Data)
}
case isFile(outputLocation):
return fmt.Errorf("unable to overwrite %s", outputLocation)
default: // write files into a directory named by outputLocation
if !isDirectory(outputLocation) {
os.Mkdir(outputLocation, 0755)
}
for _, file := range response.Files {
p := outputLocation + "/" + file.Name
dir := path.Dir(p)
os.MkdirAll(dir, 0755)
f, _ := os.Create(p)
defer f.Close()
f.Write(file.Data)
}
}
return nil
}
func (request *Request) AddModel(modelType string, model proto.Message) error {
modelBytes, err := proto.Marshal(model)
request.Models = append(request.Models, &any.Any{TypeUrl: modelType, Value: modelBytes})
return err
}
func isFile(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return !fileInfo.IsDir()
}
func isDirectory(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return fileInfo.IsDir()
}