diff --git a/BUILD.bazel b/BUILD.bazel index 64f651e9960..38a13b5f203 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -39,6 +39,7 @@ package_group( packages = [ "//protoc-gen-grpc-gateway/...", "//protoc-gen-openapiv2/...", + "//protoc-gen-openapiv3/...", ], ) diff --git a/Makefile b/Makefile index c8791a64f37..0c73b8b0c13 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,8 @@ install: go install github.com/bufbuild/buf/cmd/buf@v1.45.0 go install \ ./protoc-gen-openapiv2 \ - ./protoc-gen-grpc-gateway + ./protoc-gen-grpc-gateway \ + ./protoc-gen-openapiv3 proto: # These generation steps are run in order so that later steps can diff --git a/buf.gen.openapiv3.yaml b/buf.gen.openapiv3.yaml new file mode 100644 index 00000000000..9344f188dcd --- /dev/null +++ b/buf.gen.openapiv3.yaml @@ -0,0 +1,4 @@ +version: v2 +plugins: + - local: protoc-gen-openapiv3 + out: . diff --git a/examples/internal/proto/examplepb/echo_service.swagger.json b/examples/internal/proto/examplepb/echo_service.swagger.json index 4270e770ab2..1e36c4a8e67 100644 --- a/examples/internal/proto/examplepb/echo_service.swagger.json +++ b/examples/internal/proto/examplepb/echo_service.swagger.json @@ -1,1227 +1,847 @@ { - "swagger": "2.0", + "openapi": "3.0.0", "info": { - "title": "Echo Service", - "description": "Echo Service API consists of a single service which returns\na message.", - "version": "version not set" + "title": "examples/internal/proto/examplepb/echo_service.proto", + "version": "0.0.1" }, - "tags": [ - { - "name": "EchoService" - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], "paths": { - "/v1/example/echo/nested/{nId.nId}": { + "/v1/example/echo/nested/{n_id.n_id}": { "get": { - "summary": "Echo method receives a simple message and returns it.", - "description": "The message posted as the id parameter will also be\nreturned.", - "operationId": "EchoService_Echo7", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, + "summary": "Echo", "parameters": [ { - "name": "nId.nId", + "name": "n_id.n_id", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "id", - "description": "Id represents the message identifier.", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "lineNum", + "name": "line_num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "query", - "required": false, - "type": "string" - }, - { - "name": "status.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string" + } }, { - "name": "status.note", + "name": "status", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { "name": "en", "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "no.note", + "name": "no", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { - "name": "resourceId", + "name": "resource_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "nId.val", + "name": "n_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } } ], - "tags": [ - "EchoService" - ] + "responses": {} } }, - "/v1/example/echo/resource/{resourceId}": { + "/v1/example/echo/resource/{resource_id}": { "get": { - "summary": "Echo method receives a simple message and returns it.", - "description": "The message posted as the id parameter will also be\nreturned.", - "operationId": "EchoService_Echo6", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, + "summary": "Echo", "parameters": [ { - "name": "resourceId", + "name": "resource_id", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "id", - "description": "Id represents the message identifier.", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "lineNum", + "name": "line_num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "query", - "required": false, - "type": "string" - }, - { - "name": "status.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string" + } }, { - "name": "status.note", + "name": "status", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { "name": "en", "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.note", - "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "nId.nId", + "name": "no", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { - "name": "nId.val", + "name": "n_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } } ], - "tags": [ - "EchoService" - ] + "responses": {} } }, "/v1/example/echo/{id}": { "post": { - "summary": "Echo method receives a simple message and returns it.", - "description": "The message posted as the id parameter will also be\nreturned.", - "operationId": "EchoService_Echo", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, + "summary": "Echo", "parameters": [ { "name": "id", - "description": "Id represents the message identifier.", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "lineNum", + "name": "line_num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "query", - "required": false, - "type": "string" - }, - { - "name": "status.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string" + } }, { - "name": "status.note", + "name": "status", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { "name": "en", "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.note", - "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "resourceId", + "name": "no", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { - "name": "nId.nId", + "name": "resource_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "nId.val", + "name": "n_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } } ], - "tags": [ - "EchoService" - ] + "responses": {} } }, "/v1/example/echo/{id}/{num}": { "get": { - "summary": "Echo method receives a simple message and returns it.", - "description": "The message posted as the id parameter will also be\nreturned.", - "operationId": "EchoService_Echo2", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, + "summary": "Echo", "parameters": [ { "name": "id", - "description": "Id represents the message identifier.", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "num", "in": "path", "required": true, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "lineNum", + "name": "line_num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "query", - "required": false, - "type": "string" - }, - { - "name": "status.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string" + } }, { - "name": "status.note", + "name": "status", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { "name": "en", "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.note", - "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "resourceId", + "name": "no", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { - "name": "nId.nId", + "name": "resource_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "nId.val", + "name": "n_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } } ], - "tags": [ - "EchoService" - ] + "responses": {} } }, "/v1/example/echo/{id}/{num}/{lang}": { "get": { - "summary": "Echo method receives a simple message and returns it.", - "description": "The message posted as the id parameter will also be\nreturned.", - "operationId": "EchoService_Echo3", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, + "summary": "Echo", "parameters": [ { "name": "id", - "description": "Id represents the message identifier.", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "num", "in": "path", "required": true, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "lineNum", + "name": "line_num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "status.progress", + "name": "status", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "object" + } }, { - "name": "status.note", + "name": "en", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "en", + "name": "no", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "object" + } }, { - "name": "no.progress", + "name": "resource_id", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string" + } }, { - "name": "no.note", - "in": "query", - "required": false, - "type": "string" - }, - { - "name": "resourceId", - "in": "query", - "required": false, - "type": "string" - }, - { - "name": "nId.nId", - "in": "query", - "required": false, - "type": "string" - }, - { - "name": "nId.val", + "name": "n_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } } ], - "tags": [ - "EchoService" - ] + "responses": {} } }, - "/v1/example/echo1/{id}/{lineNum}/{status.note}": { + "/v1/example/echo1/{id}/{line_num}/{status.note}": { "get": { - "summary": "Echo method receives a simple message and returns it.", - "description": "The message posted as the id parameter will also be\nreturned.", - "operationId": "EchoService_Echo4", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, + "summary": "Echo", "parameters": [ { "name": "id", - "description": "Id represents the message identifier.", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "lineNum", + "name": "line_num", "in": "path", "required": true, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "status.note", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "status.progress", + "name": "status", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "object" + } }, { "name": "en", "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "resourceId", + "name": "no", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { - "name": "nId.nId", + "name": "resource_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "nId.val", + "name": "n_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } } ], - "tags": [ - "EchoService" - ] + "responses": {} } }, "/v1/example/echo2/{no.note}": { "get": { - "summary": "Echo method receives a simple message and returns it.", - "description": "The message posted as the id parameter will also be\nreturned.", - "operationId": "EchoService_Echo5", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, + "summary": "Echo", "parameters": [ { "name": "no.note", "in": "path", "required": true, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "id", - "description": "Id represents the message identifier.", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "lineNum", + "name": "line_num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "status.progress", + "name": "status", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "object" + } }, { "name": "en", "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "resourceId", + "name": "no", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { - "name": "nId.nId", + "name": "resource_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "nId.val", + "name": "n_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } } ], - "tags": [ - "EchoService" - ] + "responses": {} } }, "/v1/example/echo_body": { "post": { - "summary": "EchoBody method receives a simple message and returns it.", - "operationId": "EchoService_EchoBody", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" + "summary": "EchoBody", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } } } }, + "responses": {} + } + }, + "/v1/example/echo_body/{id}": { + "put": { + "summary": "EchoBody", "parameters": [ { - "name": "body", - "description": "SimpleMessage represents a simple message sent to the Echo service.", - "in": "body", + "name": "id", + "in": "path", "required": true, "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" + "type": "string" } } ], - "tags": [ - "EchoService" - ] - } - }, - "/v1/example/echo_body/{id}": { - "put": { - "summary": "EchoBody method receives a simple message and returns it.", - "operationId": "EchoService_EchoBody2", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } } } }, + "responses": {} + } + }, + "/v1/example/echo_delete": { + "delete": { + "summary": "EchoDelete", "parameters": [ { "name": "id", - "description": "Id represents the message identifier.", - "in": "path", - "required": true, - "type": "string" - }, - { - "name": "no", - "in": "body", - "required": true, + "in": "query", "schema": { - "$ref": "#/definitions/examplepbEmbedded" + "type": "string" } }, { "name": "num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "lineNum", + "name": "line_num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "status.progress", + "name": "status", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "object" + } }, { - "name": "status.note", + "name": "en", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "resourceId", + "name": "no", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { - "name": "nId.nId", + "name": "resource_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { - "name": "nId.val", + "name": "n_id", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } } ], - "tags": [ - "EchoService" - ] + "responses": {} } }, - "/v1/example/echo_delete": { - "delete": { - "summary": "EchoDelete method receives a simple message and returns it.", - "operationId": "EchoService_EchoDelete", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" + "/v1/example/echo_patch": { + "patch": { + "summary": "EchoPatch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } } } }, + "responses": {} + } + }, + "/v1/example/echo_unauthorized": { + "get": { + "summary": "EchoUnauthorized", "parameters": [ { "name": "id", - "description": "Id represents the message identifier.", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string" + } }, { "name": "num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "lineNum", + "name": "line_num", "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string", + "format": "int64" + } }, { "name": "lang", "in": "query", - "required": false, - "type": "string" - }, - { - "name": "status.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" + "schema": { + "type": "string" + } }, { - "name": "status.note", + "name": "status", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { "name": "en", "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "no.note", - "in": "query", - "required": false, - "type": "string" - }, - { - "name": "resourceId", - "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "string", + "format": "int64" + } }, { - "name": "nId.nId", + "name": "no", "in": "query", - "required": false, - "type": "string" + "schema": { + "type": "object" + } }, { - "name": "nId.val", + "name": "resource_id", "in": "query", - "required": false, - "type": "string" - } - ], - "tags": [ - "EchoService" - ] - } - }, - "/v1/example/echo_patch": { - "patch": { - "summary": "EchoPatch method receives a NonStandardUpdateRequest and returns it.", - "operationId": "EchoService_EchoPatch", - "responses": { - "200": { - "description": "A successful response.", "schema": { - "$ref": "#/definitions/examplepbDynamicMessageUpdate" + "type": "string" } }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, - "parameters": [ { - "name": "body", - "in": "body", - "required": true, + "name": "n_id", + "in": "query", "schema": { - "$ref": "#/definitions/examplepbDynamicMessage" + "type": "object" } } ], - "tags": [ - "EchoService" - ] + "responses": {} } - }, - "/v1/example/echo_unauthorized": { - "get": { - "summary": "EchoUnauthorized method receives a simple message and returns it. It must\nalways return a google.rpc.Code of `UNAUTHENTICATED` and a HTTP Status code\nof 401.", - "operationId": "EchoService_EchoUnauthorized", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/examplepbSimpleMessage" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } + } + }, + "components": { + "schemas": { + "DynamicMessage": { + "type": "object", + "properties": { + "struct_field": { + "type": "object" + }, + "value_field": { + "type": "object" } - }, - "parameters": [ - { - "name": "id", - "description": "Id represents the message identifier.", - "in": "query", - "required": false, + } + }, + "DynamicMessageUpdate": { + "type": "object", + "properties": { + "body": { + "type": "object" + }, + "update_mask": { + "type": "object" + } + } + }, + "Embedded": { + "type": "object", + "properties": { + "note": { "type": "string" }, - { - "name": "num", - "in": "query", - "required": false, + "progress": { "type": "string", "format": "int64" + } + } + }, + "NestedMessage": { + "type": "object", + "properties": { + "n_id": { + "type": "string" }, - { - "name": "lineNum", - "in": "query", - "required": false, + "val": { + "type": "string" + } + } + }, + "SimpleMessage": { + "type": "object", + "properties": { + "en": { "type": "string", "format": "int64" }, - { - "name": "lang", - "in": "query", - "required": false, + "id": { "type": "string" }, - { - "name": "status.progress", - "in": "query", - "required": false, - "type": "string", - "format": "int64" - }, - { - "name": "status.note", - "in": "query", - "required": false, + "lang": { "type": "string" }, - { - "name": "en", - "in": "query", - "required": false, + "line_num": { "type": "string", "format": "int64" }, - { - "name": "no.progress", - "in": "query", - "required": false, + "n_id": { + "type": "object" + }, + "no": { + "type": "object" + }, + "num": { "type": "string", "format": "int64" }, - { - "name": "no.note", - "in": "query", - "required": false, - "type": "string" - }, - { - "name": "resourceId", - "in": "query", - "required": false, + "resource_id": { "type": "string" }, - { - "name": "nId.nId", - "in": "query", - "required": false, - "type": "string" - }, - { - "name": "nId.val", - "in": "query", - "required": false, - "type": "string" + "status": { + "type": "object" } - ], - "tags": [ - "EchoService" - ] - } - } - }, - "definitions": { - "examplepbDynamicMessage": { - "type": "object", - "properties": { - "structField": { - "type": "object" - }, - "valueField": {} - }, - "description": "DynamicMessage represents a message which can have its structure\nbuilt dynamically using Struct and Values." - }, - "examplepbDynamicMessageUpdate": { - "type": "object", - "properties": { - "body": { - "$ref": "#/definitions/examplepbDynamicMessage" - }, - "updateMask": { - "type": "string" - } - } - }, - "examplepbEmbedded": { - "type": "object", - "properties": { - "progress": { - "type": "string", - "format": "int64" - }, - "note": { - "type": "string" - } - }, - "description": "Embedded represents a message embedded in SimpleMessage." - }, - "examplepbNestedMessage": { - "type": "object", - "properties": { - "nId": { - "type": "string" - }, - "val": { - "type": "string" } } - }, - "examplepbSimpleMessage": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Id represents the message identifier." - }, - "num": { - "type": "string", - "format": "int64" - }, - "lineNum": { - "type": "string", - "format": "int64" - }, - "lang": { - "type": "string" - }, - "status": { - "$ref": "#/definitions/examplepbEmbedded" - }, - "en": { - "type": "string", - "format": "int64" - }, - "no": { - "$ref": "#/definitions/examplepbEmbedded" - }, - "resourceId": { - "type": "string" - }, - "nId": { - "$ref": "#/definitions/examplepbNestedMessage" - } - }, - "description": "SimpleMessage represents a simple message sent to the Echo service." - }, - "protobufAny": { - "type": "object", - "properties": { - "@type": { - "type": "string", - "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com. As of May 2023, there are no widely used type server\nimplementations and no plans to implement one.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." - } - }, - "additionalProperties": {}, - "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n // or ...\n if (any.isSameTypeAs(Foo.getDefaultInstance())) {\n foo = any.unpack(Foo.getDefaultInstance());\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := \u0026pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" - }, - "protobufNullValue": { - "type": "string", - "enum": [ - "NULL_VALUE" - ], - "default": "NULL_VALUE", - "description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\nThe JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value." - }, - "rpcStatus": { - "type": "object", - "properties": { - "code": { - "type": "integer", - "format": "int32", - "description": "The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]." - }, - "message": { - "type": "string", - "description": "A developer-facing error message, which should be in English. Any\nuser-facing error message should be localized and sent in the\n[google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client." - }, - "details": { - "type": "array", - "items": { - "type": "object", - "$ref": "#/definitions/protobufAny" - }, - "description": "A list of messages that carry the error details. There is a common set of\nmessage types for APIs to use." - } - }, - "description": "The `Status` type defines a logical error model that is suitable for\ndifferent programming environments, including REST APIs and RPC APIs. It is\nused by [gRPC](https://github.com/grpc). Each `Status` message contains\nthree pieces of data: error code, error message, and error details.\n\nYou can find out more about this error model and how to work with it in the\n[API Design Guide](https://cloud.google.com/apis/design/errors)." } } -} +} \ No newline at end of file diff --git a/protoc-gen-openapiv3/BUILD.bazel b/protoc-gen-openapiv3/BUILD.bazel new file mode 100644 index 00000000000..a3994dc2c1f --- /dev/null +++ b/protoc-gen-openapiv3/BUILD.bazel @@ -0,0 +1,21 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +package(default_visibility = ["//visibility:private"]) + +go_library( + name = "protoc-gen-openapiv3_lib", + srcs = ["main.go"], + importpath = "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv3", + deps = [ + "//internal/descriptor", + "//protoc-gen-openapiv3/internal/genopenapi", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//types/pluginpb", + ], +) + +go_binary( + name = "protoc-gen-openapiv3", + embed = [":protoc-gen-openapiv3_lib"], + visibility = ["//visibility:public"], +) diff --git a/protoc-gen-openapiv3/internal/genopenapi/generator.go b/protoc-gen-openapiv3/internal/genopenapi/generator.go new file mode 100644 index 00000000000..8b43c87366f --- /dev/null +++ b/protoc-gen-openapiv3/internal/genopenapi/generator.go @@ -0,0 +1,242 @@ +package genopenapi + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" +) + +type generator struct{} + +// New returns a new generator which generates OpenAPI v3 files. +func New() *generator { + return &generator{} +} + +// Generate generates OpenAPI v3 files from the given CodeGeneratorRequest. +func (g *generator) Generate(req *pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorResponse, error) { + reg := descriptor.NewRegistry() + if err := reg.Load(req); err != nil { + return nil, fmt.Errorf("failed to load registry: %w", err) + } + + var files []*pluginpb.CodeGeneratorResponse_File + for _, file := range req.FileToGenerate { + f, err := reg.LookupFile(file) + if err != nil { + return nil, fmt.Errorf("failed to lookup file %q: %w", file, err) + } + + out, err := g.generate(f) + if err != nil { + return nil, fmt.Errorf("failed to generate openapi v3 for file %q: %w", file, err) + } + files = append(files, out) + } + + return &pluginpb.CodeGeneratorResponse{ + File: files, + }, nil +} + +func (g *generator) generate(file *descriptor.File) (*pluginpb.CodeGeneratorResponse_File, error) { + openapi := &OpenAPI{ + OpenAPI: "3.0.0", + Info: &Info{ + Title: file.GetName(), + Version: "0.0.1", + }, + Paths: &Paths{ + PathItems: make(map[string]*PathItem), + }, + Components: &Components{ + Schemas: make(map[string]*Schema), + }, + } + + g.renderMessagesAsDefinition(file, openapi.Components.Schemas) + + for _, svc := range file.Services { + for _, meth := range svc.Methods { + for _, b := range meth.Bindings { + pathItem := &PathItem{} + op := &Operation{ + Summary: meth.GetName(), + Responses: &Responses{}, + } + + var params []*Parameter + for _, pathParam := range b.PathParams { + params = append(params, &Parameter{ + Name: pathParam.FieldPath.String(), + In: "path", + Required: true, + Schema: schemaOfField(pathParam.Target), + }) + } + + if b.Body != nil { + var schema *Schema + if len(b.Body.FieldPath) > 0 { + schema = schemaOfField(b.Body.FieldPath[0].Target) + } else { + // TODO(ivucica): This should be a reference to a schema in components/schemas + schema = &Schema{Type: "object"} + } + op.RequestBody = &RequestBody{ + Content: map[string]*MediaType{ + "application/json": { + Schema: schema, + }, + }, + } + } else { + for _, field := range meth.RequestType.Fields { + if isPathParameter(field, b.PathParams) { + continue + } + params = append(params, &Parameter{ + Name: field.GetName(), + In: "query", + Schema: schemaOfField(field), + }) + } + } + op.Parameters = params + op.Responses = &Responses{ + Responses: map[string]*Response{ + "200": { + Description: "A successful response.", + Content: map[string]*MediaType{ + "application/json": { + Schema: &Schema{ + Ref: "#/components/schemas/" + meth.ResponseType.GetName(), + }, + }, + }, + }, + }, + } + + switch b.HTTPMethod { + case "GET": + pathItem.Get = op + case "POST": + pathItem.Post = op + case "PUT": + pathItem.Put = op + case "DELETE": + pathItem.Delete = op + case "PATCH": + pathItem.Patch = op + } + + openapi.Paths.PathItems[b.PathTmpl.Template] = pathItem + } + } + } + + b, err := json.MarshalIndent(openapi, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal openapi v3: %w", err) + } + + name := file.GetName() + ext := ".swagger.json" + if strings.HasSuffix(name, ".proto") { + name = name[:len(name)-len(".proto")] + } + name += ext + + return &pluginpb.CodeGeneratorResponse_File{ + Name: proto.String(name), + Content: proto.String(string(b)), + }, nil +} + +func isPathParameter(field *descriptor.Field, pathParams []descriptor.Parameter) bool { + for _, p := range pathParams { + if p.FieldPath.String() == field.GetName() { + return true + } + } + return false +} + +func (g *generator) renderMessagesAsDefinition(file *descriptor.File, schemas map[string]*Schema) { + for _, msg := range file.Messages { + schemas[msg.GetName()] = g.schemaOfMessage(msg) + } +} + +func (g *generator) schemaOfMessage(msg *descriptor.Message) *Schema { + s := &Schema{ + Type: "object", + Properties: make(map[string]*Schema), + } + for _, field := range msg.Fields { + s.Properties[field.GetName()] = schemaOfField(field) + } + return s +} + +func schemaOfField(field *descriptor.Field) *Schema { + s := &Schema{} + switch field.GetType() { + case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE: + s.Type = "number" + s.Format = "double" + case descriptorpb.FieldDescriptorProto_TYPE_FLOAT: + s.Type = "number" + s.Format = "float" + case descriptorpb.FieldDescriptorProto_TYPE_INT64: + s.Type = "string" + s.Format = "int64" + case descriptorpb.FieldDescriptorProto_TYPE_UINT64: + s.Type = "string" + s.Format = "uint64" + case descriptorpb.FieldDescriptorProto_TYPE_INT32: + s.Type = "integer" + s.Format = "int32" + case descriptorpb.FieldDescriptorProto_TYPE_FIXED64: + s.Type = "string" + s.Format = "uint64" + case descriptorpb.FieldDescriptorProto_TYPE_FIXED32: + s.Type = "integer" + s.Format = "int64" + case descriptorpb.FieldDescriptorProto_TYPE_BOOL: + s.Type = "boolean" + case descriptorpb.FieldDescriptorProto_TYPE_STRING: + s.Type = "string" + case descriptorpb.FieldDescriptorProto_TYPE_BYTES: + s.Type = "string" + s.Format = "byte" + case descriptorpb.FieldDescriptorProto_TYPE_UINT32: + s.Type = "integer" + s.Format = "int64" + case descriptorpb.FieldDescriptorProto_TYPE_SFIXED32: + s.Type = "integer" + s.Format = "int32" + case descriptorpb.FieldDescriptorProto_TYPE_SFIXED64: + s.Type = "string" + s.Format = "int64" + case descriptorpb.FieldDescriptorProto_TYPE_SINT32: + s.Type = "integer" + s.Format = "int32" + case descriptorpb.FieldDescriptorProto_TYPE_SINT64: + s.Type = "string" + s.Format = "int64" + case descriptorpb.FieldDescriptorProto_TYPE_MESSAGE: + // TODO(ivucica): handle message types + s.Type = "object" + case descriptorpb.FieldDescriptorProto_TYPE_ENUM: + // TODO(ivucica): handle enum types + s.Type = "string" + } + return s +} diff --git a/protoc-gen-openapiv3/internal/genopenapi/template.go b/protoc-gen-openapiv3/internal/genopenapi/template.go new file mode 100644 index 00000000000..6eb675bcc16 --- /dev/null +++ b/protoc-gen-openapiv3/internal/genopenapi/template.go @@ -0,0 +1,4 @@ +package genopenapi + +// This file is a placeholder for template-related logic. +// For the minimal implementation, all logic is in generator.go. diff --git a/protoc-gen-openapiv3/internal/genopenapi/types.go b/protoc-gen-openapiv3/internal/genopenapi/types.go new file mode 100644 index 00000000000..35a5cf3b096 --- /dev/null +++ b/protoc-gen-openapiv3/internal/genopenapi/types.go @@ -0,0 +1,318 @@ +package genopenapi + +import "encoding/json" + +// OpenAPI is the root document object of the OpenAPI document. +type OpenAPI struct { + OpenAPI string `json:"openapi" yaml:"openapi"` + Info *Info `json:"info" yaml:"info"` + Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` + Paths *Paths `json:"paths" yaml:"paths"` + Components *Components `json:"components,omitempty" yaml:"components,omitempty"` + Security []*SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"` + Tags []*Tag `json:"tags,omitempty" yaml:"tags,omitempty"` + ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` +} + +// Info provides metadata about the API. +type Info struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` + Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` + License *License `json:"license,omitempty" yaml:"license,omitempty"` + Version string `json:"version" yaml:"version"` +} + +// Contact information for the exposed API. +type Contact struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` + Email string `json:"email,omitempty" yaml:"email,omitempty"` +} + +// License information for the exposed API. +type License struct { + Name string `json:"name" yaml:"name"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` +} + +// Server represents a Server. +type Server struct { + URL string `json:"url" yaml:"url"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` +} + +// ServerVariable is an object representing a Server Variable for server URL template substitution. +type ServerVariable struct { + Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` + Default string `json:"default" yaml:"default"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` +} + +// Components holds a set of reusable objects for different aspects of the OAS. +type Components struct { + Schemas map[string]*Schema `json:"schemas,omitempty" yaml:"schemas,omitempty"` + Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` + Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` + RequestBodies map[string]*RequestBody `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + SecuritySchemes map[string]*SecurityScheme `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` + Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"` + Callbacks map[string]*Callback `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` +} + +// Paths holds the relative paths to the individual endpoints and their operations. +type Paths struct { + // Using map[string]*PathItem instead of a custom object to handle patterned fields + PathItems map[string]*PathItem `json:"-" yaml:"-"` +} + +// MarshalJSON implements json.Marshaler. +func (p Paths) MarshalJSON() ([]byte, error) { + return json.Marshal(p.PathItems) +} + +// PathItem describes the operations available on a single path. +type PathItem struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` + Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` + Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` + Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` + Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` + Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` + Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` + Trace *Operation `json:"trace,omitempty" yaml:"trace,omitempty"` + Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` + Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` +} + +// Operation describes a single API operation on a path. +type Operation struct { + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + RequestBody *RequestBody `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` + Responses *Responses `json:"responses" yaml:"responses"` + Callbacks map[string]*Callback `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Security []*SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"` + Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` +} + +// ExternalDocumentation allows referencing an external resource for extended documentation. +type ExternalDocumentation struct { + Description string `json:"description,omitempty" yaml:"description,omitempty"` + URL string `json:"url" yaml:"url"` +} + +// Parameter describes a single operation parameter. +type Parameter struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Name string `json:"name" yaml:"name"` + In string `json:"in" yaml:"in"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` + Content map[string]*MediaType `json:"content,omitempty" yaml:"content,omitempty"` +} + +// Tag adds metadata to a single tag that is used by the Operation Object. +type Tag struct { + Name string `json:"name" yaml:"name"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` +} + +// Reference is a simple object to allow referencing other components in the specification, internally and externally. +type Reference struct { + Ref string `json:"$ref" yaml:"$ref"` +} + +// Schema allows the definition of input and output data types. +type Schema struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + MultipleOf float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Maximum float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + Minimum float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + MaxLength uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + MaxItems uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + MaxProperties uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + MinProperties uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + AllOf []*Schema `json:"allOf,omitempty" yaml:"allOf,omitempty"` + OneOf []*Schema `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` + AnyOf []*Schema `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` + Not *Schema `json:"not,omitempty" yaml:"not,omitempty"` + Items *Schema `json:"items,omitempty" yaml:"items,omitempty"` + Properties map[string]*Schema `json:"properties,omitempty" yaml:"properties,omitempty"` + AdditionalProperties interface{} `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` + Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` + XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"` + ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` +} + +// Discriminator is an object used to aid in serialization, deserialization, and validation. +type Discriminator struct { + PropertyName string `json:"propertyName" yaml:"propertyName"` + Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` +} + +// XML is a metadata object that allows for more fine-tuned XML model definitions. +type XML struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` + Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"` + Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"` +} + +// SecurityScheme defines a security scheme that can be used by the operations. +type SecurityScheme struct { + Type string `json:"type" yaml:"type"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` + Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` + OpenIDConnectURL string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` +} + +// OAuthFlows allows configuration of the supported OAuth Flows. +type OAuthFlows struct { + Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` + Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` + ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` + AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` +} + +// OAuthFlow configuration details for a supported OAuth Flow +type OAuthFlow struct { + AuthorizationURL string `json:"authorizationUrl" yaml:"authorizationUrl"` + TokenURL string `json:"tokenUrl" yaml:"tokenUrl"` + RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes map[string]string `json:"scopes" yaml:"scopes"` +} + +// SecurityRequirement lists the required security schemes to execute this operation. +type SecurityRequirement map[string][]string + +// RequestBody describes a single request body. +type RequestBody struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Content map[string]*MediaType `json:"content" yaml:"content"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` +} + +// MediaType provides schema and examples for the media type identified by its key. +type MediaType struct { + Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` + Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` +} + +// Encoding is a single encoding definition applied to a single schema property. +type Encoding struct { + ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` +} + +// Responses is a container for the expected responses of an operation. +type Responses struct { + Default *Response `json:"default,omitempty" yaml:"default,omitempty"` + // Using map[string]*Response instead of a custom object to handle patterned fields for status codes + Responses map[string]*Response `json:"-" yaml:"-"` +} + +// Response describes a single response from an API Operation. +type Response struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Description string `json:"description" yaml:"description"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Content map[string]*MediaType `json:"content,omitempty" yaml:"content,omitempty"` + Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"` +} + +// Callback is a map of possible out-of-band callbacks related to the parent operation. +type Callback struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + // Using map[string]*PathItem to handle patterned fields + PathItems map[string]*PathItem `json:"-" yaml:"-"` +} + +// Example is an example of a media type. +type Example struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Value interface{} `json:"value,omitempty" yaml:"value,omitempty"` + ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` +} + +// Link represents a possible design-time link for a response. +type Link struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` + OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` + RequestBody interface{} `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Server *Server `json:"server,omitempty" yaml:"server,omitempty"` +} + +// Header follows the structure of the Parameter Object with the following changes: +// 1. name MUST NOT be specified, it is given in the corresponding headers map. +// 2. in MUST NOT be specified, it is implicitly in header. +// 3. All traits that are affected by the location MUST be applicable to a location of header (for example, style). +type Header struct { + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` + Content map[string]*MediaType `json:"content,omitempty" yaml:"content,omitempty"` +} diff --git a/protoc-gen-openapiv3/main.go b/protoc-gen-openapiv3/main.go new file mode 100644 index 00000000000..ef77a562014 --- /dev/null +++ b/protoc-gen-openapiv3/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "io/ioutil" + "os" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/pluginpb" + "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv3/internal/genopenapi" +) + +func main() { + req, err := readRequest() + if err != nil { + emitError(err) + return + } + + g := genopenapi.New() + resp, err := g.Generate(req) + if err != nil { + emitError(err) + return + } + + emitResponse(resp) +} + +func readRequest() (*pluginpb.CodeGeneratorRequest, error) { + data, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return nil, err + } + + req := new(pluginpb.CodeGeneratorRequest) + if err := proto.Unmarshal(data, req); err != nil { + return nil, err + } + + return req, nil +} + +func emitResponse(resp *pluginpb.CodeGeneratorResponse) { + buf, err := proto.Marshal(resp) + if err != nil { + panic(err) + } + if _, err := os.Stdout.Write(buf); err != nil { + panic(err) + } +} + +func emitError(err error) { + emitResponse(&pluginpb.CodeGeneratorResponse{Error: proto.String(err.Error())}) +} diff --git a/protoc-gen-openapiv3/options/openapiv3.proto b/protoc-gen-openapiv3/options/openapiv3.proto new file mode 100644 index 00000000000..e5a695e31de --- /dev/null +++ b/protoc-gen-openapiv3/options/openapiv3.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package grpc.gateway.protoc_gen_openapiv3.options; + +option go_package = "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv3/options"; + +// This file is a placeholder for OpenAPI v3 options.