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 index d4f9c31..c110caa 100644 --- a/apps/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml +++ b/apps/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml @@ -3,13 +3,26 @@ openapi: 3.0.3 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/*`' + title: LibraryService API + 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. + tags: + - LibraryService + description: |- + 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 @@ -30,7 +43,9 @@ paths: schema: $ref: '#/components/schemas/ListShelvesResponse' post: - summary: Creates a shelf, and returns the new Shelf. + tags: + - LibraryService + description: Creates a shelf, and returns the new Shelf. operationId: LibraryService_CreateShelf requestBody: content: @@ -47,7 +62,9 @@ paths: $ref: '#/components/schemas/Shelf' /v1/shelves/{shelf}: get: - summary: Gets a shelf. Returns NOT_FOUND if the shelf does not exist. + tags: + - LibraryService + description: Gets a shelf. Returns NOT_FOUND if the shelf does not exist. operationId: LibraryService_GetShelf parameters: - name: shelf @@ -64,7 +81,9 @@ paths: schema: $ref: '#/components/schemas/Shelf' delete: - summary: Deletes a shelf. Returns NOT_FOUND if the shelf does not exist. + tags: + - LibraryService + description: Deletes a shelf. Returns NOT_FOUND if the shelf does not exist. operationId: LibraryService_DeleteShelf parameters: - name: shelf @@ -79,7 +98,12 @@ paths: content: {} /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. + tags: + - LibraryService + description: |- + 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 @@ -106,7 +130,9 @@ paths: schema: $ref: '#/components/schemas/ListBooksResponse' post: - summary: Creates a book, and returns the new Book. + tags: + - LibraryService + description: Creates a book, and returns the new Book. operationId: LibraryService_CreateBook parameters: - name: shelf @@ -130,7 +156,9 @@ paths: $ref: '#/components/schemas/Book' /v1/shelves/{shelf}/books/{book}: get: - summary: Gets a book. Returns NOT_FOUND if the book does not exist. + tags: + - LibraryService + description: Gets a book. Returns NOT_FOUND if the book does not exist. operationId: LibraryService_GetBook parameters: - name: shelf @@ -153,7 +181,11 @@ paths: 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. + tags: + - LibraryService + description: |- + 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 @@ -187,7 +219,9 @@ paths: schema: $ref: '#/components/schemas/Book' delete: - summary: Deletes a book. Returns NOT_FOUND if the book does not exist. + tags: + - LibraryService + description: Deletes a book. Returns NOT_FOUND if the book does not exist. operationId: LibraryService_DeleteBook parameters: - name: shelf @@ -208,7 +242,11 @@ paths: content: {} /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. + tags: + - LibraryService + description: |- + 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 @@ -238,7 +276,16 @@ paths: $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. + tags: + - LibraryService + description: |- + 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 @@ -356,3 +403,5 @@ components: description: The last update date and time. format: date-time description: A Shelf contains a collection of books with a theme. +tags: + - name: LibraryService diff --git a/apps/protoc-gen-openapi/examples/google/example/library/v1/openapi_json.yaml b/apps/protoc-gen-openapi/examples/google/example/library/v1/openapi_json.yaml new file mode 100644 index 0000000..d5a94b9 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/google/example/library/v1/openapi_json.yaml @@ -0,0 +1,407 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: LibraryService API + 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: 1.2.3 +paths: + /v1/shelves: + get: + tags: + - LibraryService + description: |- + 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: pageSize + 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: pageToken + 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: + tags: + - LibraryService + description: 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: + tags: + - LibraryService + description: 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: + tags: + - LibraryService + description: 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: {} + /v1/shelves/{shelf}/books: + get: + tags: + - LibraryService + description: |- + 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: pageSize + 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: pageToken + 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: + tags: + - LibraryService + description: 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: + tags: + - LibraryService + description: 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: + tags: + - LibraryService + description: |- + 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: + tags: + - LibraryService + description: 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: {} + /v1/shelves/{shelf}/books/{book}:move: + post: + tags: + - LibraryService + description: |- + 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: + tags: + - LibraryService + description: |- + 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. + borrowTime: + readOnly: true + type: string + description: The previous borrowing timestamp. + format: RFC3339 + createdAt: + readOnly: true + type: string + description: The creation date and time. + format: date-time + updatedAt: + readOnly: true + type: string + description: The last update date and time. + format: date-time + description: A single book in the library. + ListBooksResponse: + properties: + books: + type: array + items: + $ref: '#/components/schemas/Book' + description: The list of books. + nextPageToken: + 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. + nextPageToken: + 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. + otherShelfName: + 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. + otherShelfName: + 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 + nextSortAt: + readOnly: true + type: string + description: The next sorting date. + format: date + createdAt: + readOnly: true + type: string + description: The creation date and time. + format: date-time + updatedAt: + readOnly: true + type: string + description: The last update date and time. + format: date-time + description: A Shelf contains a collection of books with a theme. +tags: + - name: LibraryService diff --git a/apps/protoc-gen-openapi/examples/tests/bodymapping/openapi.yaml b/apps/protoc-gen-openapi/examples/tests/bodymapping/openapi.yaml index 737e99e..711a272 100644 --- a/apps/protoc-gen-openapi/examples/tests/bodymapping/openapi.yaml +++ b/apps/protoc-gen-openapi/examples/tests/bodymapping/openapi.yaml @@ -3,15 +3,18 @@ openapi: 3.0.3 info: - title: Messaging + title: Messaging API version: 0.0.1 paths: /v1/messages/{message_id}: patch: + tags: + - Messaging operationId: Messaging_UpdateMessage parameters: - name: message_id - in: query + in: path + required: true schema: type: string requestBody: @@ -35,3 +38,5 @@ components: type: string text: type: string +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/bodymapping/openapi_json.yaml b/apps/protoc-gen-openapi/examples/tests/bodymapping/openapi_json.yaml new file mode 100644 index 0000000..e90f8f5 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/bodymapping/openapi_json.yaml @@ -0,0 +1,42 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + version: 1.2.3 +paths: + /v1/messages/{messageId}: + patch: + tags: + - Messaging + operationId: Messaging_UpdateMessage + parameters: + - name: messageId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: string + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' +components: + schemas: + Message: + properties: + messageId: + type: string + text: + type: string +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/jsonoptions/message.proto b/apps/protoc-gen-openapi/examples/tests/jsonoptions/message.proto new file mode 100644 index 0000000..5e83235 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/jsonoptions/message.proto @@ -0,0 +1,48 @@ +// 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 tests.jsonnames.message.v1; + +import "google/api/annotations.proto"; + +option go_package = "github.com/google/gnostic/apps/protoc-gen-openapi/examples/tests/jsonnames/message/v1;message"; + +// Messaging service +service Messaging { + rpc CreateMessage(Message) returns (Message) { + option (google.api.http) = { + post : "/v1/messages/{message_id}" + body : "body_text" + }; + } + rpc UpdateMessage(Message2) returns (Message2) { + option (google.api.http) = { + patch : "/v1/messages/{message_id}" + body : "body_text" + }; + } +} +message Message { + string message_id = 1; + string body_text = 2; + string not_used = 3; +} +message Message2 { + string message_id = 1 [ json_name = "message_id" ]; + string body_text = 2 [ json_name = "body_text" ]; + string not_used = 3 [ json_name = "not_used" ]; +} diff --git a/apps/protoc-gen-openapi/examples/tests/jsonoptions/openapi.yaml b/apps/protoc-gen-openapi/examples/tests/jsonoptions/openapi.yaml new file mode 100644 index 0000000..1bb5772 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/jsonoptions/openapi.yaml @@ -0,0 +1,84 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + description: Messaging service + version: 0.0.1 +paths: + /v1/messages/{message_id}: + post: + tags: + - Messaging + operationId: Messaging_CreateMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + - name: not_used + in: query + schema: + type: string + requestBody: + content: + application/json: + schema: + type: string + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + patch: + tags: + - Messaging + operationId: Messaging_UpdateMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + - name: not_used + in: query + schema: + type: string + requestBody: + content: + application/json: + schema: + type: string + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message2' +components: + schemas: + Message: + properties: + message_id: + type: string + body_text: + type: string + not_used: + type: string + Message2: + properties: + message_id: + type: string + body_text: + type: string + not_used: + type: string +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/jsonoptions/openapi_json.yaml b/apps/protoc-gen-openapi/examples/tests/jsonoptions/openapi_json.yaml new file mode 100644 index 0000000..78a4a4f --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/jsonoptions/openapi_json.yaml @@ -0,0 +1,85 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + description: Messaging service + version: 1.2.3 +paths: + /v1/messages/{messageId}: + post: + tags: + - Messaging + operationId: Messaging_CreateMessage + parameters: + - name: messageId + in: path + required: true + schema: + type: string + - name: notUsed + in: query + schema: + type: string + requestBody: + content: + application/json: + schema: + type: string + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + /v1/messages/{message_id}: + patch: + tags: + - Messaging + operationId: Messaging_UpdateMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + - name: not_used + in: query + schema: + type: string + requestBody: + content: + application/json: + schema: + type: string + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message2' +components: + schemas: + Message: + properties: + messageId: + type: string + bodyText: + type: string + notUsed: + type: string + Message2: + properties: + message_id: + type: string + body_text: + type: string + not_used: + type: string +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml b/apps/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml index af94ebf..aa403a6 100644 --- a/apps/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml +++ b/apps/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml @@ -3,12 +3,20 @@ openapi: 3.0.3 info: - title: Messaging + title: Messaging API version: 0.0.1 paths: /v1/messages/{message_id}: patch: + tags: + - Messaging operationId: Messaging_UpdateMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string requestBody: content: application/json: @@ -30,3 +38,5 @@ components: type: string labels: type: object +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml b/apps/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml new file mode 100644 index 0000000..0eb736d --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml @@ -0,0 +1,42 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + version: 1.2.3 +paths: + /v1/messages/{messageId}: + patch: + tags: + - Messaging + operationId: Messaging_UpdateMessage + parameters: + - name: messageId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' +components: + schemas: + Message: + properties: + messageId: + type: string + labels: + type: object +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/pathparams/message.proto b/apps/protoc-gen-openapi/examples/tests/pathparams/message.proto new file mode 100644 index 0000000..447af38 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/pathparams/message.proto @@ -0,0 +1,53 @@ +// Copyright 2021 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 tests.pathparams.message.v1; + +import "google/api/annotations.proto"; + +option go_package = "github.com/google/gnostic/apps/protoc-gen-openapi/examples/tests/pathparams/message/v1;message"; + +service Messaging { + rpc GetMessage(GetMessageRequest) returns (Message) { + option (google.api.http) = { + get : "/v1/messages/{message_id}" + }; + } + + rpc GetUserMessage(GetMessageRequest) returns (Message) { + option (google.api.http) = { + get : "/v1/users/{user_id}/messages/{message_id}" + }; + } + + rpc CreateMessage(Message) returns (Message) { + option (google.api.http) = { + post : "/v1/messages/{message_id}" + body : "*" + }; + } +} +message GetMessageRequest { + string message_id = 1; + string user_id = 2; +} + +message Message { + string message_id = 1; + string user_id = 2; + string content = 3; +} \ No newline at end of file diff --git a/apps/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml b/apps/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml new file mode 100644 index 0000000..d398f69 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml @@ -0,0 +1,88 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + version: 0.0.1 +paths: + /v1/messages/{message_id}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + - name: user_id + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + post: + tags: + - Messaging + operationId: Messaging_CreateMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + /v1/users/{user_id}/messages/{message_id}: + get: + tags: + - Messaging + operationId: Messaging_GetUserMessage + parameters: + - name: user_id + in: path + required: true + schema: + type: string + - name: message_id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' +components: + schemas: + Message: + properties: + message_id: + type: string + user_id: + type: string + content: + type: string +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml b/apps/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml new file mode 100644 index 0000000..2d38fd0 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml @@ -0,0 +1,88 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + version: 1.2.3 +paths: + /v1/messages/{messageId}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage + parameters: + - name: messageId + in: path + required: true + schema: + type: string + - name: userId + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + post: + tags: + - Messaging + operationId: Messaging_CreateMessage + parameters: + - name: messageId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + /v1/users/{userId}/messages/{messageId}: + get: + tags: + - Messaging + operationId: Messaging_GetUserMessage + parameters: + - name: userId + in: path + required: true + schema: + type: string + - name: messageId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' +components: + schemas: + Message: + properties: + messageId: + type: string + userId: + type: string + content: + type: string +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/protobuftypes/message.proto b/apps/protoc-gen-openapi/examples/tests/protobuftypes/message.proto new file mode 100644 index 0000000..95a1686 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/protobuftypes/message.proto @@ -0,0 +1,50 @@ +// Copyright 2021 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 tests.protobuftypes.message.v1; + +import "google/api/annotations.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/empty.proto"; + +option go_package = "github.com/google/gnostic/apps/protoc-gen-openapi/examples/tests/protobuftypes/message/v1;message"; + +service Messaging { + rpc CreateMessage(Message) returns (Message) { + option (google.api.http) = { + post : "/v1/messages/{message_id}" + body : "*" + }; + } + rpc GetMessage(Message) returns (Message) { + option (google.api.http) = { + get : "/v1/messages/{message_id}" + }; + } + rpc UpdateMessage(Message) returns (google.protobuf.Struct) { + option (google.api.http) = { + patch : "/v1/messages/{message_id}" + body : "body" + }; + } +} +message Message { + string message_id = 1; + google.protobuf.Struct body = 2; + repeated google.protobuf.Struct media = 3; + google.protobuf.Empty not_used = 4; +} diff --git a/apps/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml b/apps/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml new file mode 100644 index 0000000..3bcc813 --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml @@ -0,0 +1,103 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + version: 0.0.1 +paths: + /v1/messages/{message_id}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + - name: body + in: query + schema: + type: string + - name: media + in: query + schema: + type: string + - name: not_used + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + post: + tags: + - Messaging + operationId: Messaging_CreateMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + patch: + tags: + - Messaging + operationId: Messaging_UpdateMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + - name: media + in: query + schema: + type: string + - name: not_used + in: query + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + required: true + responses: + "200": + description: OK + content: {} +components: + schemas: + Message: + properties: + message_id: + type: string + body: + type: object + media: + type: array + items: + type: object +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml b/apps/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml new file mode 100644 index 0000000..3e52c0b --- /dev/null +++ b/apps/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml @@ -0,0 +1,103 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/apps/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + version: 1.2.3 +paths: + /v1/messages/{messageId}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage + parameters: + - name: messageId + in: path + required: true + schema: + type: string + - name: body + in: query + schema: + type: string + - name: media + in: query + schema: + type: string + - name: notUsed + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + post: + tags: + - Messaging + operationId: Messaging_CreateMessage + parameters: + - name: messageId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + patch: + tags: + - Messaging + operationId: Messaging_UpdateMessage + parameters: + - name: messageId + in: path + required: true + schema: + type: string + - name: media + in: query + schema: + type: string + - name: notUsed + in: query + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + required: true + responses: + "200": + description: OK + content: {} +components: + schemas: + Message: + properties: + messageId: + type: string + body: + type: object + media: + type: array + items: + type: object +tags: + - name: Messaging diff --git a/apps/protoc-gen-openapi/generator/openapi-v3.go b/apps/protoc-gen-openapi/generator/openapi-v3.go index adbc92e..8ff32bb 100644 --- a/apps/protoc-gen-openapi/generator/openapi-v3.go +++ b/apps/protoc-gen-openapi/generator/openapi-v3.go @@ -30,26 +30,39 @@ import ( v3 "github.com/google/gnostic/openapiv3" ) +type Configuration struct { + Version *string + Title *string + Description *string + Naming *string +} + const infoURL = "https://github.com/google/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 { + conf Configuration plugin *protogen.Plugin + singleService bool // 1 file with 1 service 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 + pathPattern *regexp.Regexp + namedPathPattern *regexp.Regexp } // NewOpenAPIv3Generator creates a new generator for a protoc plugin invocation. -func NewOpenAPIv3Generator(plugin *protogen.Plugin) *OpenAPIv3Generator { +func NewOpenAPIv3Generator(plugin *protogen.Plugin, conf Configuration) *OpenAPIv3Generator { return &OpenAPIv3Generator{ - plugin: plugin, + conf: conf, + plugin: plugin, + requiredSchemas: make([]string, 0), generatedSchemas: make([]string, 0), linterRulePattern: regexp.MustCompile(`\(-- .* --\)`), - namePattern: regexp.MustCompile("{(.*)=(.*)}"), + pathPattern: regexp.MustCompile("{([^=}]+)}"), + namedPathPattern: regexp.MustCompile("{(.+)=(.+)}"), } } @@ -68,21 +81,39 @@ func (g *OpenAPIv3Generator) Run() error { // buildDocumentV3 builds an OpenAPIv3 document for a plugin request. func (g *OpenAPIv3Generator) buildDocumentV3() *v3.Document { d := &v3.Document{} + d.Openapi = "3.0.3" d.Info = &v3.Info{ - Title: "", - Version: "0.0.1", - Description: "", + Version: *g.conf.Version, + Title: *g.conf.Title, + Description: *g.conf.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) + if file.Generate { + g.addPathsToDocumentV3(d, file) + } + } + + // If there is only 1 service, then use it's title for the document, + // if the document is missing it. + if len(d.Tags) == 1 { + if d.Info.Title == "" && d.Tags[0].Name != "" { + d.Info.Title = d.Tags[0].Name + " API" + } + if d.Info.Description == "" { + d.Info.Description = d.Tags[0].Description + } + d.Tags[0].Description = "" } + for len(g.requiredSchemas) > 0 { count := len(g.requiredSchemas) for _, file := range g.plugin.Files { @@ -90,6 +121,14 @@ func (g *OpenAPIv3Generator) buildDocumentV3() *v3.Document { } g.requiredSchemas = g.requiredSchemas[count:len(g.requiredSchemas)] } + // Sort the tags. + { + pairs := d.Tags + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + d.Tags = pairs + } // Sort the paths. { pairs := d.Paths.Path @@ -110,9 +149,11 @@ func (g *OpenAPIv3Generator) buildDocumentV3() *v3.Document { } // filterCommentString removes line breaks and linter rules from comments. -func (g *OpenAPIv3Generator) filterCommentString(c protogen.Comments) string { +func (g *OpenAPIv3Generator) filterCommentString(c protogen.Comments, removeNewLines bool) string { comment := string(c) - comment = strings.Replace(comment, "\n", "", -1) + if removeNewLines { + comment = strings.Replace(comment, "\n", "", -1) + } comment = g.linterRulePattern.ReplaceAllString(comment, "") return strings.TrimSpace(comment) } @@ -120,11 +161,11 @@ func (g *OpenAPIv3Generator) filterCommentString(c protogen.Comments) string { // 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 + comment := g.filterCommentString(service.Comments.Leading, false) + d.Tags = append(d.Tags, &v3.Tag{Name: service.GoName, Description: comment}) + for _, method := range service.Methods { - comment := g.filterCommentString(method.Comments.Leading) + comment := g.filterCommentString(method.Comments.Leading, false) inputMessage := method.Input outputMessage := method.Output operationID := service.GoName + "_" + method.GoName @@ -160,17 +201,65 @@ func (g *OpenAPIv3Generator) addPathsToDocumentV3(d *v3.Document, file *protogen } if methodName != "" { op, path2 := g.buildOperationV3( - file, operationID, comment, path, body, inputMessage, outputMessage) + file, operationID, service.GoName, comment, path, body, inputMessage, outputMessage) g.addOperationV3(d, op, path2, methodName) } } } } +func (g *OpenAPIv3Generator) formatMessageRef(name string) string { + if *g.conf.Naming == "proto" { + return name + } + + if len(name) > 1 { + return strings.ToUpper(name[0:1]) + name[1:] + } + + if len(name) == 1 { + return strings.ToLower(name) + } + + return name +} + +func (g *OpenAPIv3Generator) formatMessageName(message *protogen.Message) string { + if *g.conf.Naming == "proto" { + return string(message.Desc.Name()) + } + + name := string(message.Desc.Name()) + if len(name) > 0 { + return strings.ToUpper(name[0:1]) + name[1:] + } + + return name +} + +func (g *OpenAPIv3Generator) formatFieldName(field *protogen.Field) string { + if *g.conf.Naming == "proto" { + return string(field.Desc.Name()) + } + + return field.Desc.JSONName() +} + +func (g *OpenAPIv3Generator) findAndFormatFieldName(name string, inMessage *protogen.Message) string { + for _, field := range inMessage.Fields { + if string(field.Desc.Name()) == name { + return g.formatFieldName(field) + } + } + + return name +} + // buildOperationV3 constructs an operation for a set of values. func (g *OpenAPIv3Generator) buildOperationV3( file *protogen.File, operationID string, + tagName string, description string, path string, bodyField string, @@ -184,36 +273,74 @@ func (g *OpenAPIv3Generator) buildOperationV3( } // 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 { + // Find simple path parameters like {id} + if allMatches := g.pathPattern.FindAllStringSubmatch(path, -1); allMatches != nil { + for _, matches := range allMatches { + // Add the value to the list of covered parameters. + coveredParameters = append(coveredParameters, matches[1]) + pathParameter := g.findAndFormatFieldName(matches[1], inputMessage) + path = strings.Replace(path, matches[1], pathParameter, 1) + pathParameters = append(pathParameters, pathParameter) + } + } + + // 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, + Schema: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "string", + }, + }, + }, + }, + }, + }) + } + + // Build a list of named path parameters. + namedPathParameters := make([]string, 0) + // Find named path parameters like {name=shelves/*} + if matches := g.namedPathPattern.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}". + // We want to convert it to "things/{thingsId}/otherthings/{otherthingsId}". for i := 0; i < len(parts)-1; i += 2 { section := parts[i] - parameter := singular(section) - parts[i+1] = "{" + parameter + "}" - pathParameters = append(pathParameters, parameter) + namedPathParameter := g.findAndFormatFieldName(section, inputMessage) + namedPathParameter = singular(namedPathParameter) + parts[i+1] = "{" + namedPathParameter + "}" + namedPathParameters = append(namedPathParameters, namedPathParameter) } // 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 { + + // Add the named path parameters to the operation parameters. + for _, namedPathParameter := range namedPathParameters { parameters = append(parameters, &v3.ParameterOrReference{ Oneof: &v3.ParameterOrReference_Parameter{ Parameter: &v3.Parameter{ - Name: pathParameter, + Name: namedPathParameter, In: "path", Required: true, - Description: "The " + pathParameter + " id.", + Description: "The " + namedPathParameter + " id.", Schema: &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ Schema: &v3.Schema{ @@ -230,13 +357,14 @@ func (g *OpenAPIv3Generator) buildOperationV3( for _, field := range inputMessage.Fields { fieldName := string(field.Desc.Name()) if !contains(coveredParameters, fieldName) { + bodyFieldName := g.formatFieldName(field) // Get the field description from the comments. - fieldDescription := g.filterCommentString(field.Comments.Leading) + fieldDescription := g.filterCommentString(field.Comments.Leading, true) parameters = append(parameters, &v3.ParameterOrReference{ Oneof: &v3.ParameterOrReference_Parameter{ Parameter: &v3.Parameter{ - Name: fieldName, + Name: bodyFieldName, In: "query", Description: fieldDescription, Required: false, @@ -256,7 +384,7 @@ func (g *OpenAPIv3Generator) buildOperationV3( // Create the response. responses := &v3.Responses{ ResponseOrReference: []*v3.NamedResponseOrReference{ - &v3.NamedResponseOrReference{ + { Name: "200", Value: &v3.ResponseOrReference{ Oneof: &v3.ResponseOrReference_Response{ @@ -271,7 +399,8 @@ func (g *OpenAPIv3Generator) buildOperationV3( } // Create the operation. op := &v3.Operation{ - Summary: description, + Tags: []string{tagName}, + Description: description, OperationId: operationID, Parameters: parameters, Responses: responses, @@ -309,11 +438,24 @@ func (g *OpenAPIv3Generator) buildOperationV3( }, } } else if bodyFieldMessageTypeName != "" { - requestSchema = &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Reference{ - Reference: &v3.Reference{ - XRef: g.schemaReferenceForTypeName(bodyFieldMessageTypeName), - }}, + switch bodyFieldMessageTypeName { + case ".google.protobuf.Empty": + fallthrough + case ".google.protobuf.Struct": + requestSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "object", + }, + }, + } + default: + requestSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{ + XRef: g.schemaReferenceForTypeName(bodyFieldMessageTypeName), + }}, + } } } op.RequestBody = &v3.RequestBodyOrReference{ @@ -322,7 +464,7 @@ func (g *OpenAPIv3Generator) buildOperationV3( Required: true, Content: &v3.MediaTypes{ AdditionalProperties: []*v3.NamedMediaType{ - &v3.NamedMediaType{ + { Name: "application/json", Value: &v3.MediaType{ Schema: requestSchema, @@ -373,12 +515,12 @@ func (g *OpenAPIv3Generator) schemaReferenceForTypeName(typeName string) string } parts := strings.Split(typeName, ".") lastPart := parts[len(parts)-1] - return "#/components/schemas/" + lastPart + return "#/components/schemas/" + g.formatMessageRef(lastPart) } // itemsItemForTypeName is a helper constructor. func itemsItemForTypeName(typeName string) *v3.ItemsItem { - return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{&v3.SchemaOrReference{ + return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{{ Oneof: &v3.SchemaOrReference_Schema{ Schema: &v3.Schema{ Type: typeName}}}}} @@ -386,7 +528,7 @@ func itemsItemForTypeName(typeName string) *v3.ItemsItem { // itemsItemForReference is a helper constructor. func itemsItemForReference(xref string) *v3.ItemsItem { - return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{&v3.SchemaOrReference{ + return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{{ Oneof: &v3.SchemaOrReference_Reference{ Reference: &v3.Reference{ XRef: xref}}}}} @@ -403,11 +545,14 @@ func (g *OpenAPIv3Generator) responseContentForMessage(outputMessage *protogen.M if typeName == ".google.protobuf.Empty" { return &v3.MediaTypes{} } + if typeName == ".google.protobuf.Struct" { + return &v3.MediaTypes{} + } if typeName == ".google.api.HttpBody" { return &v3.MediaTypes{ AdditionalProperties: []*v3.NamedMediaType{ - &v3.NamedMediaType{ + { Name: "application/octet-stream", Value: &v3.MediaType{}, }, @@ -417,7 +562,7 @@ func (g *OpenAPIv3Generator) responseContentForMessage(outputMessage *protogen.M return &v3.MediaTypes{ AdditionalProperties: []*v3.NamedMediaType{ - &v3.NamedMediaType{ + { Name: "application/json", Value: &v3.MediaType{ Schema: &v3.SchemaOrReference{ @@ -445,7 +590,7 @@ func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protog } g.generatedSchemas = append(g.generatedSchemas, typeName) // Get the message description from the comments. - messageDescription := g.filterCommentString(message.Comments.Leading) + messageDescription := g.filterCommentString(message.Comments.Leading, true) // Build an array holding the fields of the message. definitionProperties := &v3.Properties{ AdditionalProperties: make([]*v3.NamedSchemaOrReference, 0), @@ -467,7 +612,7 @@ func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protog } } // Get the field description from the comments. - fieldDescription := g.filterCommentString(field.Comments.Leading) + fieldDescription := g.filterCommentString(field.Comments.Leading, true) // The field is either described by a reference or a schema. XRef := "" fieldSchema := &v3.Schema{ @@ -480,9 +625,28 @@ func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protog fieldSchema.Type = "array" switch field.Desc.Kind() { case protoreflect.MessageKind: - fieldSchema.Items = itemsItemForReference( - g.schemaReferenceForTypeName( - fullMessageTypeName(field.Message))) + typeName := fullMessageTypeName(field.Message) + switch typeName { + case ".google.protobuf.Timestamp": + // Timestamps are serialized as strings + fieldSchema.Items = itemsItemForTypeName("string") + case ".google.type.Date": + // Dates are serialized as strings + fieldSchema.Items = itemsItemForTypeName("string") + case ".google.type.DateTime": + // DateTimes are serialized as strings + fieldSchema.Items = itemsItemForTypeName("string") + case ".google.protobuf.Struct": + // Struct is equivalent to a JSON object + fieldSchema.Items = itemsItemForTypeName("object") + case ".google.protobuf.Empty": + // Struct is close to JSON null, so ignore this field + continue + default: + // The field is described by a reference. + fieldSchema.Items = itemsItemForReference( + g.schemaReferenceForTypeName(typeName)) + } case protoreflect.StringKind: fieldSchema.Items = itemsItemForTypeName("string") case protoreflect.Int32Kind, @@ -529,6 +693,12 @@ func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protog // DateTimes are serialized as strings fieldSchema.Type = "string" fieldSchema.Format = "date-time" + case ".google.protobuf.Struct": + // Struct is equivalent to a JSON object + fieldSchema.Type = "object" + case ".google.protobuf.Empty": + // Struct is close to JSON null, so ignore this field + continue default: // The field is described by a reference. XRef = g.schemaReferenceForTypeName(typeName) @@ -581,7 +751,7 @@ func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protog definitionProperties.AdditionalProperties = append( definitionProperties.AdditionalProperties, &v3.NamedSchemaOrReference{ - Name: string(field.Desc.Name()), + Name: g.formatFieldName(field), Value: value, }, ) @@ -589,7 +759,7 @@ func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protog // Add the schema to the components.schema list. d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, &v3.NamedSchemaOrReference{ - Name: string(message.Desc.Name()), + Name: g.formatMessageName(message), Value: &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ Schema: &v3.Schema{ diff --git a/apps/protoc-gen-openapi/main.go b/apps/protoc-gen-openapi/main.go index af621d6..735e607 100644 --- a/apps/protoc-gen-openapi/main.go +++ b/apps/protoc-gen-openapi/main.go @@ -16,13 +16,27 @@ package main import ( - "google.golang.org/protobuf/compiler/protogen" + "flag" "github.com/google/gnostic/apps/protoc-gen-openapi/generator" + "google.golang.org/protobuf/compiler/protogen" ) +var flags flag.FlagSet + func main() { - protogen.Options{}.Run(func(plugin *protogen.Plugin) error { - return generator.NewOpenAPIv3Generator(plugin).Run() + conf := generator.Configuration{ + Version: flags.String("version", "0.0.1", "version number text, e.g. 1.2.3"), + Title: flags.String("title", "", "name of the API"), + Description: flags.String("description", "", "description of the API"), + Naming: flags.String("naming", "json", `naming convention. Use "proto" for passing names directly from the proto files`), + } + + opts := protogen.Options{ + ParamFunc: flags.Set, + } + + opts.Run(func(plugin *protogen.Plugin) error { + return generator.NewOpenAPIv3Generator(plugin, conf).Run() }) } diff --git a/apps/protoc-gen-openapi/plugin_test.go b/apps/protoc-gen-openapi/plugin_test.go index 60b6afe..00e34f0 100644 --- a/apps/protoc-gen-openapi/plugin_test.go +++ b/apps/protoc-gen-openapi/plugin_test.go @@ -17,68 +17,67 @@ package main import ( "os" "os/exec" + "path" "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", "../../", - "-I", "../../third_party", - "-I", "examples", - "examples/google/example/library/v1/library.proto", - "--openapi_out=.").Run() - if err != nil { - t.Fatalf("protoc failed: %+v", err) - } - // 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.Fatalf("Diff failed: %+v", err) - } - // if the test succeeded, clean up - os.Remove("openapi.yaml") +var openapiTests = []struct { + name string + path string + protofile string +}{ + {name: "Google Library example", path: "examples/google/example/library/v1/", protofile: "library.proto"}, + {name: "Body mapping", path: "examples/tests/bodymapping/", protofile: "message.proto"}, + {name: "Map fields", path: "examples/tests/mapfields/", protofile: "message.proto"}, + {name: "Path params", path: "examples/tests/pathparams/", protofile: "message.proto"}, + {name: "Protobuf types", path: "examples/tests/protobuftypes/", protofile: "message.proto"}, + {name: "JSON options", path: "examples/tests/jsonoptions/", protofile: "message.proto"}, } -func TestBodyMappingOpenAPI(t *testing.T) { - var err error - // Run protoc and the protoc-gen-openapi plugin to generate an OpenAPI spec. - err = exec.Command("protoc", - "-I", "../../", - "-I", "../../third_party", - "-I", "examples", - "examples/tests/bodymapping/message.proto", - "--openapi_out=.").Run() - if err != nil { - t.Fatalf("protoc failed: %+v", err) +func TestOpenAPIProtobufNaming(t *testing.T) { + for _, tt := range openapiTests { + t.Run(tt.name, func(t *testing.T) { + // Run protoc and the protoc-gen-openapi plugin to generate an OpenAPI spec. + err := exec.Command("protoc", + "-I", "../../", + "-I", "../../third_party", + "-I", "examples", + path.Join(tt.path, tt.protofile), + "--openapi_out=naming=proto:.").Run() + if err != nil { + t.Fatalf("protoc failed: %+v", err) + } + // Verify that the generated spec matches our expected version. + err = exec.Command("diff", "openapi.yaml", path.Join(tt.path, "openapi.yaml")).Run() + if err != nil { + t.Fatalf("Diff failed: %+v", err) + } + // if the test succeeded, clean up + os.Remove("openapi.yaml") + }) } - // Verify that the generated spec matches our expected version. - err = exec.Command("diff", "openapi.yaml", "examples/tests/bodymapping/openapi.yaml").Run() - if err != nil { - t.Fatalf("Diff failed: %+v", err) - } - // if the test succeeded, clean up - os.Remove("openapi.yaml") } -func TestMapFieldsOpenAPI(t *testing.T) { - var err error - // Run protoc and the protoc-gen-openapi plugin to generate an OpenAPI spec. - err = exec.Command("protoc", - "-I", "../../", - "-I", "../../third_party", - "-I", "examples", - "examples/tests/mapfields/message.proto", - "--openapi_out=.").Run() - if err != nil { - t.Fatalf("protoc failed: %+v", err) - } - // Verify that the generated spec matches our expected version. - err = exec.Command("diff", "openapi.yaml", "examples/tests/mapfields/openapi.yaml").Run() - if err != nil { - t.Fatalf("Diff failed: %+v", err) +func TestOpenAPIJSONNaming(t *testing.T) { + for _, tt := range openapiTests { + t.Run(tt.name, func(t *testing.T) { + // Run protoc and the protoc-gen-openapi plugin to generate an OpenAPI spec with JSON naming. + err := exec.Command("protoc", + "-I", "../../", + "-I", "../../third_party", + "-I", "examples", + path.Join(tt.path, tt.protofile), + "--openapi_out=version=1.2.3:.").Run() + if err != nil { + t.Fatalf("protoc failed: %+v", err) + } + // Verify that the generated spec matches our expected version. + err = exec.Command("diff", "openapi.yaml", path.Join(tt.path, "openapi_json.yaml")).Run() + if err != nil { + t.Fatalf("Diff failed: %+v", err) + } + // if the test succeeded, clean up + os.Remove("openapi.yaml") + }) } - // if the test succeeded, clean up - os.Remove("openapi.yaml") }