| 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() |
| } |