prometheus-adapter/vendor/github.com/googleapis/gnostic/gnostic.go
2017-06-26 19:23:05 +03:00

393 lines
11 KiB
Go

// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:generate ./COMPILE-PROTOS.sh
// Gnostic is a tool for building better REST APIs through knowledge.
//
// Gnostic reads declarative descriptions of REST APIs that conform
// to the OpenAPI Specification, reports errors, resolves internal
// dependencies, and puts the results in a binary form that can
// be used in any language that is supported by the Protocol Buffer
// tools.
//
// Gnostic models are validated and typed. This allows API tool
// developers to focus on their product and not worry about input
// validation and type checking.
//
// Gnostic calls plugins that implement a variety of API implementation
// and support features including generation of client and server
// support code.
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/golang/protobuf/proto"
"github.com/googleapis/gnostic/OpenAPIv2"
"github.com/googleapis/gnostic/OpenAPIv3"
"github.com/googleapis/gnostic/compiler"
plugins "github.com/googleapis/gnostic/plugins"
)
const ( // OpenAPI Version
OpenAPIvUnknown = 0
OpenAPIv2 = 2
OpenAPIv3 = 3
)
func openapi_version(info interface{}) int {
m, ok := compiler.UnpackMap(info)
if !ok {
return OpenAPIvUnknown
}
swagger, ok := compiler.MapValueForKey(m, "swagger").(string)
if ok && swagger == "2.0" {
return OpenAPIv2
}
openapi, ok := compiler.MapValueForKey(m, "openapi").(string)
if ok && openapi == "3.0" {
return OpenAPIv3
}
return OpenAPIvUnknown
}
type PluginCall struct {
Name string
Invocation string
}
func (pluginCall *PluginCall) perform(document proto.Message, openapi_version int, sourceName string) error {
if pluginCall.Name != "" {
request := &plugins.Request{}
// Infer the name of the executable by adding the prefix.
executableName := "gnostic_" + 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
version.Minor = 1
version.Patch = 0
request.CompilerVersion = version
request.OutputPath = outputLocation
wrapper := &plugins.Wrapper{}
wrapper.Name = sourceName
switch openapi_version {
case OpenAPIv2:
wrapper.Version = "v2"
case OpenAPIv3:
wrapper.Version = "v3"
default:
wrapper.Version = "unknown"
}
protoBytes, _ := proto.Marshal(document)
wrapper.Value = protoBytes
request.Wrapper = wrapper
requestBytes, _ := proto.Marshal(request)
cmd := exec.Command(executableName)
cmd.Stdin = bytes.NewReader(requestBytes)
cmd.Stderr = os.Stderr
output, err := cmd.Output()
if err != nil {
return err
}
response := &plugins.Response{}
err = proto.Unmarshal(output, response)
if err != nil {
return err
}
if response.Errors != nil {
return errors.New(fmt.Sprintf("Plugin error: %+v", response.Errors))
}
// write files to the specified directory
var writer io.Writer
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 {
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 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()
}
func writeFile(name string, bytes []byte, source string, extension string) {
var writer io.Writer
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
base = base[0 : len(base)-len(filepath.Ext(base))]
// build the path that puts the result in the passed-in directory
filename := name + "/" + base + "." + extension
file, _ := os.Create(filename)
defer file.Close()
writer = file
} else {
file, _ := os.Create(name)
defer file.Close()
writer = file
}
writer.Write(bytes)
if name == "-" || name == "=" {
writer.Write([]byte("\n"))
}
}
func main() {
usage := `
Usage: gnostic OPENAPI_SOURCE [OPTIONS]
OPENAPI_SOURCE is the filename or URL of an OpenAPI description to read.
Options:
--pb_out=PATH Write a binary proto to the specified location.
--json_out=PATH Write a json proto to the specified location.
--text_out=PATH Write a text proto to the specified location.
--errors_out=PATH Write compilation errors to the specified location.
--PLUGIN_out=PATH Run the plugin named gnostic_PLUGIN and write results
to the specified location.
--resolve_refs Explicitly resolve $ref references.
This could have problems with recursive definitions.
--extension=NAME Run the specified gnostic extension to process
extensions found in OpenAPI descriptions.
`
// default values for all options
sourceName := ""
binaryProtoPath := ""
jsonProtoPath := ""
textProtoPath := ""
errorPath := ""
pluginCalls := make([]*PluginCall, 0)
resolveReferences := false
extensionHandlers := make([]compiler.ExtensionHandler, 0)
// arg processing matches patterns of the form "--PLUGIN_out=PATH"
plugin_regex := regexp.MustCompile("--(.+)_out=(.+)")
// arg processing matches patterns of the form "--extension=GENERATOR_NAME"
extensionHandler_regex, err := regexp.Compile("--extension=(.+)")
defaultPrefixForExtensions := "openapi_extensions_"
for i, arg := range os.Args {
if i == 0 {
continue // skip the tool name
}
var m [][]byte
if m = plugin_regex.FindSubmatch([]byte(arg)); m != nil {
pluginName := string(m[1])
invocation := string(m[2])
switch pluginName {
case "pb":
binaryProtoPath = invocation
case "json":
jsonProtoPath = invocation
case "text":
textProtoPath = invocation
case "errors":
errorPath = invocation
default:
pluginCall := &PluginCall{Name: pluginName, Invocation: invocation}
pluginCalls = append(pluginCalls, pluginCall)
}
} else if m = extensionHandler_regex.FindSubmatch([]byte(arg)); m != nil {
extensionHandlers = append(extensionHandlers, compiler.ExtensionHandler{Name: defaultPrefixForExtensions + string(m[1])})
} else if arg == "--resolve_refs" {
resolveReferences = true
} else if arg[0] == '-' {
fmt.Fprintf(os.Stderr, "Unknown option: %s.\n%s\n", arg, usage)
os.Exit(-1)
} else {
sourceName = arg
}
}
if binaryProtoPath == "" &&
jsonProtoPath == "" &&
textProtoPath == "" &&
errorPath == "" &&
len(pluginCalls) == 0 {
fmt.Fprintf(os.Stderr, "Missing output directives.\n%s\n", usage)
os.Exit(-1)
}
if sourceName == "" {
fmt.Fprintf(os.Stderr, "No input specified.\n%s\n", usage)
os.Exit(-1)
}
errorPrefix := "Errors reading " + sourceName + "\n"
// If we get here and the error output is unspecified, write errors to stderr.
if errorPath == "" {
errorPath = "="
}
// Read the OpenAPI source.
info, err := compiler.ReadInfoForFile(sourceName)
if err != nil {
writeFile(errorPath, []byte(errorPrefix+err.Error()), sourceName, "errors")
os.Exit(-1)
}
// Determine the OpenAPI version.
openapi_version := openapi_version(info)
if openapi_version == OpenAPIvUnknown {
fmt.Fprintf(os.Stderr, "Unknown OpenAPI Version\n")
os.Exit(-1)
}
var message proto.Message
if openapi_version == OpenAPIv2 {
document, err := openapi_v2.NewDocument(info, compiler.NewContextWithExtensions("$root", nil, &extensionHandlers))
if err != nil {
writeFile(errorPath, []byte(errorPrefix+err.Error()), sourceName, "errors")
os.Exit(-1)
}
// optionally resolve internal references
if resolveReferences {
_, err = document.ResolveReferences(sourceName)
if err != nil {
writeFile(errorPath, []byte(errorPrefix+err.Error()), sourceName, "errors")
os.Exit(-1)
}
}
message = document
} else if openapi_version == OpenAPIv3 {
document, err := openapi_v3.NewDocument(info, compiler.NewContextWithExtensions("$root", nil, &extensionHandlers))
if err != nil {
writeFile(errorPath, []byte(errorPrefix+err.Error()), sourceName, "errors")
os.Exit(-1)
}
// optionally resolve internal references
if resolveReferences {
_, err = document.ResolveReferences(sourceName)
if err != nil {
writeFile(errorPath, []byte(errorPrefix+err.Error()), sourceName, "errors")
os.Exit(-1)
}
}
message = document
}
// perform all specified actions
if binaryProtoPath != "" {
// write proto in binary format
protoBytes, _ := proto.Marshal(message)
writeFile(binaryProtoPath, protoBytes, sourceName, "pb")
}
if jsonProtoPath != "" {
// write proto in json format
jsonBytes, _ := json.Marshal(message)
writeFile(jsonProtoPath, jsonBytes, sourceName, "json")
}
if textProtoPath != "" {
// write proto in text format
bytes := []byte(proto.MarshalTextString(message))
writeFile(textProtoPath, bytes, sourceName, "text")
}
for _, pluginCall := range pluginCalls {
err = pluginCall.perform(message, openapi_version, sourceName)
if err != nil {
writeFile(errorPath, []byte(errorPrefix+err.Error()), sourceName, "errors")
defer os.Exit(-1)
}
}
}