From 42aec32c2aea3caa57419d923d456f01f1e51edf Mon Sep 17 00:00:00 2001 From: Tim Burks Date: Fri, 23 Oct 2020 16:28:20 -0700 Subject: [PATCH] protoc-gen-openapi (#221) * First steps toward a protoc plugin for generating OpenAPI models. Based on the Go code generation plugin (protoc-gen-go) and the gnostic petstore-builder sample app. * protoc-gen-openapi operation stubs * updated, fixed build problems * preliminary structural improvements to put everything in a single spec. * handle more field and array types + patch requests * work-in-progress, focusing on generating OpenAPI v3 from protoc-gen-openapi * protoc-gen-openapi only generates v3, now includes parameters * protoc-gen-openapi: get API name and description from protos. * protoc-gen-openapi: only export schemas that are used * protoc-gen-openapi: include message and field comments in openapi * protoc-gen-openapi: mark output-only fields * protoc-gen-openapi: general cleanup * protoc-gen-openapi: add test * protoc-gen-openapi: update to use the v2 protobuf-go plugin API * protoc-gen-openapi: remove unneeded parameters from buildOperationV3 * protoc-gen-openapi: remove blank lines --- apps/protoc-gen-openapi/README.md | 20 + .../examples/google/api/annotations.proto | 31 + .../examples/google/api/client.proto | 101 ++++ .../examples/google/api/field_behavior.proto | 80 +++ .../examples/google/api/http.proto | 318 ++++++++++ .../examples/google/api/resource.proto | 299 +++++++++ .../google/example/library/v1/library.proto | 339 +++++++++++ .../google/example/library/v1/openapi.yaml | 337 +++++++++++ .../generator/openapi-v3.go | 567 ++++++++++++++++++ apps/protoc-gen-openapi/main.go | 27 + apps/protoc-gen-openapi/plugin_test.go | 42 ++ discovery/discovery.pb.go | 2 +- extensions/extension.pb.go | 2 +- go.mod | 7 +- go.sum | 57 ++ metrics/complexity.pb.go | 2 +- metrics/vocabulary.pb.go | 2 +- openapiv2/OpenAPIv2.pb.go | 2 +- openapiv2/document.go | 16 +- openapiv3/OpenAPIv3.pb.go | 2 +- openapiv3/document.go | 16 +- plugins/plugin.pb.go | 2 +- surface/surface.pb.go | 2 +- 23 files changed, 2261 insertions(+), 12 deletions(-) create mode 100644 apps/protoc-gen-openapi/README.md create mode 100644 apps/protoc-gen-openapi/examples/google/api/annotations.proto create mode 100644 apps/protoc-gen-openapi/examples/google/api/client.proto create mode 100644 apps/protoc-gen-openapi/examples/google/api/field_behavior.proto create mode 100644 apps/protoc-gen-openapi/examples/google/api/http.proto create mode 100644 apps/protoc-gen-openapi/examples/google/api/resource.proto create mode 100644 apps/protoc-gen-openapi/examples/google/example/library/v1/library.proto create mode 100644 apps/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml create mode 100644 apps/protoc-gen-openapi/generator/openapi-v3.go create mode 100644 apps/protoc-gen-openapi/main.go create mode 100644 apps/protoc-gen-openapi/plugin_test.go diff --git a/apps/protoc-gen-openapi/README.md b/apps/protoc-gen-openapi/README.md new file mode 100644 index 00000000..9ac922bb --- /dev/null +++ b/apps/protoc-gen-openapi/README.md @@ -0,0 +1,20 @@ +# protoc-gen-openapi + +This directory contains a protoc plugin that generates an +OpenAPI description for a REST API that corresponds to a +Protocol Buffer service. + +Installation: + + go get github.com/googleapis/gnostic + go install github.com/googleapis/gnostic/apps/protoc-gen-openapi + + +Usage: + + protoc sample.proto -I. --openapi_out=. + +This runs the plugin for a file named `sample.proto` which +refers to additional .proto files in the same directory as +`sample.proto`. Output is written to the current directory. + diff --git a/apps/protoc-gen-openapi/examples/google/api/annotations.proto b/apps/protoc-gen-openapi/examples/google/api/annotations.proto new file mode 100644 index 00000000..85c361b4 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/apps/protoc-gen-openapi/examples/google/api/client.proto b/apps/protoc-gen-openapi/examples/google/api/client.proto new file mode 100644 index 00000000..c6846e28 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/google/api/client.proto @@ -0,0 +1,101 @@ +// Copyright 2019 Google LLC. +// +// 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 +// +// https://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. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "ClientProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + + +extend google.protobuf.ServiceOptions { + // The hostname for this service. + // This should be specified with no prefix or protocol. + // + // Example: + // + // service Foo { + // option (google.api.default_host) = "foo.googleapi.com"; + // ... + // } + string default_host = 1049; + + // OAuth scopes needed for the client. + // + // Example: + // + // service Foo { + // option (google.api.oauth_scopes) = \ + // "https://www.googleapis.com/auth/cloud-platform"; + // ... + // } + // + // If there is more than one scope, use a comma-separated string: + // + // Example: + // + // service Foo { + // option (google.api.oauth_scopes) = \ + // "https://www.googleapis.com/auth/cloud-platform," + // "https://www.googleapis.com/auth/monitoring"; + // ... + // } + string oauth_scopes = 1050; +} + + +extend google.protobuf.MethodOptions { + // A definition of a client library method signature. + // + // In client libraries, each proto RPC corresponds to one or more methods + // which the end user is able to call, and calls the underlying RPC. + // Normally, this method receives a single argument (a struct or instance + // corresponding to the RPC request object). Defining this field will + // add one or more overloads providing flattened or simpler method signatures + // in some languages. + // + // The fields on the method signature are provided as a comma-separated + // string. + // + // For example, the proto RPC and annotation: + // + // rpc CreateSubscription(CreateSubscriptionRequest) + // returns (Subscription) { + // option (google.api.method_signature) = "name,topic"; + // } + // + // Would add the following Java overload (in addition to the method accepting + // the request object): + // + // public final Subscription createSubscription(String name, String topic) + // + // The following backwards-compatibility guidelines apply: + // + // * Adding this annotation to an unannotated method is backwards + // compatible. + // * Adding this annotation to a method which already has existing + // method signature annotations is backwards compatible if and only if + // the new method signature annotation is last in the sequence. + // * Modifying or removing an existing method signature annotation is + // a breaking change. + // * Re-ordering existing method signature annotations is a breaking + // change. + repeated string method_signature = 1051; +} diff --git a/apps/protoc-gen-openapi/examples/google/api/field_behavior.proto b/apps/protoc-gen-openapi/examples/google/api/field_behavior.proto new file mode 100644 index 00000000..78838496 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/google/api/field_behavior.proto @@ -0,0 +1,80 @@ +// Copyright 2019 Google LLC. +// +// 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 +// +// https://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. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; +} + + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated FieldBehavior field_behavior = 1052; +} diff --git a/apps/protoc-gen-openapi/examples/google/api/http.proto b/apps/protoc-gen-openapi/examples/google/api/http.proto new file mode 100644 index 00000000..2bd3a19b --- /dev/null +++ b/apps/protoc-gen-openapi/examples/google/api/http.proto @@ -0,0 +1,318 @@ +// Copyright 2018 Google LLC +// +// 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. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parmeters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// `HttpRule` defines the mapping of an RPC method to one or more HTTP +// REST API methods. The mapping specifies how different portions of the RPC +// request message are mapped to URL path, URL query parameters, and +// HTTP request body. The mapping is typically specified as an +// `google.api.http` annotation on the RPC method, +// see "google/api/annotations.proto" for details. +// +// The mapping consists of a field specifying the path template and +// method kind. The path template can refer to fields in the request +// message, as in the example below which describes a REST GET +// operation on a resource collection of messages: +// +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}"; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // mapped to the URL +// SubMessage sub = 2; // `sub.subfield` is url-mapped +// } +// message Message { +// string text = 1; // content of the resource +// } +// +// The same http annotation can alternatively be expressed inside the +// `GRPC API Configuration` YAML file. +// +// http: +// rules: +// - selector: .Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// This definition enables an automatic, bidrectional mapping of HTTP +// JSON to RPC. Example: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))` +// +// In general, not only fields but also field paths can be referenced +// from a path pattern. Fields mapped to the path pattern cannot be +// repeated and must have a primitive (non-message) type. +// +// Any fields in the request message which are not bound by the path +// pattern automatically become (optional) HTTP query +// parameters. Assume the following definition of the request message: +// +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http).get = "/v1/messages/{message_id}"; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // mapped to the URL +// int64 revision = 2; // becomes a parameter +// SubMessage sub = 3; // `sub.subfield` becomes a parameter +// } +// +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))` +// +// Note that fields which are mapped to HTTP parameters must have a +// primitive type or a repeated primitive type. Message types are not +// allowed. In the case of a repeated type, the parameter can be +// repeated in the URL, as in `...?param=A¶m=B`. +// +// For HTTP method kinds which allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// put: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | RPC +// -----|----- +// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// put: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | RPC +// -----|----- +// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice of +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// +// This enables the following two alternative HTTP JSON to RPC +// mappings: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")` +// +// # Rules for HTTP mapping +// +// The rules for mapping HTTP path, query parameters, and body fields +// to the request message are as follows: +// +// 1. The `body` field specifies either `*` or a field path, or is +// omitted. If omitted, it indicates there is no HTTP request body. +// 2. Leaf fields (recursive expansion of nested messages in the +// request) can be classified into three types: +// (a) Matched in the URL template. +// (b) Covered by body (if body is `*`, everything except (a) fields; +// else everything under the body field) +// (c) All other fields. +// 3. URL query parameters found in the HTTP request are mapped to (c) fields. +// 4. Any body sent with an HTTP request can contain only (b) fields. +// +// The syntax of the path template is as follows: +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single path segment. The syntax `**` matches zero +// or more path segments, which must be the last part of the path except the +// `Verb`. The syntax `LITERAL` matches literal text in the path. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path, all characters +// except `[-_.~0-9a-zA-Z]` are percent-encoded. Such variables show up in the +// Discovery Document as `{var}`. +// +// If a variable contains one or more path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path, all +// characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. Such variables +// show up in the Discovery Document as `{+var}`. +// +// NOTE: While the single segment variable matches the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 +// Simple String Expansion, the multi segment variable **does not** match +// RFC 6570 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. +// +// NOTE: the field paths in variables and in the `body` must not refer to +// repeated fields or map fields. +message HttpRule { + // Selects methods to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Used for listing and getting information about resources. + string get = 2; + + // Used for updating a resource. + string put = 3; + + // Used for creating a resource. + string post = 4; + + // Used for deleting a resource. + string delete = 5; + + // Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP body, or + // `*` for mapping all fields not captured by the path pattern to the HTTP + // body. NOTE: the referred field must not be a repeated field and must be + // present at the top-level of request message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // body of response. Other response fields are ignored. When + // not set, the response message will be used as HTTP body of response. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/apps/protoc-gen-openapi/examples/google/api/resource.proto b/apps/protoc-gen-openapi/examples/google/api/resource.proto new file mode 100644 index 00000000..fd9ee66d --- /dev/null +++ b/apps/protoc-gen-openapi/examples/google/api/resource.proto @@ -0,0 +1,299 @@ +// Copyright 2020 Google LLC +// +// 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. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "ResourceProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // An annotation that describes a resource reference, see + // [ResourceReference][]. + google.api.ResourceReference resource_reference = 1055; +} + +extend google.protobuf.FileOptions { + // An annotation that describes a resource definition without a corresponding + // message; see [ResourceDescriptor][]. + repeated google.api.ResourceDescriptor resource_definition = 1053; +} + +extend google.protobuf.MessageOptions { + // An annotation that describes a resource definition, see + // [ResourceDescriptor][]. + google.api.ResourceDescriptor resource = 1053; +} + +// A simple descriptor of a resource type. +// +// ResourceDescriptor annotates a resource message (either by means of a +// protobuf annotation or use in the service config), and associates the +// resource's schema, the resource type, and the pattern of the resource name. +// +// Example: +// +// message Topic { +// // Indicates this message defines a resource schema. +// // Declares the resource type in the format of {service}/{kind}. +// // For Kubernetes resources, the format is {api group}/{kind}. +// option (google.api.resource) = { +// type: "pubsub.googleapis.com/Topic" +// name_descriptor: { +// pattern: "projects/{project}/topics/{topic}" +// parent_type: "cloudresourcemanager.googleapis.com/Project" +// parent_name_extractor: "projects/{project}" +// } +// }; +// } +// +// The ResourceDescriptor Yaml config will look like: +// +// resources: +// - type: "pubsub.googleapis.com/Topic" +// name_descriptor: +// - pattern: "projects/{project}/topics/{topic}" +// parent_type: "cloudresourcemanager.googleapis.com/Project" +// parent_name_extractor: "projects/{project}" +// +// Sometimes, resources have multiple patterns, typically because they can +// live under multiple parents. +// +// Example: +// +// message LogEntry { +// option (google.api.resource) = { +// type: "logging.googleapis.com/LogEntry" +// name_descriptor: { +// pattern: "projects/{project}/logs/{log}" +// parent_type: "cloudresourcemanager.googleapis.com/Project" +// parent_name_extractor: "projects/{project}" +// } +// name_descriptor: { +// pattern: "folders/{folder}/logs/{log}" +// parent_type: "cloudresourcemanager.googleapis.com/Folder" +// parent_name_extractor: "folders/{folder}" +// } +// name_descriptor: { +// pattern: "organizations/{organization}/logs/{log}" +// parent_type: "cloudresourcemanager.googleapis.com/Organization" +// parent_name_extractor: "organizations/{organization}" +// } +// name_descriptor: { +// pattern: "billingAccounts/{billing_account}/logs/{log}" +// parent_type: "billing.googleapis.com/BillingAccount" +// parent_name_extractor: "billingAccounts/{billing_account}" +// } +// }; +// } +// +// The ResourceDescriptor Yaml config will look like: +// +// resources: +// - type: 'logging.googleapis.com/LogEntry' +// name_descriptor: +// - pattern: "projects/{project}/logs/{log}" +// parent_type: "cloudresourcemanager.googleapis.com/Project" +// parent_name_extractor: "projects/{project}" +// - pattern: "folders/{folder}/logs/{log}" +// parent_type: "cloudresourcemanager.googleapis.com/Folder" +// parent_name_extractor: "folders/{folder}" +// - pattern: "organizations/{organization}/logs/{log}" +// parent_type: "cloudresourcemanager.googleapis.com/Organization" +// parent_name_extractor: "organizations/{organization}" +// - pattern: "billingAccounts/{billing_account}/logs/{log}" +// parent_type: "billing.googleapis.com/BillingAccount" +// parent_name_extractor: "billingAccounts/{billing_account}" +// +// For flexible resources, the resource name doesn't contain parent names, but +// the resource itself has parents for policy evaluation. +// +// Example: +// +// message Shelf { +// option (google.api.resource) = { +// type: "library.googleapis.com/Shelf" +// name_descriptor: { +// pattern: "shelves/{shelf}" +// parent_type: "cloudresourcemanager.googleapis.com/Project" +// } +// name_descriptor: { +// pattern: "shelves/{shelf}" +// parent_type: "cloudresourcemanager.googleapis.com/Folder" +// } +// }; +// } +// +// The ResourceDescriptor Yaml config will look like: +// +// resources: +// - type: 'library.googleapis.com/Shelf' +// name_descriptor: +// - pattern: "shelves/{shelf}" +// parent_type: "cloudresourcemanager.googleapis.com/Project" +// - pattern: "shelves/{shelf}" +// parent_type: "cloudresourcemanager.googleapis.com/Folder" +message ResourceDescriptor { + // A description of the historical or future-looking state of the + // resource pattern. + enum History { + // The "unset" value. + HISTORY_UNSPECIFIED = 0; + + // The resource originally had one pattern and launched as such, and + // additional patterns were added later. + ORIGINALLY_SINGLE_PATTERN = 1; + + // The resource has one pattern, but the API owner expects to add more + // later. (This is the inverse of ORIGINALLY_SINGLE_PATTERN, and prevents + // that from being necessary once there are multiple patterns.) + FUTURE_MULTI_PATTERN = 2; + } + + // A flag representing a specific style that a resource claims to conform to. + enum Style { + // The unspecified value. Do not use. + STYLE_UNSPECIFIED = 0; + + // This resource is intended to be "declarative-friendly". + // + // Declarative-friendly resources must be more strictly consistent, and + // setting this to true communicates to tools that this resource should + // adhere to declarative-friendly expectations. + // + // Note: This is used by the API linter (linter.aip.dev) to enable + // additional checks. + DECLARATIVE_FRIENDLY = 1; + } + + // The resource type. It must be in the format of + // {service_name}/{resource_type_kind}. The `resource_type_kind` must be + // singular and must not include version numbers. + // + // Example: `storage.googleapis.com/Bucket` + // + // The value of the resource_type_kind must follow the regular expression + // /[A-Za-z][a-zA-Z0-9]+/. It should start with an upper case character and + // should use PascalCase (UpperCamelCase). The maximum number of + // characters allowed for the `resource_type_kind` is 100. + string type = 1; + + // Optional. The relative resource name pattern associated with this resource + // type. The DNS prefix of the full resource name shouldn't be specified here. + // + // The path pattern must follow the syntax, which aligns with HTTP binding + // syntax: + // + // Template = Segment { "/" Segment } ; + // Segment = LITERAL | Variable ; + // Variable = "{" LITERAL "}" ; + // + // Examples: + // + // - "projects/{project}/topics/{topic}" + // - "projects/{project}/knowledgeBases/{knowledge_base}" + // + // The components in braces correspond to the IDs for each resource in the + // hierarchy. It is expected that, if multiple patterns are provided, + // the same component name (e.g. "project") refers to IDs of the same + // type of resource. + repeated string pattern = 2; + + // Optional. The field on the resource that designates the resource name + // field. If omitted, this is assumed to be "name". + string name_field = 3; + + // Optional. The historical or future-looking state of the resource pattern. + // + // Example: + // + // // The InspectTemplate message originally only supported resource + // // names with organization, and project was added later. + // message InspectTemplate { + // option (google.api.resource) = { + // type: "dlp.googleapis.com/InspectTemplate" + // pattern: + // "organizations/{organization}/inspectTemplates/{inspect_template}" + // pattern: "projects/{project}/inspectTemplates/{inspect_template}" + // history: ORIGINALLY_SINGLE_PATTERN + // }; + // } + History history = 4; + + // The plural name used in the resource name and permission names, such as + // 'projects' for the resource name of 'projects/{project}' and the permission + // name of 'cloudresourcemanager.googleapis.com/projects.get'. It is the same + // concept of the `plural` field in k8s CRD spec + // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ + // + // Note: The plural form is required even for singleton resources. See + // https://aip.dev/156 + string plural = 5; + + // The same concept of the `singular` field in k8s CRD spec + // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ + // Such as "project" for the `resourcemanager.googleapis.com/Project` type. + string singular = 6; + + // Style flag(s) for this resource. + // These indicate that a resource is expected to conform to a given + // style. See the specific style flags for additional information. + repeated Style style = 10; +} + +// Defines a proto annotation that describes a string field that refers to +// an API resource. +message ResourceReference { + // The resource type that the annotated field references. + // + // Example: + // + // message Subscription { + // string topic = 2 [(google.api.resource_reference) = { + // type: "pubsub.googleapis.com/Topic" + // }]; + // } + // + // Occasionally, a field may reference an arbitrary resource. In this case, + // APIs use the special value * in their resource reference. + // + // Example: + // + // message GetIamPolicyRequest { + // string resource = 2 [(google.api.resource_reference) = { + // type: "*" + // }]; + // } + string type = 1; + + // The resource type of a child collection that the annotated field + // references. This is useful for annotating the `parent` field that + // doesn't have a fixed resource type. + // + // Example: + // + // message ListLogEntriesRequest { + // string parent = 1 [(google.api.resource_reference) = { + // child_type: "logging.googleapis.com/LogEntry" + // }; + // } + string child_type = 2; +} diff --git a/apps/protoc-gen-openapi/examples/google/example/library/v1/library.proto b/apps/protoc-gen-openapi/examples/google/example/library/v1/library.proto new file mode 100644 index 00000000..80912cf6 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/google/example/library/v1/library.proto @@ -0,0 +1,339 @@ +// Copyright 2019 Google LLC. +// +// 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. +// + +syntax = "proto3"; + +package google.example.library.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; + +option go_package = "google.golang.org/genproto/googleapis/example/library/v1;library"; +option java_multiple_files = true; +option java_outer_classname = "LibraryProto"; +option java_package = "com.google.example.library.v1"; + +// This API represents a simple digital library. It lets you manage Shelf +// resources and Book resources in the library. It defines the following +// resource model: +// +// - The API has a collection of [Shelf][google.example.library.v1.Shelf] +// resources, named `shelves/*` +// +// - Each Shelf has a collection of [Book][google.example.library.v1.Book] +// resources, named `shelves/*/books/*` +service LibraryService { + option (google.api.default_host) = "library-example.googleapis.com"; + + // Creates a shelf, and returns the new Shelf. + rpc CreateShelf(CreateShelfRequest) returns (Shelf) { + option (google.api.http) = { + post: "/v1/shelves" + body: "shelf" + }; + option (google.api.method_signature) = "shelf"; + } + + // Gets a shelf. Returns NOT_FOUND if the shelf does not exist. + rpc GetShelf(GetShelfRequest) returns (Shelf) { + option (google.api.http) = { + get: "/v1/{name=shelves/*}" + }; + option (google.api.method_signature) = "name"; + } + + // Lists shelves. The order is unspecified but deterministic. Newly created + // shelves will not necessarily be added to the end of this list. + rpc ListShelves(ListShelvesRequest) returns (ListShelvesResponse) { + option (google.api.http) = { + get: "/v1/shelves" + }; + } + + // Deletes a shelf. Returns NOT_FOUND if the shelf does not exist. + rpc DeleteShelf(DeleteShelfRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/v1/{name=shelves/*}" + }; + option (google.api.method_signature) = "name"; + } + + // Merges two shelves by adding all books from the shelf named + // `other_shelf_name` to shelf `name`, and deletes + // `other_shelf_name`. Returns the updated shelf. + // The book ids of the moved books may not be the same as the original books. + // + // Returns NOT_FOUND if either shelf does not exist. + // This call is a no-op if the specified shelves are the same. + rpc MergeShelves(MergeShelvesRequest) returns (Shelf) { + option (google.api.http) = { + post: "/v1/{name=shelves/*}:merge" + body: "*" + }; + option (google.api.method_signature) = "name,other_shelf_name"; + } + + // Creates a book, and returns the new Book. + rpc CreateBook(CreateBookRequest) returns (Book) { + option (google.api.http) = { + post: "/v1/{name=shelves/*}/books" + body: "book" + }; + option (google.api.method_signature) = "name,book"; + } + + // Gets a book. Returns NOT_FOUND if the book does not exist. + rpc GetBook(GetBookRequest) returns (Book) { + option (google.api.http) = { + get: "/v1/{name=shelves/*/books/*}" + }; + option (google.api.method_signature) = "name"; + } + + // Lists books in a shelf. The order is unspecified but deterministic. Newly + // created books will not necessarily be added to the end of this list. + // Returns NOT_FOUND if the shelf does not exist. + rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) { + option (google.api.http) = { + get: "/v1/{name=shelves/*}/books" + }; + option (google.api.method_signature) = "name"; + } + + // Deletes a book. Returns NOT_FOUND if the book does not exist. + rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/v1/{name=shelves/*/books/*}" + }; + } + + // Updates a book. Returns INVALID_ARGUMENT if the name of the book + // is non-empty and does not equal the existing name. + rpc UpdateBook(UpdateBookRequest) returns (Book) { + option (google.api.http) = { + put: "/v1/{book.name=shelves/*/books/*}" + body: "book" + }; + option (google.api.method_signature) = "book"; + } + + // Moves a book to another shelf, and returns the new book. The book + // id of the new book may not be the same as the original book. + rpc MoveBook(MoveBookRequest) returns (Book) { + option (google.api.http) = { + post: "/v1/{name=shelves/*/books/*}:move" + body: "*" + }; + option (google.api.method_signature) = "name,other_shelf_name"; + } +} + +// A single book in the library. +message Book { + option (google.api.resource) = { + type: "library-example.googleapis.com/Book", + pattern: "shelves/{shelf_id}/books/{book_id}" + }; + // The resource name of the book. + // Book names have the form `shelves/{shelf_id}/books/{book_id}`. + // The name is ignored when creating a book. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "library-example.googleapis.com/Book" + ]; + + // The name of the book author. + string author = 2; + + // The title of the book. + string title = 3; + + // Value indicating whether the book has been read. + bool read = 4; +} + +// A Shelf contains a collection of books with a theme. +message Shelf { + option (google.api.resource) = { + type: "library-example.googleapis.com/Shelf", + pattern: "shelves/{shelf_id}" + }; + // The resource name of the shelf. + // Shelf names have the form `shelves/{shelf_id}`. + // The name is ignored when creating a shelf. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "library-example.googleapis.com/Shelf" + ]; + + // The theme of the shelf + string theme = 2; +} + +// Request message for LibraryService.CreateShelf. +message CreateShelfRequest { + // The shelf to create. + Shelf shelf = 1 [(google.api.field_behavior) = REQUIRED]; +} + +// Request message for LibraryService.GetShelf. +message GetShelfRequest { + // The name of the shelf to retrieve. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Shelf" + ]; +} + +// Request message for LibraryService.ListShelves. +message ListShelvesRequest { + // Requested page size. Server may return fewer shelves than requested. + // If unspecified, server will pick an appropriate default. + int32 page_size = 1; + + // A token identifying a page of results the server should return. + // Typically, this is the value of + // [ListShelvesResponse.next_page_token][google.example.library.v1.ListShelvesResponse.next_page_token] + // returned from the previous call to `ListShelves` method. + string page_token = 2; +} + +// Response message for LibraryService.ListShelves. +message ListShelvesResponse { + // The list of shelves. + repeated Shelf shelves = 1; + + // A token to retrieve next page of results. + // Pass this value in the + // [ListShelvesRequest.page_token][google.example.library.v1.ListShelvesRequest.page_token] + // field in the subsequent call to `ListShelves` method to retrieve the next + // page of results. + string next_page_token = 2; +} + +// Request message for LibraryService.DeleteShelf. +message DeleteShelfRequest { + // The name of the shelf to delete. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Shelf" + ]; +} + +// Describes the shelf being removed (other_shelf_name) and updated +// (name) in this merge. +message MergeShelvesRequest { + // The name of the shelf we're adding books to. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Shelf" + ]; + + // The name of the shelf we're removing books from and deleting. + string other_shelf_name = 2 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Shelf" + ]; +} + +// Request message for LibraryService.CreateBook. +message CreateBookRequest { + // The name of the shelf in which the book is created. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Shelf" + ]; + + // The book to create. + Book book = 2 [(google.api.field_behavior) = REQUIRED]; +} + +// Request message for LibraryService.GetBook. +message GetBookRequest { + // The name of the book to retrieve. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Book" + ]; +} + +// Request message for LibraryService.ListBooks. +message ListBooksRequest { + // The name of the shelf whose books we'd like to list. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Shelf" + ]; + + // Requested page size. Server may return fewer books than requested. + // If unspecified, server will pick an appropriate default. + int32 page_size = 2; + + // A token identifying a page of results the server should return. + // Typically, this is the value of + // [ListBooksResponse.next_page_token][google.example.library.v1.ListBooksResponse.next_page_token]. + // returned from the previous call to `ListBooks` method. + string page_token = 3; +} + +// Response message for LibraryService.ListBooks. +message ListBooksResponse { + // The list of books. + repeated Book books = 1; + + // A token to retrieve next page of results. + // Pass this value in the + // [ListBooksRequest.page_token][google.example.library.v1.ListBooksRequest.page_token] + // field in the subsequent call to `ListBooks` method to retrieve the next + // page of results. + string next_page_token = 2; +} + +// Request message for LibraryService.UpdateBook. +message UpdateBookRequest { + // The name of the book to update. + string name = 1 [(google.api.field_behavior) = REQUIRED]; + + // The book to update with. The name must match or be empty. + Book book = 2 [(google.api.field_behavior) = REQUIRED]; +} + +// Request message for LibraryService.DeleteBook. +message DeleteBookRequest { + // The name of the book to delete. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Book" + ]; +} + +// Describes what book to move (name) and what shelf we're moving it +// to (other_shelf_name). +message MoveBookRequest { + // The name of the book to move. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Book" + ]; + + // The name of the destination shelf. + string other_shelf_name = 2 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference).type = "Shelf" + ]; +} diff --git a/apps/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml b/apps/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml new file mode 100644 index 00000000..c866572f --- /dev/null +++ b/apps/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml @@ -0,0 +1,337 @@ +# Generated with protoc-gen-openapi +# https://github.com/googleapis/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: "3.0" +info: + title: LibraryService + description: 'This API represents a simple digital library. It lets you manage Shelf resources and Book resources in the library. It defines the following resource model: - The API has a collection of [Shelf][google.example.library.v1.Shelf] resources, named `shelves/*` - Each Shelf has a collection of [Book][google.example.library.v1.Book] resources, named `shelves/*/books/*`' + version: 0.0.1 +paths: + /v1/shelves: + get: + summary: Lists shelves. The order is unspecified but deterministic. Newly created shelves will not necessarily be added to the end of this list. + operationId: LibraryService_ListShelves + parameters: + - name: page_size + in: query + description: Requested page size. Server may return fewer shelves than requested. If unspecified, server will pick an appropriate default. + schema: + type: string + - name: page_token + in: query + description: A token identifying a page of results the server should return. Typically, this is the value of [ListShelvesResponse.next_page_token][google.example.library.v1.ListShelvesResponse.next_page_token] returned from the previous call to `ListShelves` method. + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListShelvesResponse' + post: + summary: Creates a shelf, and returns the new Shelf. + operationId: LibraryService_CreateShelf + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Shelf' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Shelf' + /v1/shelves/{shelf}: + get: + summary: Gets a shelf. Returns NOT_FOUND if the shelf does not exist. + operationId: LibraryService_GetShelf + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Shelf' + delete: + summary: Deletes a shelf. Returns NOT_FOUND if the shelf does not exist. + operationId: LibraryService_DeleteShelf + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Empty' + /v1/shelves/{shelf}/books: + get: + summary: Lists books in a shelf. The order is unspecified but deterministic. Newly created books will not necessarily be added to the end of this list. Returns NOT_FOUND if the shelf does not exist. + operationId: LibraryService_ListBooks + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + - name: page_size + in: query + description: Requested page size. Server may return fewer books than requested. If unspecified, server will pick an appropriate default. + schema: + type: string + - name: page_token + in: query + description: A token identifying a page of results the server should return. Typically, this is the value of [ListBooksResponse.next_page_token][google.example.library.v1.ListBooksResponse.next_page_token]. returned from the previous call to `ListBooks` method. + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListBooksResponse' + post: + summary: Creates a book, and returns the new Book. + operationId: LibraryService_CreateBook + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + /v1/shelves/{shelf}/books/{book}: + get: + summary: Gets a book. Returns NOT_FOUND if the book does not exist. + operationId: LibraryService_GetBook + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + - name: book + in: path + description: The book id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + put: + summary: Updates a book. Returns INVALID_ARGUMENT if the name of the book is non-empty and does not equal the existing name. + operationId: LibraryService_UpdateBook + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + - name: book + in: path + description: The book id. + required: true + schema: + type: string + - name: name + in: query + description: The name of the book to update. + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + delete: + summary: Deletes a book. Returns NOT_FOUND if the book does not exist. + operationId: LibraryService_DeleteBook + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + - name: book + in: path + description: The book id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Empty' + /v1/shelves/{shelf}/books/{book}:move: + post: + summary: Moves a book to another shelf, and returns the new book. The book id of the new book may not be the same as the original book. + operationId: LibraryService_MoveBook + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + - name: book + in: path + description: The book id. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MoveBookRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Book' + /v1/shelves/{shelf}:merge: + post: + summary: Merges two shelves by adding all books from the shelf named `other_shelf_name` to shelf `name`, and deletes `other_shelf_name`. Returns the updated shelf. The book ids of the moved books may not be the same as the original books. Returns NOT_FOUND if either shelf does not exist. This call is a no-op if the specified shelves are the same. + operationId: LibraryService_MergeShelves + parameters: + - name: shelf + in: path + description: The shelf id. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MergeShelvesRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Shelf' +components: + schemas: + Book: + properties: + name: + type: string + description: The resource name of the book. Book names have the form `shelves/{shelf_id}/books/{book_id}`. The name is ignored when creating a book. + author: + type: string + description: The name of the book author. + title: + type: string + description: The title of the book. + read: + type: boolean + description: Value indicating whether the book has been read. + description: A single book in the library. + Empty: + properties: {} + description: 'A generic empty message that you can re-use to avoid defining duplicated empty messages in your APIs. A typical example is to use it as the request or the response type of an API method. For instance: service Foo { rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); } The JSON representation for `Empty` is empty JSON object `{}`.' + ListBooksResponse: + properties: + books: + type: array + items: + $ref: '#/components/schemas/Book' + description: The list of books. + next_page_token: + type: string + description: A token to retrieve next page of results. Pass this value in the [ListBooksRequest.page_token][google.example.library.v1.ListBooksRequest.page_token] field in the subsequent call to `ListBooks` method to retrieve the next page of results. + description: Response message for LibraryService.ListBooks. + ListShelvesResponse: + properties: + shelves: + type: array + items: + $ref: '#/components/schemas/Shelf' + description: The list of shelves. + next_page_token: + type: string + description: A token to retrieve next page of results. Pass this value in the [ListShelvesRequest.page_token][google.example.library.v1.ListShelvesRequest.page_token] field in the subsequent call to `ListShelves` method to retrieve the next page of results. + description: Response message for LibraryService.ListShelves. + MergeShelvesRequest: + properties: + name: + type: string + description: The name of the shelf we're adding books to. + other_shelf_name: + type: string + description: The name of the shelf we're removing books from and deleting. + description: Describes the shelf being removed (other_shelf_name) and updated (name) in this merge. + MoveBookRequest: + properties: + name: + type: string + description: The name of the book to move. + other_shelf_name: + type: string + description: The name of the destination shelf. + description: Describes what book to move (name) and what shelf we're moving it to (other_shelf_name). + Shelf: + properties: + name: + type: string + description: The resource name of the shelf. Shelf names have the form `shelves/{shelf_id}`. The name is ignored when creating a shelf. + theme: + type: string + description: The theme of the shelf + description: A Shelf contains a collection of books with a theme. diff --git a/apps/protoc-gen-openapi/generator/openapi-v3.go b/apps/protoc-gen-openapi/generator/openapi-v3.go new file mode 100644 index 00000000..6cd8ad3d --- /dev/null +++ b/apps/protoc-gen-openapi/generator/openapi-v3.go @@ -0,0 +1,567 @@ +// Copyright 2020 Google LLC. 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. +// + +package generator + +import ( + "fmt" + "log" + "regexp" + "sort" + "strings" + + v3 "github.com/googleapis/gnostic/openapiv3" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +const infoURL = "https://github.com/googleapis/gnostic/tree/master/apps/protoc-gen-openapi" + +// OpenAPIv3Generator holds internal state needed to generate an OpenAPIv3 document for a transcoded Protocol Buffer service. +type OpenAPIv3Generator struct { + plugin *protogen.Plugin + + requiredSchemas []string // Names of schemas that need to be generated. + generatedSchemas []string // Names of schemas that have already been generated. + linterRulePattern *regexp.Regexp + namePattern *regexp.Regexp +} + +// NewOpenAPIv3Generator creates a new generator for a protoc plugin invocation. +func NewOpenAPIv3Generator(plugin *protogen.Plugin) *OpenAPIv3Generator { + return &OpenAPIv3Generator{ + plugin: plugin, + requiredSchemas: make([]string, 0), + generatedSchemas: make([]string, 0), + linterRulePattern: regexp.MustCompile(`\(-- .* --\)`), + namePattern: regexp.MustCompile("{(.*)=(.*)}"), + } +} + +// Run runs the generator. +func (g *OpenAPIv3Generator) Run() error { + d := g.buildDocumentV3() + bytes, err := d.YAMLValue("Generated with protoc-gen-openapi\n" + infoURL) + if err != nil { + return fmt.Errorf("failed to marshal yaml: %s", err.Error()) + } + outputFile := g.plugin.NewGeneratedFile("openapi.yaml", "") + outputFile.Write(bytes) + return nil +} + +// buildDocumentV3 builds an OpenAPIv3 document for a plugin request. +func (g *OpenAPIv3Generator) buildDocumentV3() *v3.Document { + d := &v3.Document{} + d.Openapi = "3.0" + d.Info = &v3.Info{ + Title: "", + Version: "0.0.1", + Description: "", + } + d.Paths = &v3.Paths{} + d.Components = &v3.Components{ + Schemas: &v3.SchemasOrReferences{ + AdditionalProperties: []*v3.NamedSchemaOrReference{}, + }, + } + for _, file := range g.plugin.Files { + g.addPathsToDocumentV3(d, file) + } + for len(g.requiredSchemas) > 0 { + log.Printf("REQUIRED SCHEMAS %+v", g.requiredSchemas) + count := len(g.requiredSchemas) + for _, file := range g.plugin.Files { + g.addSchemasToDocumentV3(d, file) + } + g.requiredSchemas = g.requiredSchemas[count:len(g.requiredSchemas)] + } + // Sort the paths. + { + pairs := d.Paths.Path + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + d.Paths.Path = pairs + } + // Sort the schemas. + { + pairs := d.Components.Schemas.AdditionalProperties + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + d.Components.Schemas.AdditionalProperties = pairs + } + return d +} + +// filterCommentString removes line breaks and linter rules from comments. +func (g *OpenAPIv3Generator) filterCommentString(c protogen.Comments) string { + comment := string(c) + comment = strings.Replace(comment, "\n", "", -1) + comment = g.linterRulePattern.ReplaceAllString(comment, "") + return strings.TrimSpace(comment) +} + +// addPathsToDocumentV3 adds paths from a specified file descriptor. +func (g *OpenAPIv3Generator) addPathsToDocumentV3(d *v3.Document, file *protogen.File) { + for _, service := range file.Services { + comment := g.filterCommentString(service.Comments.Leading) + d.Info.Title = service.GoName + d.Info.Description = comment + for _, method := range service.Methods { + comment := g.filterCommentString(method.Comments.Leading) + inputMessage := method.Input + outputMessage := method.Output + operationID := service.GoName + "_" + method.GoName + extension := proto.GetExtension(method.Desc.Options(), annotations.E_Http) + var path string + var methodName string + var body string + if extension != nil { + rule := extension.(*annotations.HttpRule) + body = rule.Body + switch pattern := rule.Pattern.(type) { + case *annotations.HttpRule_Get: + path = pattern.Get + methodName = "GET" + case *annotations.HttpRule_Post: + path = pattern.Post + methodName = "POST" + case *annotations.HttpRule_Put: + path = pattern.Put + methodName = "PUT" + case *annotations.HttpRule_Delete: + path = pattern.Delete + methodName = "DELETE" + case *annotations.HttpRule_Patch: + path = pattern.Patch + methodName = "PATCH" + case *annotations.HttpRule_Custom: + path = "custom-unsupported" + default: + path = "unknown-unsupported" + } + } + if methodName != "" { + op, path2 := g.buildOperationV3( + file, operationID, comment, path, body, inputMessage, outputMessage) + g.addOperationV3(d, op, path2, methodName) + } + } + } +} + +// buildOperationV3 constructs an operation for a set of values. +func (g *OpenAPIv3Generator) buildOperationV3( + file *protogen.File, + operationID string, + description string, + path string, + bodyField string, + inputMessage *protogen.Message, + outputMessage *protogen.Message, +) (*v3.Operation, string) { + // coveredParameters tracks the parameters that have been used in the body or path. + coveredParameters := make([]string, 0) + if bodyField != "" { + coveredParameters = append(coveredParameters, bodyField) + } + // Initialize the list of operation parameters. + parameters := []*v3.ParameterOrReference{} + // Build a list of path parameters. + pathParameters := make([]string, 0) + if matches := g.namePattern.FindStringSubmatch(path); matches != nil { + // Add the "name=" "name" value to the list of covered parameters. + coveredParameters = append(coveredParameters, matches[1]) + // Convert the path from the starred form to use named path parameters. + starredPath := matches[2] + parts := strings.Split(starredPath, "/") + // The starred path is assumed to be in the form "things/*/otherthings/*". + // We want to convert it to "things/{thing}/otherthings/{otherthing}". + for i := 0; i < len(parts); i += 2 { + section := parts[i] + parameter := singular(section) + parts[i+1] = "{" + parameter + "}" + pathParameters = append(pathParameters, parameter) + } + // Rewrite the path to use the path parameters. + newPath := strings.Join(parts, "/") + path = strings.Replace(path, matches[0], newPath, 1) + } + // Add the path parameters to the operation parameters. + for _, pathParameter := range pathParameters { + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: pathParameter, + In: "path", + Required: true, + Description: "The " + pathParameter + " id.", + Schema: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "string", + }, + }, + }, + }, + }, + }) + } + // Add any unhandled fields in the request message as query parameters. + if bodyField != "*" { + for _, field := range inputMessage.Fields { + fieldName := string(field.Desc.Name()) + if !contains(coveredParameters, fieldName) { + // Get the field description from the comments. + fieldDescription := g.filterCommentString(field.Comments.Leading) + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: fieldName, + In: "query", + Description: fieldDescription, + Required: false, + Schema: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "string", + }, + }, + }, + }, + }, + }) + } + } + } + // Create the response. + responses := &v3.Responses{ + ResponseOrReference: []*v3.NamedResponseOrReference{ + &v3.NamedResponseOrReference{ + Name: "200", + Value: &v3.ResponseOrReference{ + Oneof: &v3.ResponseOrReference_Response{ + Response: &v3.Response{ + Description: "OK", + Content: &v3.MediaTypes{ + AdditionalProperties: []*v3.NamedMediaType{ + &v3.NamedMediaType{ + Name: "application/json", + Value: &v3.MediaType{ + Schema: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{ + XRef: g.schemaReferenceForTypeName(fullMessageTypeName(outputMessage)), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + // Create the operation. + op := &v3.Operation{ + Summary: description, + OperationId: operationID, + Parameters: parameters, + Responses: responses, + } + // If a body field is specified, we need to pass a message as the request body. + if bodyField != "" { + var bodyFieldTypeName string + if bodyField == "*" { + // Pass the entire request message as the request body. + bodyFieldTypeName = fullMessageTypeName(inputMessage) + } else { + // If body refers to a message field, use that type. + for _, field := range inputMessage.Fields { + if string(field.Desc.Name()) == bodyField { + bodyFieldTypeName = fullMessageTypeName(field.Message) + break + } + } + } + op.RequestBody = &v3.RequestBodyOrReference{ + Oneof: &v3.RequestBodyOrReference_RequestBody{ + RequestBody: &v3.RequestBody{ + Required: true, + Content: &v3.MediaTypes{ + AdditionalProperties: []*v3.NamedMediaType{ + &v3.NamedMediaType{ + Name: "application/json", + Value: &v3.MediaType{ + Schema: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{ + XRef: g.schemaReferenceForTypeName(bodyFieldTypeName), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + } + return op, path +} + +// addOperationV3 adds an operation to the specified path/method. +func (g *OpenAPIv3Generator) addOperationV3(d *v3.Document, op *v3.Operation, path string, methodName string) { + var selectedPathItem *v3.NamedPathItem + for _, namedPathItem := range d.Paths.Path { + if namedPathItem.Name == path { + selectedPathItem = namedPathItem + break + } + } + // If we get here, we need to create a path item. + if selectedPathItem == nil { + selectedPathItem = &v3.NamedPathItem{Name: path, Value: &v3.PathItem{}} + d.Paths.Path = append(d.Paths.Path, selectedPathItem) + } + // Set the operation on the specified method. + switch methodName { + case "GET": + selectedPathItem.Value.Get = op + case "POST": + selectedPathItem.Value.Post = op + case "PUT": + selectedPathItem.Value.Put = op + case "DELETE": + selectedPathItem.Value.Delete = op + case "PATCH": + selectedPathItem.Value.Patch = op + } +} + +// schemaReferenceForTypeName returns an OpenAPI JSON Reference to the schema that represents a type. +func (g *OpenAPIv3Generator) schemaReferenceForTypeName(typeName string) string { + if !contains(g.requiredSchemas, typeName) { + g.requiredSchemas = append(g.requiredSchemas, typeName) + } + parts := strings.Split(typeName, ".") + lastPart := parts[len(parts)-1] + return "#/components/schemas/" + lastPart +} + +// itemsItemForTypeName is a helper constructor. +func itemsItemForTypeName(typeName string) *v3.ItemsItem { + return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{&v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: typeName}}}}} +} + +// itemsItemForReference is a helper constructor. +func itemsItemForReference(xref string) *v3.ItemsItem { + return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{&v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{ + XRef: xref}}}}} +} + +// fullMessageTypeName builds the full type name of a message. +func fullMessageTypeName(message *protogen.Message) string { + return "." + string(message.Desc.ParentFile().Package()) + "." + string(message.Desc.Name()) +} + +// addSchemasToDocumentV3 adds info from one file descriptor. +func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protogen.File) { + // For each message, generate a definition. + for _, message := range file.Messages { + typeName := fullMessageTypeName(message) + // Only generate this if we need it and haven't already generated it. + if !contains(g.requiredSchemas, typeName) || + contains(g.generatedSchemas, typeName) { + continue + } + g.generatedSchemas = append(g.generatedSchemas, typeName) + // Get the message description from the comments. + messageDescription := g.filterCommentString(message.Comments.Leading) + // Build an array holding the fields of the message. + definitionProperties := &v3.Properties{ + AdditionalProperties: make([]*v3.NamedSchemaOrReference, 0), + } + for _, field := range message.Fields { + // Check the field annotations to see if this is a readonly field. + outputOnly := false + extension := proto.GetExtension(field.Desc.Options(), annotations.E_FieldBehavior) + if extension != nil { + switch v := extension.(type) { + case []annotations.FieldBehavior: + for _, vv := range v { + if vv == annotations.FieldBehavior_OUTPUT_ONLY { + outputOnly = true + } + } + default: + log.Printf("unsupported extension type %T", extension) + } + } + // Get the field description from the comments. + fieldDescription := g.filterCommentString(field.Comments.Leading) + // The field is either described by a reference or a schema. + XRef := "" + fieldSchema := &v3.Schema{ + Description: fieldDescription, + } + if outputOnly { + fieldSchema.ReadOnly = true + } + if field.Desc.IsList() { + fieldSchema.Type = "array" + switch field.Desc.Kind() { + case protoreflect.MessageKind: + fieldSchema.Items = itemsItemForReference( + g.schemaReferenceForTypeName( + fullMessageTypeName(field.Message))) + case protoreflect.StringKind: + fieldSchema.Items = itemsItemForTypeName("string") + case protoreflect.Int32Kind, + protoreflect.Sint32Kind, + protoreflect.Uint32Kind, + protoreflect.Int64Kind, + protoreflect.Sint64Kind, + protoreflect.Uint64Kind, + protoreflect.Sfixed32Kind, + protoreflect.Fixed32Kind, + protoreflect.Sfixed64Kind, + protoreflect.Fixed64Kind: + fieldSchema.Items = itemsItemForTypeName("integer") + case protoreflect.EnumKind: + fieldSchema.Items = itemsItemForTypeName("integer") + case protoreflect.BoolKind: + fieldSchema.Items = itemsItemForTypeName("boolean") + case protoreflect.FloatKind, protoreflect.DoubleKind: + fieldSchema.Items = itemsItemForTypeName("number") + case protoreflect.BytesKind: + fieldSchema.Items = itemsItemForTypeName("string") + default: + log.Printf("(TODO) Unsupported array type: %+v", fullMessageTypeName(field.Message)) + } + } else { + k := field.Desc.Kind() + switch k { + case protoreflect.MessageKind: + // The field is described by a reference. + XRef = g.schemaReferenceForTypeName(fullMessageTypeName(field.Message)) + case protoreflect.StringKind: + fieldSchema.Type = "string" + case protoreflect.Int32Kind, + protoreflect.Sint32Kind, + protoreflect.Uint32Kind, + protoreflect.Int64Kind, + protoreflect.Sint64Kind, + protoreflect.Uint64Kind, + protoreflect.Sfixed32Kind, + protoreflect.Fixed32Kind, + protoreflect.Sfixed64Kind, + protoreflect.Fixed64Kind: + fieldSchema.Type = "integer" + fieldSchema.Format = k.String() + case protoreflect.EnumKind: + fieldSchema.Type = "integer" + fieldSchema.Format = "enum" + case protoreflect.BoolKind: + fieldSchema.Type = "boolean" + case protoreflect.FloatKind, protoreflect.DoubleKind: + fieldSchema.Type = "number" + fieldSchema.Format = k.String() + case protoreflect.BytesKind: + fieldSchema.Type = "string" + fieldSchema.Format = "bytes" + default: + log.Printf("(TODO) Unsupported field type: %+v", fullMessageTypeName(field.Message)) + } + } + var value *v3.SchemaOrReference + if XRef != "" { + value = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{ + XRef: XRef, + }, + }, + } + } else { + value = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: fieldSchema, + }, + } + } + definitionProperties.AdditionalProperties = append( + definitionProperties.AdditionalProperties, + &v3.NamedSchemaOrReference{ + Name: string(field.Desc.Name()), + Value: value, + }, + ) + } + // Add the schema to the components.schema list. + d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, + &v3.NamedSchemaOrReference{ + Name: string(message.Desc.Name()), + Value: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Description: messageDescription, + Properties: definitionProperties, + }, + }, + }, + }, + ) + } +} + +// contains returns true if an array contains a specified string. +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +// singular produces the singular form of a collection name. +func singular(plural string) string { + if strings.HasSuffix(plural, "ves") { + return strings.TrimSuffix(plural, "ves") + "f" + } + if strings.HasSuffix(plural, "ies") { + return strings.TrimSuffix(plural, "ies") + "y" + } + if strings.HasSuffix(plural, "s") { + return strings.TrimSuffix(plural, "s") + } + return plural +} diff --git a/apps/protoc-gen-openapi/main.go b/apps/protoc-gen-openapi/main.go new file mode 100644 index 00000000..081a373a --- /dev/null +++ b/apps/protoc-gen-openapi/main.go @@ -0,0 +1,27 @@ +// Copyright 2020 Google LLC. 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. +// + +package main + +import ( + "github.com/googleapis/gnostic/apps/protoc-gen-openapi/generator" + "google.golang.org/protobuf/compiler/protogen" +) + +func main() { + protogen.Options{}.Run(func(plugin *protogen.Plugin) error { + return generator.NewOpenAPIv3Generator(plugin).Run() + }) +} diff --git a/apps/protoc-gen-openapi/plugin_test.go b/apps/protoc-gen-openapi/plugin_test.go new file mode 100644 index 00000000..a6e2df70 --- /dev/null +++ b/apps/protoc-gen-openapi/plugin_test.go @@ -0,0 +1,42 @@ +// Copyright 2020 Google LLC. 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. + +package main + +import ( + "os" + "os/exec" + "testing" +) + +func TestLibraryOpenAPI(t *testing.T) { + var err error + // Run protoc and the protoc-gen-openapi plugin to generate an OpenAPI spec. + err = exec.Command("protoc", + "-I", "examples", + "examples/google/example/library/v1/library.proto", + "--openapi_out=.").Run() + if err != nil { + t.Logf("protoc failed: %+v", err) + t.FailNow() + } + // Verify that the generated spec matches our expected version. + err = exec.Command("diff", "openapi.yaml", "examples/google/example/library/v1/openapi.yaml").Run() + if err != nil { + t.Logf("Diff failed: %+v", err) + t.FailNow() + } + // if the test succeeded, clean up + os.Remove("openapi.yaml") +} diff --git a/discovery/discovery.pb.go b/discovery/discovery.pb.go index f76d235a..8b862ab3 100644 --- a/discovery/discovery.pb.go +++ b/discovery/discovery.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.12.0 // source: discovery/discovery.proto diff --git a/extensions/extension.pb.go b/extensions/extension.pb.go index 18eb53c7..6b6a8e28 100644 --- a/extensions/extension.pb.go +++ b/extensions/extension.pb.go @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.12.0 // source: extensions/extension.proto diff --git a/go.mod b/go.mod index a3837905..a83d03dd 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,13 @@ go 1.12 require ( github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 - github.com/golang/protobuf v1.4.2 + github.com/gogo/protobuf v1.3.1 + github.com/golang/protobuf v1.4.3 github.com/kr/pretty v0.2.0 // indirect github.com/stoewer/go-strcase v1.2.0 - google.golang.org/protobuf v1.23.0 + google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154 + google.golang.org/protobuf v1.24.0 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 + gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 ) diff --git a/go.sum b/go.sum index f32956ff..fcfd9fc2 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,95 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154 h1:bFFRpT+e8JJVY7lMMfvezL1ZIwqiwmPl2bsE2yx4HqM= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/metrics/complexity.pb.go b/metrics/complexity.pb.go index bf715707..c327925c 100644 --- a/metrics/complexity.pb.go +++ b/metrics/complexity.pb.go @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.12.0 // source: metrics/complexity.proto diff --git a/metrics/vocabulary.pb.go b/metrics/vocabulary.pb.go index c143a406..c4124416 100644 --- a/metrics/vocabulary.pb.go +++ b/metrics/vocabulary.pb.go @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.12.0 // source: metrics/vocabulary.proto diff --git a/openapiv2/OpenAPIv2.pb.go b/openapiv2/OpenAPIv2.pb.go index a9e551e2..4320dc37 100644 --- a/openapiv2/OpenAPIv2.pb.go +++ b/openapiv2/OpenAPIv2.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.12.0 // source: openapiv2/OpenAPIv2.proto diff --git a/openapiv2/document.go b/openapiv2/document.go index 3a26d319..56e5966b 100644 --- a/openapiv2/document.go +++ b/openapiv2/document.go @@ -14,7 +14,10 @@ package openapi_v2 -import "github.com/googleapis/gnostic/compiler" +import ( + "github.com/googleapis/gnostic/compiler" + "gopkg.in/yaml.v3" +) // ParseDocument reads an OpenAPI v2 description from a YAML/JSON representation. func ParseDocument(b []byte) (*Document, error) { @@ -25,3 +28,14 @@ func ParseDocument(b []byte) (*Document, error) { root := info.Content[0] return NewDocument(root, compiler.NewContextWithExtensions("$root", root, nil, nil)) } + +// YAMLValue produces a serialized YAML representation of the document. +func (d *Document) YAMLValue(comment string) ([]byte, error) { + rawInfo := d.ToRawInfo() + rawInfo = &yaml.Node{ + Kind: yaml.DocumentNode, + Content: []*yaml.Node{rawInfo}, + HeadComment: comment, + } + return yaml.Marshal(rawInfo) +} diff --git a/openapiv3/OpenAPIv3.pb.go b/openapiv3/OpenAPIv3.pb.go index 0c5b8b66..7a15298d 100644 --- a/openapiv3/OpenAPIv3.pb.go +++ b/openapiv3/OpenAPIv3.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.12.0 // source: openapiv3/OpenAPIv3.proto diff --git a/openapiv3/document.go b/openapiv3/document.go index 583a82ba..999f7fd2 100644 --- a/openapiv3/document.go +++ b/openapiv3/document.go @@ -14,7 +14,10 @@ package openapi_v3 -import "github.com/googleapis/gnostic/compiler" +import ( + "github.com/googleapis/gnostic/compiler" + "gopkg.in/yaml.v3" +) // ParseDocument reads an OpenAPI v3 description from a YAML/JSON representation. func ParseDocument(b []byte) (*Document, error) { @@ -25,3 +28,14 @@ func ParseDocument(b []byte) (*Document, error) { root := info.Content[0] return NewDocument(root, compiler.NewContextWithExtensions("$root", root, nil, nil)) } + +// YAMLValue produces a serialized YAML representation of the document. +func (d *Document) YAMLValue(comment string) ([]byte, error) { + rawInfo := d.ToRawInfo() + rawInfo = &yaml.Node{ + Kind: yaml.DocumentNode, + Content: []*yaml.Node{rawInfo}, + HeadComment: comment, + } + return yaml.Marshal(rawInfo) +} diff --git a/plugins/plugin.pb.go b/plugins/plugin.pb.go index e8d8d172..ecc689da 100644 --- a/plugins/plugin.pb.go +++ b/plugins/plugin.pb.go @@ -22,7 +22,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.12.0 // source: plugins/plugin.proto diff --git a/surface/surface.pb.go b/surface/surface.pb.go index e568ef47..df9f4f23 100644 --- a/surface/surface.pb.go +++ b/surface/surface.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.12.0 // source: surface/surface.proto