gnostic.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. // Copyright 2017 Google Inc. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. //go:generate ./COMPILE-PROTOS.sh
  15. // Gnostic is a tool for building better REST APIs through knowledge.
  16. //
  17. // Gnostic reads declarative descriptions of REST APIs that conform
  18. // to the OpenAPI Specification, reports errors, resolves internal
  19. // dependencies, and puts the results in a binary form that can
  20. // be used in any language that is supported by the Protocol Buffer
  21. // tools.
  22. //
  23. // Gnostic models are validated and typed. This allows API tool
  24. // developers to focus on their product and not worry about input
  25. // validation and type checking.
  26. //
  27. // Gnostic calls plugins that implement a variety of API implementation
  28. // and support features including generation of client and server
  29. // support code.
  30. package main
  31. import (
  32. "bytes"
  33. "errors"
  34. "fmt"
  35. "io"
  36. "os"
  37. "os/exec"
  38. "path"
  39. "path/filepath"
  40. "regexp"
  41. "strings"
  42. "github.com/golang/protobuf/proto"
  43. "github.com/googleapis/gnostic/OpenAPIv2"
  44. "github.com/googleapis/gnostic/OpenAPIv3"
  45. "github.com/googleapis/gnostic/compiler"
  46. "github.com/googleapis/gnostic/jsonwriter"
  47. plugins "github.com/googleapis/gnostic/plugins"
  48. "gopkg.in/yaml.v2"
  49. )
  50. const ( // OpenAPI Version
  51. openAPIvUnknown = 0
  52. openAPIv2 = 2
  53. openAPIv3 = 3
  54. )
  55. // Determine the version of an OpenAPI description read from JSON or YAML.
  56. func getOpenAPIVersionFromInfo(info interface{}) int {
  57. m, ok := compiler.UnpackMap(info)
  58. if !ok {
  59. return openAPIvUnknown
  60. }
  61. swagger, ok := compiler.MapValueForKey(m, "swagger").(string)
  62. if ok && strings.HasPrefix(swagger, "2.0") {
  63. return openAPIv2
  64. }
  65. openapi, ok := compiler.MapValueForKey(m, "openapi").(string)
  66. if ok && strings.HasPrefix(openapi, "3.0") {
  67. return openAPIv3
  68. }
  69. return openAPIvUnknown
  70. }
  71. const (
  72. pluginPrefix = "gnostic-"
  73. extensionPrefix = "gnostic-x-"
  74. )
  75. type pluginCall struct {
  76. Name string
  77. Invocation string
  78. }
  79. // Invokes a plugin.
  80. func (p *pluginCall) perform(document proto.Message, openAPIVersion int, sourceName string) error {
  81. if p.Name != "" {
  82. request := &plugins.Request{}
  83. // Infer the name of the executable by adding the prefix.
  84. executableName := pluginPrefix + p.Name
  85. // Validate invocation string with regular expression.
  86. invocation := p.Invocation
  87. //
  88. // Plugin invocations must consist of
  89. // zero or more comma-separated key=value pairs followed by a path.
  90. // If pairs are present, a colon separates them from the path.
  91. // Keys and values must be alphanumeric strings and may contain
  92. // dashes, underscores, periods, or forward slashes.
  93. // A path can contain any characters other than the separators ',', ':', and '='.
  94. //
  95. invocationRegex := regexp.MustCompile(`^([\w-_\/\.]+=[\w-_\/\.]+(,[\w-_\/\.]+=[\w-_\/\.]+)*:)?[^,:=]+$`)
  96. if !invocationRegex.Match([]byte(p.Invocation)) {
  97. return fmt.Errorf("Invalid invocation of %s: %s", executableName, invocation)
  98. }
  99. invocationParts := strings.Split(p.Invocation, ":")
  100. var outputLocation string
  101. switch len(invocationParts) {
  102. case 1:
  103. outputLocation = invocationParts[0]
  104. case 2:
  105. parameters := strings.Split(invocationParts[0], ",")
  106. for _, keyvalue := range parameters {
  107. pair := strings.Split(keyvalue, "=")
  108. if len(pair) == 2 {
  109. request.Parameters = append(request.Parameters, &plugins.Parameter{Name: pair[0], Value: pair[1]})
  110. }
  111. }
  112. outputLocation = invocationParts[1]
  113. default:
  114. // badly-formed request
  115. outputLocation = invocationParts[len(invocationParts)-1]
  116. }
  117. version := &plugins.Version{}
  118. version.Major = 0
  119. version.Minor = 1
  120. version.Patch = 0
  121. request.CompilerVersion = version
  122. request.OutputPath = outputLocation
  123. wrapper := &plugins.Wrapper{}
  124. wrapper.Name = sourceName
  125. switch openAPIVersion {
  126. case openAPIv2:
  127. wrapper.Version = "v2"
  128. case openAPIv3:
  129. wrapper.Version = "v3"
  130. default:
  131. wrapper.Version = "unknown"
  132. }
  133. protoBytes, _ := proto.Marshal(document)
  134. wrapper.Value = protoBytes
  135. request.Wrapper = wrapper
  136. requestBytes, _ := proto.Marshal(request)
  137. cmd := exec.Command(executableName)
  138. cmd.Stdin = bytes.NewReader(requestBytes)
  139. cmd.Stderr = os.Stderr
  140. output, err := cmd.Output()
  141. if err != nil {
  142. return err
  143. }
  144. response := &plugins.Response{}
  145. err = proto.Unmarshal(output, response)
  146. if err != nil {
  147. return err
  148. }
  149. if response.Errors != nil {
  150. return fmt.Errorf("Plugin error: %+v", response.Errors)
  151. }
  152. // Write files to the specified directory.
  153. var writer io.Writer
  154. switch {
  155. case outputLocation == "!":
  156. // Write nothing.
  157. case outputLocation == "-":
  158. writer = os.Stdout
  159. for _, file := range response.Files {
  160. writer.Write([]byte("\n\n" + file.Name + " -------------------- \n"))
  161. writer.Write(file.Data)
  162. }
  163. case isFile(outputLocation):
  164. return fmt.Errorf("unable to overwrite %s", outputLocation)
  165. default: // write files into a directory named by outputLocation
  166. if !isDirectory(outputLocation) {
  167. os.Mkdir(outputLocation, 0755)
  168. }
  169. for _, file := range response.Files {
  170. p := outputLocation + "/" + file.Name
  171. dir := path.Dir(p)
  172. os.MkdirAll(dir, 0755)
  173. f, _ := os.Create(p)
  174. defer f.Close()
  175. f.Write(file.Data)
  176. }
  177. }
  178. }
  179. return nil
  180. }
  181. func isFile(path string) bool {
  182. fileInfo, err := os.Stat(path)
  183. if err != nil {
  184. return false
  185. }
  186. return !fileInfo.IsDir()
  187. }
  188. func isDirectory(path string) bool {
  189. fileInfo, err := os.Stat(path)
  190. if err != nil {
  191. return false
  192. }
  193. return fileInfo.IsDir()
  194. }
  195. // Write bytes to a named file.
  196. // Certain names have special meaning:
  197. // ! writes nothing
  198. // - writes to stdout
  199. // = writes to stderr
  200. // If a directory name is given, the file is written there with
  201. // a name derived from the source and extension arguments.
  202. func writeFile(name string, bytes []byte, source string, extension string) {
  203. var writer io.Writer
  204. if name == "!" {
  205. return
  206. } else if name == "-" {
  207. writer = os.Stdout
  208. } else if name == "=" {
  209. writer = os.Stderr
  210. } else if isDirectory(name) {
  211. base := filepath.Base(source)
  212. // Remove the original source extension.
  213. base = base[0 : len(base)-len(filepath.Ext(base))]
  214. // Build the path that puts the result in the passed-in directory.
  215. filename := name + "/" + base + "." + extension
  216. file, _ := os.Create(filename)
  217. defer file.Close()
  218. writer = file
  219. } else {
  220. file, _ := os.Create(name)
  221. defer file.Close()
  222. writer = file
  223. }
  224. writer.Write(bytes)
  225. if name == "-" || name == "=" {
  226. writer.Write([]byte("\n"))
  227. }
  228. }
  229. // The Gnostic structure holds global state information for gnostic.
  230. type Gnostic struct {
  231. usage string
  232. sourceName string
  233. binaryOutputPath string
  234. textOutputPath string
  235. yamlOutputPath string
  236. jsonOutputPath string
  237. errorOutputPath string
  238. resolveReferences bool
  239. pluginCalls []*pluginCall
  240. extensionHandlers []compiler.ExtensionHandler
  241. openAPIVersion int
  242. }
  243. // Initialize a structure to store global application state.
  244. func newGnostic() *Gnostic {
  245. g := &Gnostic{}
  246. // Option fields initialize to their default values.
  247. g.usage = `
  248. Usage: gnostic OPENAPI_SOURCE [OPTIONS]
  249. OPENAPI_SOURCE is the filename or URL of an OpenAPI description to read.
  250. Options:
  251. --pb-out=PATH Write a binary proto to the specified location.
  252. --text-out=PATH Write a text proto to the specified location.
  253. --json-out=PATH Write a json API description to the specified location.
  254. --yaml-out=PATH Write a yaml API description to the specified location.
  255. --errors-out=PATH Write compilation errors to the specified location.
  256. --PLUGIN-out=PATH Run the plugin named gnostic_PLUGIN and write results
  257. to the specified location.
  258. --x-EXTENSION Use the extension named gnostic-x-EXTENSION
  259. to process OpenAPI specification extensions.
  260. --resolve-refs Explicitly resolve $ref references.
  261. This could have problems with recursive definitions.
  262. `
  263. // Initialize internal structures.
  264. g.pluginCalls = make([]*pluginCall, 0)
  265. g.extensionHandlers = make([]compiler.ExtensionHandler, 0)
  266. return g
  267. }
  268. // Parse command-line options.
  269. func (g *Gnostic) readOptions() {
  270. // plugin processing matches patterns of the form "--PLUGIN-out=PATH" and "--PLUGIN_out=PATH"
  271. pluginRegex := regexp.MustCompile("--(.+)[-_]out=(.+)")
  272. // extension processing matches patterns of the form "--x-EXTENSION"
  273. extensionRegex := regexp.MustCompile("--x-(.+)")
  274. for i, arg := range os.Args {
  275. if i == 0 {
  276. continue // skip the tool name
  277. }
  278. var m [][]byte
  279. if m = pluginRegex.FindSubmatch([]byte(arg)); m != nil {
  280. pluginName := string(m[1])
  281. invocation := string(m[2])
  282. switch pluginName {
  283. case "pb":
  284. g.binaryOutputPath = invocation
  285. case "text":
  286. g.textOutputPath = invocation
  287. case "json":
  288. g.jsonOutputPath = invocation
  289. case "yaml":
  290. g.yamlOutputPath = invocation
  291. case "errors":
  292. g.errorOutputPath = invocation
  293. default:
  294. p := &pluginCall{Name: pluginName, Invocation: invocation}
  295. g.pluginCalls = append(g.pluginCalls, p)
  296. }
  297. } else if m = extensionRegex.FindSubmatch([]byte(arg)); m != nil {
  298. extensionName := string(m[1])
  299. extensionHandler := compiler.ExtensionHandler{Name: extensionPrefix + extensionName}
  300. g.extensionHandlers = append(g.extensionHandlers, extensionHandler)
  301. } else if arg == "--resolve-refs" {
  302. g.resolveReferences = true
  303. } else if arg[0] == '-' {
  304. fmt.Fprintf(os.Stderr, "Unknown option: %s.\n%s\n", arg, g.usage)
  305. os.Exit(-1)
  306. } else {
  307. g.sourceName = arg
  308. }
  309. }
  310. }
  311. // Validate command-line options.
  312. func (g *Gnostic) validateOptions() {
  313. if g.binaryOutputPath == "" &&
  314. g.textOutputPath == "" &&
  315. g.yamlOutputPath == "" &&
  316. g.jsonOutputPath == "" &&
  317. g.errorOutputPath == "" &&
  318. len(g.pluginCalls) == 0 {
  319. fmt.Fprintf(os.Stderr, "Missing output directives.\n%s\n", g.usage)
  320. os.Exit(-1)
  321. }
  322. if g.sourceName == "" {
  323. fmt.Fprintf(os.Stderr, "No input specified.\n%s\n", g.usage)
  324. os.Exit(-1)
  325. }
  326. // If we get here and the error output is unspecified, write errors to stderr.
  327. if g.errorOutputPath == "" {
  328. g.errorOutputPath = "="
  329. }
  330. }
  331. // Generate an error message to be written to stderr or a file.
  332. func (g *Gnostic) errorBytes(err error) []byte {
  333. return []byte("Errors reading " + g.sourceName + "\n" + err.Error())
  334. }
  335. // Read an OpenAPI description from YAML or JSON.
  336. func (g *Gnostic) readOpenAPIText(bytes []byte) (message proto.Message, err error) {
  337. info, err := compiler.ReadInfoFromBytes(g.sourceName, bytes)
  338. if err != nil {
  339. return nil, err
  340. }
  341. // Determine the OpenAPI version.
  342. g.openAPIVersion = getOpenAPIVersionFromInfo(info)
  343. if g.openAPIVersion == openAPIvUnknown {
  344. return nil, errors.New("unable to identify OpenAPI version")
  345. }
  346. // Compile to the proto model.
  347. if g.openAPIVersion == openAPIv2 {
  348. document, err := openapi_v2.NewDocument(info, compiler.NewContextWithExtensions("$root", nil, &g.extensionHandlers))
  349. if err != nil {
  350. return nil, err
  351. }
  352. message = document
  353. } else if g.openAPIVersion == openAPIv3 {
  354. document, err := openapi_v3.NewDocument(info, compiler.NewContextWithExtensions("$root", nil, &g.extensionHandlers))
  355. if err != nil {
  356. return nil, err
  357. }
  358. message = document
  359. }
  360. return message, err
  361. }
  362. // Read an OpenAPI binary file.
  363. func (g *Gnostic) readOpenAPIBinary(data []byte) (message proto.Message, err error) {
  364. // try to read an OpenAPI v3 document
  365. documentV3 := &openapi_v3.Document{}
  366. err = proto.Unmarshal(data, documentV3)
  367. if err == nil && strings.HasPrefix(documentV3.Openapi, "3.0") {
  368. g.openAPIVersion = openAPIv3
  369. return documentV3, nil
  370. }
  371. // if that failed, try to read an OpenAPI v2 document
  372. documentV2 := &openapi_v2.Document{}
  373. err = proto.Unmarshal(data, documentV2)
  374. if err == nil && strings.HasPrefix(documentV2.Swagger, "2.0") {
  375. g.openAPIVersion = openAPIv2
  376. return documentV2, nil
  377. }
  378. return nil, err
  379. }
  380. // Write a binary pb representation.
  381. func (g *Gnostic) writeBinaryOutput(message proto.Message) {
  382. protoBytes, err := proto.Marshal(message)
  383. if err != nil {
  384. writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
  385. defer os.Exit(-1)
  386. } else {
  387. writeFile(g.binaryOutputPath, protoBytes, g.sourceName, "pb")
  388. }
  389. }
  390. // Write a text pb representation.
  391. func (g *Gnostic) writeTextOutput(message proto.Message) {
  392. bytes := []byte(proto.MarshalTextString(message))
  393. writeFile(g.textOutputPath, bytes, g.sourceName, "text")
  394. }
  395. // Write JSON/YAML OpenAPI representations.
  396. func (g *Gnostic) writeJSONYAMLOutput(message proto.Message) {
  397. // Convert the OpenAPI document into an exportable MapSlice.
  398. var rawInfo yaml.MapSlice
  399. var ok bool
  400. var err error
  401. if g.openAPIVersion == openAPIv2 {
  402. document := message.(*openapi_v2.Document)
  403. rawInfo, ok = document.ToRawInfo().(yaml.MapSlice)
  404. if !ok {
  405. rawInfo = nil
  406. }
  407. } else if g.openAPIVersion == openAPIv3 {
  408. document := message.(*openapi_v3.Document)
  409. rawInfo, ok = document.ToRawInfo().(yaml.MapSlice)
  410. if !ok {
  411. rawInfo = nil
  412. }
  413. }
  414. // Optionally write description in yaml format.
  415. if g.yamlOutputPath != "" {
  416. var bytes []byte
  417. if rawInfo != nil {
  418. bytes, err = yaml.Marshal(rawInfo)
  419. if err != nil {
  420. fmt.Fprintf(os.Stderr, "Error generating yaml output %s\n", err.Error())
  421. }
  422. writeFile(g.yamlOutputPath, bytes, g.sourceName, "yaml")
  423. } else {
  424. fmt.Fprintf(os.Stderr, "No yaml output available.\n")
  425. }
  426. }
  427. // Optionally write description in json format.
  428. if g.jsonOutputPath != "" {
  429. var bytes []byte
  430. if rawInfo != nil {
  431. bytes, _ = jsonwriter.Marshal(rawInfo)
  432. if err != nil {
  433. fmt.Fprintf(os.Stderr, "Error generating json output %s\n", err.Error())
  434. }
  435. writeFile(g.jsonOutputPath, bytes, g.sourceName, "json")
  436. } else {
  437. fmt.Fprintf(os.Stderr, "No json output available.\n")
  438. }
  439. }
  440. }
  441. // Perform all actions specified in the command-line options.
  442. func (g *Gnostic) performActions(message proto.Message) (err error) {
  443. // Optionally resolve internal references.
  444. if g.resolveReferences {
  445. if g.openAPIVersion == openAPIv2 {
  446. document := message.(*openapi_v2.Document)
  447. _, err = document.ResolveReferences(g.sourceName)
  448. } else if g.openAPIVersion == openAPIv3 {
  449. document := message.(*openapi_v3.Document)
  450. _, err = document.ResolveReferences(g.sourceName)
  451. }
  452. if err != nil {
  453. return err
  454. }
  455. }
  456. // Optionally write proto in binary format.
  457. if g.binaryOutputPath != "" {
  458. g.writeBinaryOutput(message)
  459. }
  460. // Optionally write proto in text format.
  461. if g.textOutputPath != "" {
  462. g.writeTextOutput(message)
  463. }
  464. // Optionaly write document in yaml and/or json formats.
  465. if g.yamlOutputPath != "" || g.jsonOutputPath != "" {
  466. g.writeJSONYAMLOutput(message)
  467. }
  468. // Call all specified plugins.
  469. for _, p := range g.pluginCalls {
  470. err := p.perform(message, g.openAPIVersion, g.sourceName)
  471. if err != nil {
  472. writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
  473. defer os.Exit(-1) // run all plugins, even when some have errors
  474. }
  475. }
  476. return nil
  477. }
  478. func (g *Gnostic) main() {
  479. var err error
  480. g.readOptions()
  481. g.validateOptions()
  482. // Read the OpenAPI source.
  483. bytes, err := compiler.ReadBytesForFile(g.sourceName)
  484. if err != nil {
  485. writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
  486. os.Exit(-1)
  487. }
  488. extension := strings.ToLower(filepath.Ext(g.sourceName))
  489. var message proto.Message
  490. if extension == ".json" || extension == ".yaml" {
  491. // Try to read the source as JSON/YAML.
  492. message, err = g.readOpenAPIText(bytes)
  493. if err != nil {
  494. writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
  495. os.Exit(-1)
  496. }
  497. } else if extension == ".pb" {
  498. // Try to read the source as a binary protocol buffer.
  499. message, err = g.readOpenAPIBinary(bytes)
  500. if err != nil {
  501. writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
  502. os.Exit(-1)
  503. }
  504. } else {
  505. err = errors.New("unknown file extension. 'json', 'yaml', and 'pb' are accepted")
  506. writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
  507. os.Exit(-1)
  508. }
  509. // Perform actions specified by command options.
  510. err = g.performActions(message)
  511. if err != nil {
  512. writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
  513. os.Exit(-1)
  514. }
  515. }
  516. func main() {
  517. g := newGnostic()
  518. g.main()
  519. }