Переглянути джерело

feat: swagger生成方法移过来

lihf 3 місяців тому
батько
коміт
d5541cca3c
8 змінених файлів з 1556 додано та 7 видалено
  1. 894 0
      generator/generator.go
  2. 235 0
      generator/reflector.go
  3. 64 0
      generator/utils.go
  4. 55 0
      generator/wellknown/mediatypes.go
  5. 304 0
      generator/wellknown/schemas.go
  6. 3 4
      go.mod
  7. 0 2
      go.sum
  8. 1 1
      service.go

+ 894 - 0
generator/generator.go

@@ -0,0 +1,894 @@
+// Copyright 2020 Google LLC. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package generator
+
+import (
+	"fmt"
+	"google.golang.org/protobuf/types/descriptorpb"
+	"log"
+	"net/url"
+	"regexp"
+	"sort"
+	"strings"
+
+	"google.golang.org/genproto/googleapis/api/annotations"
+	status_pb "google.golang.org/genproto/googleapis/rpc/status"
+	"google.golang.org/protobuf/compiler/protogen"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+	any_pb "google.golang.org/protobuf/types/known/anypb"
+
+	wk "git.ikuban.com/server/swagger-api/v2/generator/wellknown"
+	v3 "github.com/google/gnostic/openapiv3"
+)
+
+type Configuration struct {
+	Version         *string
+	Title           *string
+	Description     *string
+	Naming          *string
+	FQSchemaNaming  *bool
+	EnumType        *string
+	CircularDepth   *int
+	DefaultResponse *bool
+	OutputMode      *string
+}
+
+const (
+	infoURL = "git.ikuban.com/server/swagger-api"
+)
+
+// In order to dynamically add google.rpc.Status responses we need
+// to know the message descriptors for google.rpc.Status as well
+// as google.protobuf.Any.
+var statusProtoDesc = (&status_pb.Status{}).ProtoReflect().Descriptor()
+var anyProtoDesc = (&any_pb.Any{}).ProtoReflect().Descriptor()
+
+// OpenAPIv3Generator holds internal state needed to generate an OpenAPIv3 document for a transcoded Protocol Buffer service.
+type OpenAPIv3Generator struct {
+	conf   Configuration
+	plugin *protogen.Plugin
+
+	inputFiles        []*protogen.File
+	reflect           *OpenAPIv3Reflector
+	generatedSchemas  []string // Names of schemas that have already been generated.
+	linterRulePattern *regexp.Regexp
+	pathPattern       *regexp.Regexp
+	namedPathPattern  *regexp.Regexp
+}
+
+// NewOpenAPIv3Generator creates a new generator for a protoc plugin invocation.
+func NewOpenAPIv3Generator(plugin *protogen.Plugin, conf Configuration, inputFiles []*protogen.File) *OpenAPIv3Generator {
+	return &OpenAPIv3Generator{
+		conf:   conf,
+		plugin: plugin,
+
+		inputFiles:        inputFiles,
+		reflect:           NewOpenAPIv3Reflector(conf),
+		generatedSchemas:  make([]string, 0),
+		linterRulePattern: regexp.MustCompile(`\(-- .* --\)`),
+		pathPattern:       regexp.MustCompile("{([^=}]+)}"),
+		namedPathPattern:  regexp.MustCompile("{(.+)=(.+)}"),
+	}
+}
+
+// Run runs the generator.
+func (g *OpenAPIv3Generator) Run(outputFile *protogen.GeneratedFile) error {
+	d := g.buildDocumentV3()
+	bytes, err := d.YAMLValue("Generated with protoc-gen-openapi\n" + infoURL)
+	if err != nil {
+		return fmt.Errorf("failed to marshal yaml: %s", err.Error())
+	}
+	if _, err = outputFile.Write(bytes); err != nil {
+		return fmt.Errorf("failed to write yaml: %s", err.Error())
+	}
+	return nil
+}
+
+func (g *OpenAPIv3Generator) RunV2() ([]byte, error) {
+	d := g.buildDocumentV3()
+	bytes, err := d.YAMLValue("Generated with protoc-gen-openapi\n" + infoURL)
+	if err != nil {
+		return bytes, fmt.Errorf("failed to marshal yaml: %s", err.Error())
+	}
+	return bytes, nil
+}
+
+// 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{
+		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{},
+		},
+	}
+
+	// Go through the files and add the services to the documents, keeping
+	// track of which schemas are referenced in the response so we can
+	// add them later.
+	for _, file := range g.inputFiles {
+		if file.Generate {
+			// Merge any `Document` annotations with the current
+			extDocument := proto.GetExtension(file.Desc.Options(), v3.E_Document)
+			if extDocument != nil {
+				proto.Merge(d, extDocument.(*v3.Document))
+			}
+
+			g.addPathsToDocumentV3(d, file.Services)
+		}
+	}
+
+	// While we have required schemas left to generate, go through the files again
+	// looking for the related message and adding them to the document if required.
+	for len(g.reflect.requiredSchemas) > 0 {
+		count := len(g.reflect.requiredSchemas)
+		for _, file := range g.plugin.Files {
+			g.addSchemasForMessagesToDocumentV3(d, file.Messages, file.Proto.GetEdition())
+		}
+		g.reflect.requiredSchemas = g.reflect.requiredSchemas[count:len(g.reflect.requiredSchemas)]
+	}
+
+	// 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 = ""
+	}
+
+	allServers := []string{}
+
+	// If paths methods has servers, but they're all the same, then move servers to path level
+	for _, path := range d.Paths.Path {
+		servers := []string{}
+		// Only 1 server will ever be set, per method, by the generator
+
+		if path.Value.Get != nil && len(path.Value.Get.Servers) == 1 {
+			servers = appendUnique(servers, path.Value.Get.Servers[0].Url)
+			allServers = appendUnique(servers, path.Value.Get.Servers[0].Url)
+		}
+		if path.Value.Post != nil && len(path.Value.Post.Servers) == 1 {
+			servers = appendUnique(servers, path.Value.Post.Servers[0].Url)
+			allServers = appendUnique(servers, path.Value.Post.Servers[0].Url)
+		}
+		if path.Value.Put != nil && len(path.Value.Put.Servers) == 1 {
+			servers = appendUnique(servers, path.Value.Put.Servers[0].Url)
+			allServers = appendUnique(servers, path.Value.Put.Servers[0].Url)
+		}
+		if path.Value.Delete != nil && len(path.Value.Delete.Servers) == 1 {
+			servers = appendUnique(servers, path.Value.Delete.Servers[0].Url)
+			allServers = appendUnique(servers, path.Value.Delete.Servers[0].Url)
+		}
+		if path.Value.Patch != nil && len(path.Value.Patch.Servers) == 1 {
+			servers = appendUnique(servers, path.Value.Patch.Servers[0].Url)
+			allServers = appendUnique(servers, path.Value.Patch.Servers[0].Url)
+		}
+
+		if len(servers) == 1 {
+			path.Value.Servers = []*v3.Server{{Url: servers[0]}}
+
+			if path.Value.Get != nil {
+				path.Value.Get.Servers = nil
+			}
+			if path.Value.Post != nil {
+				path.Value.Post.Servers = nil
+			}
+			if path.Value.Put != nil {
+				path.Value.Put.Servers = nil
+			}
+			if path.Value.Delete != nil {
+				path.Value.Delete.Servers = nil
+			}
+			if path.Value.Patch != nil {
+				path.Value.Patch.Servers = nil
+			}
+		}
+	}
+
+	// Set all servers on API level
+	if len(allServers) > 0 {
+		d.Servers = []*v3.Server{}
+		for _, server := range allServers {
+			d.Servers = append(d.Servers, &v3.Server{Url: server})
+		}
+	}
+
+	// If there is only 1 server, we can safely remove all path level servers
+	if len(allServers) == 1 {
+		for _, path := range d.Paths.Path {
+			path.Value.Servers = nil
+		}
+	}
+
+	// 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
+		sort.Slice(pairs, func(i, j int) bool {
+			return pairs[i].Name < pairs[j].Name
+		})
+		d.Paths.Path = pairs
+	}
+	// Sort the schemas.
+	{
+		pairs := d.Components.Schemas.AdditionalProperties
+		sort.Slice(pairs, func(i, j int) bool {
+			return pairs[i].Name < pairs[j].Name
+		})
+		d.Components.Schemas.AdditionalProperties = pairs
+	}
+	return d
+}
+
+// filterCommentString removes linter rules from comments.
+func (g *OpenAPIv3Generator) filterCommentString(c protogen.Comments) string {
+	comment := g.linterRulePattern.ReplaceAllString(string(c), "")
+	return strings.TrimSpace(comment)
+}
+
+func (g *OpenAPIv3Generator) findField(name string, inMessage *protogen.Message) *protogen.Field {
+	for _, field := range inMessage.Fields {
+		if string(field.Desc.Name()) == name || string(field.Desc.JSONName()) == name {
+			return field
+		}
+	}
+
+	return nil
+}
+
+func (g *OpenAPIv3Generator) findAndFormatFieldName(name string, inMessage *protogen.Message) string {
+	field := g.findField(name, inMessage)
+	if field != nil {
+		return g.reflect.formatFieldName(field.Desc)
+	}
+
+	return name
+}
+
+// Note that fields which are mapped to URL query parameters must have a primitive type
+// or a repeated primitive type or a non-repeated message type.
+// In the case of a repeated type, the parameter can be repeated in the URL as ...?param=A&param=B.
+// In the case of a message type, each field of the message is mapped to a separate parameter,
+// such as ...?foo.a=A&foo.b=B&foo.c=C.
+// There are exceptions:
+// - for wrapper types it will use the same representation as the wrapped primitive type in JSON
+// - for google.protobuf.timestamp type it will be serialized as a string
+//
+// maps, Struct and Empty can NOT be used
+// messages can have any number of sub messages - including circular (e.g. sub.subsub.sub.subsub.id)
+
+// buildQueryParamsV3 extracts any valid query params, including sub and recursive messages
+func (g *OpenAPIv3Generator) buildQueryParamsV3(field *protogen.Field) []*v3.ParameterOrReference {
+	depths := map[string]int{}
+	return g._buildQueryParamsV3(field, depths)
+}
+
+// depths are used to keep track of how many times a message's fields has been seen
+func (g *OpenAPIv3Generator) _buildQueryParamsV3(field *protogen.Field, depths map[string]int) []*v3.ParameterOrReference {
+	parameters := []*v3.ParameterOrReference{}
+
+	queryFieldName := g.reflect.formatFieldName(field.Desc)
+	fieldDescription := g.filterCommentString(field.Comments.Leading)
+
+	if field.Desc.IsMap() {
+		// Map types are not allowed in query parameteres
+		return parameters
+
+	} else if field.Desc.Kind() == protoreflect.MessageKind {
+		typeName := g.reflect.fullMessageTypeName(field.Desc.Message())
+
+		switch typeName {
+		case ".google.protobuf.Value":
+			fieldSchema := g.reflect.schemaOrReferenceForField(field.Desc)
+			parameters = append(parameters,
+				&v3.ParameterOrReference{
+					Oneof: &v3.ParameterOrReference_Parameter{
+						Parameter: &v3.Parameter{
+							Name:        queryFieldName,
+							In:          "query",
+							Description: fieldDescription,
+							Required:    false,
+							Schema:      fieldSchema,
+						},
+					},
+				})
+			return parameters
+
+		case ".google.protobuf.BoolValue", ".google.protobuf.BytesValue", ".google.protobuf.Int32Value", ".google.protobuf.UInt32Value",
+			".google.protobuf.StringValue", ".google.protobuf.Int64Value", ".google.protobuf.UInt64Value", ".google.protobuf.FloatValue",
+			".google.protobuf.DoubleValue":
+			valueField := getValueField(field.Message.Desc)
+			fieldSchema := g.reflect.schemaOrReferenceForField(valueField)
+			parameters = append(parameters,
+				&v3.ParameterOrReference{
+					Oneof: &v3.ParameterOrReference_Parameter{
+						Parameter: &v3.Parameter{
+							Name:        queryFieldName,
+							In:          "query",
+							Description: fieldDescription,
+							Required:    false,
+							Schema:      fieldSchema,
+						},
+					},
+				})
+			return parameters
+
+		case ".google.protobuf.Timestamp":
+			fieldSchema := g.reflect.schemaOrReferenceForMessage(field.Message.Desc)
+			parameters = append(parameters,
+				&v3.ParameterOrReference{
+					Oneof: &v3.ParameterOrReference_Parameter{
+						Parameter: &v3.Parameter{
+							Name:        queryFieldName,
+							In:          "query",
+							Description: fieldDescription,
+							Required:    false,
+							Schema:      fieldSchema,
+						},
+					},
+				})
+			return parameters
+		case ".google.protobuf.Duration":
+			fieldSchema := g.reflect.schemaOrReferenceForMessage(field.Message.Desc)
+			parameters = append(parameters,
+				&v3.ParameterOrReference{
+					Oneof: &v3.ParameterOrReference_Parameter{
+						Parameter: &v3.Parameter{
+							Name:        queryFieldName,
+							In:          "query",
+							Description: fieldDescription,
+							Required:    false,
+							Schema:      fieldSchema,
+						},
+					},
+				})
+			return parameters
+		}
+
+		if field.Desc.IsList() {
+			// Only non-repeated message types are valid
+			return parameters
+		}
+
+		// Represent field masks directly as strings (don't expand them).
+		if typeName == ".google.protobuf.FieldMask" {
+			fieldSchema := g.reflect.schemaOrReferenceForField(field.Desc)
+			parameters = append(parameters,
+				&v3.ParameterOrReference{
+					Oneof: &v3.ParameterOrReference_Parameter{
+						Parameter: &v3.Parameter{
+							Name:        queryFieldName,
+							In:          "query",
+							Description: fieldDescription,
+							Required:    false,
+							Schema:      fieldSchema,
+						},
+					},
+				})
+			return parameters
+		}
+
+		// Sub messages are allowed, even circular, as long as the final type is a primitive.
+		// Go through each of the sub message fields
+		for _, subField := range field.Message.Fields {
+			subFieldFullName := string(subField.Desc.FullName())
+			seen, ok := depths[subFieldFullName]
+			if !ok {
+				depths[subFieldFullName] = 0
+			}
+
+			if seen < *g.conf.CircularDepth {
+				depths[subFieldFullName]++
+				subParams := g._buildQueryParamsV3(subField, depths)
+				for _, subParam := range subParams {
+					if param, ok := subParam.Oneof.(*v3.ParameterOrReference_Parameter); ok {
+						param.Parameter.Name = queryFieldName + "." + param.Parameter.Name
+						parameters = append(parameters, subParam)
+					}
+				}
+			}
+		}
+
+	} else if field.Desc.Kind() != protoreflect.GroupKind {
+		// schemaOrReferenceForField also handles array types
+		fieldSchema := g.reflect.schemaOrReferenceForField(field.Desc)
+
+		parameters = append(parameters,
+			&v3.ParameterOrReference{
+				Oneof: &v3.ParameterOrReference_Parameter{
+					Parameter: &v3.Parameter{
+						Name:        queryFieldName,
+						In:          "query",
+						Description: fieldDescription,
+						Required:    false,
+						Schema:      fieldSchema,
+					},
+				},
+			})
+	}
+
+	return parameters
+}
+
+// buildOperationV3 constructs an operation for a set of values.
+func (g *OpenAPIv3Generator) buildOperationV3(
+	d *v3.Document,
+	operationID string,
+	tagName string,
+	description string,
+	defaultHost string,
+	path string,
+	bodyField string,
+	inputMessage *protogen.Message,
+	outputMessage *protogen.Message,
+) (*v3.Operation, string) {
+	// coveredParameters tracks the parameters that have been used in the body or path.
+	coveredParameters := make([]string, 0)
+	if bodyField != "" {
+		coveredParameters = append(coveredParameters, bodyField)
+	}
+	// Initialize the list of operation parameters.
+	parameters := []*v3.ParameterOrReference{}
+
+	// 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)
+
+			// Add the path parameters to the operation parameters.
+			var fieldSchema *v3.SchemaOrReference
+
+			var fieldDescription string
+			field := g.findField(pathParameter, inputMessage)
+			if field != nil {
+				fieldSchema = g.reflect.schemaOrReferenceForField(field.Desc)
+				fieldDescription = g.filterCommentString(field.Comments.Leading)
+			} else {
+				// If field does not exist, it is safe to set it to string, as it is ignored downstream
+				fieldSchema = &v3.SchemaOrReference{
+					Oneof: &v3.SchemaOrReference_Schema{
+						Schema: &v3.Schema{
+							Type: "string",
+						},
+					},
+				}
+			}
+
+			parameters = append(parameters,
+				&v3.ParameterOrReference{
+					Oneof: &v3.ParameterOrReference_Parameter{
+						Parameter: &v3.Parameter{
+							Name:        pathParameter,
+							In:          "path",
+							Description: fieldDescription,
+							Required:    true,
+							Schema:      fieldSchema,
+						},
+					},
+				})
+		}
+	}
+
+	// Find named path parameters like {name=shelves/*}
+	if matches := g.namedPathPattern.FindStringSubmatch(path); matches != nil {
+		// Build a list of named path parameters.
+		namedPathParameters := make([]string, 0)
+
+		// 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/{thingsId}/otherthings/{otherthingsId}".
+		for i := 0; i < len(parts)-1; i += 2 {
+			section := parts[i]
+			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 named path parameters to the operation parameters.
+		for _, namedPathParameter := range namedPathParameters {
+			parameters = append(parameters,
+				&v3.ParameterOrReference{
+					Oneof: &v3.ParameterOrReference_Parameter{
+						Parameter: &v3.Parameter{
+							Name:        namedPathParameter,
+							In:          "path",
+							Required:    true,
+							Description: "The " + namedPathParameter + " id.",
+							Schema: &v3.SchemaOrReference{
+								Oneof: &v3.SchemaOrReference_Schema{
+									Schema: &v3.Schema{
+										Type: "string",
+									},
+								},
+							},
+						},
+					},
+				})
+		}
+	}
+
+	// Add any unhandled fields in the request message as query parameters.
+	if bodyField != "*" && string(inputMessage.Desc.FullName()) != "google.api.HttpBody" {
+		for _, field := range inputMessage.Fields {
+			fieldName := string(field.Desc.Name())
+			if !contains(coveredParameters, fieldName) && fieldName != bodyField {
+				fieldParams := g.buildQueryParamsV3(field)
+				parameters = append(parameters, fieldParams...)
+			}
+		}
+	}
+
+	// Create the response.
+	name, content := g.reflect.responseContentForMessage(outputMessage.Desc)
+	responses := &v3.Responses{
+		ResponseOrReference: []*v3.NamedResponseOrReference{
+			{
+				Name: name,
+				Value: &v3.ResponseOrReference{
+					Oneof: &v3.ResponseOrReference_Response{
+						Response: &v3.Response{
+							Description: "OK",
+							Content:     content,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	// Add the default reponse if needed
+	if *g.conf.DefaultResponse {
+		anySchemaName := g.reflect.formatMessageName(anyProtoDesc)
+		anySchema := wk.NewGoogleProtobufAnySchema(anySchemaName)
+		g.addSchemaToDocumentV3(d, anySchema)
+
+		statusSchemaName := g.reflect.formatMessageName(statusProtoDesc)
+		statusSchema := wk.NewGoogleRpcStatusSchema(statusSchemaName, anySchemaName)
+		g.addSchemaToDocumentV3(d, statusSchema)
+
+		defaultResponse := &v3.NamedResponseOrReference{
+			Name: "default",
+			Value: &v3.ResponseOrReference{
+				Oneof: &v3.ResponseOrReference_Response{
+					Response: &v3.Response{
+						Description: "Default error response",
+						Content: wk.NewApplicationJsonMediaType(&v3.SchemaOrReference{
+							Oneof: &v3.SchemaOrReference_Reference{
+								Reference: &v3.Reference{XRef: "#/components/schemas/" + statusSchemaName}}}),
+					},
+				},
+			},
+		}
+
+		responses.ResponseOrReference = append(responses.ResponseOrReference, defaultResponse)
+	}
+
+	// Create the operation.
+	op := &v3.Operation{
+		Tags:        []string{tagName},
+		Description: description,
+		OperationId: operationID,
+		Parameters:  parameters,
+		Responses:   responses,
+	}
+
+	if defaultHost != "" {
+		hostURL, err := url.Parse(defaultHost)
+		if err == nil {
+			hostURL.Scheme = "https"
+			op.Servers = append(op.Servers, &v3.Server{Url: hostURL.String()})
+		}
+	}
+
+	// If a body field is specified, we need to pass a message as the request body.
+	if bodyField != "" {
+		var requestSchema *v3.SchemaOrReference
+
+		if bodyField == "*" {
+			// Pass the entire request message as the request body.
+			requestSchema = g.reflect.schemaOrReferenceForMessage(inputMessage.Desc)
+
+		} else {
+			// If body refers to a message field, use that type.
+			for _, field := range inputMessage.Fields {
+				if string(field.Desc.Name()) == bodyField {
+					switch field.Desc.Kind() {
+					case protoreflect.StringKind:
+						requestSchema = &v3.SchemaOrReference{
+							Oneof: &v3.SchemaOrReference_Schema{
+								Schema: &v3.Schema{
+									Type: "string",
+								},
+							},
+						}
+
+					case protoreflect.MessageKind:
+						requestSchema = g.reflect.schemaOrReferenceForMessage(field.Message.Desc)
+
+					default:
+						log.Printf("unsupported field type %+v", field.Desc)
+					}
+					break
+				}
+			}
+		}
+
+		op.RequestBody = &v3.RequestBodyOrReference{
+			Oneof: &v3.RequestBodyOrReference_RequestBody{
+				RequestBody: &v3.RequestBody{
+					Required: true,
+					Content: &v3.MediaTypes{
+						AdditionalProperties: []*v3.NamedMediaType{
+							{
+								Name: "application/json",
+								Value: &v3.MediaType{
+									Schema: requestSchema,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+	}
+	return op, path
+}
+
+// addOperationToDocumentV3 adds an operation to the specified path/method.
+func (g *OpenAPIv3Generator) addOperationToDocumentV3(d *v3.Document, op *v3.Operation, path string, methodName string) {
+	var selectedPathItem *v3.NamedPathItem
+	for _, namedPathItem := range d.Paths.Path {
+		if namedPathItem.Name == path {
+			selectedPathItem = namedPathItem
+			break
+		}
+	}
+	// If we get here, we need to create a path item.
+	if selectedPathItem == nil {
+		selectedPathItem = &v3.NamedPathItem{Name: path, Value: &v3.PathItem{}}
+		d.Paths.Path = append(d.Paths.Path, selectedPathItem)
+	}
+	// Set the operation on the specified method.
+	switch methodName {
+	case "GET":
+		selectedPathItem.Value.Get = op
+	case "POST":
+		selectedPathItem.Value.Post = op
+	case "PUT":
+		selectedPathItem.Value.Put = op
+	case "DELETE":
+		selectedPathItem.Value.Delete = op
+	case "PATCH":
+		selectedPathItem.Value.Patch = op
+	}
+}
+
+// addPathsToDocumentV3 adds paths from a specified file descriptor.
+func (g *OpenAPIv3Generator) addPathsToDocumentV3(d *v3.Document, services []*protogen.Service) {
+	for _, service := range services {
+		annotationsCount := 0
+
+		for _, method := range service.Methods {
+			comment := g.filterCommentString(method.Comments.Leading)
+			inputMessage := method.Input
+			outputMessage := method.Output
+			operationID := service.GoName + "_" + method.GoName
+
+			extOperation := proto.GetExtension(method.Desc.Options(), v3.E_Operation)
+			if extOperation == nil || extOperation == v3.E_Operation.InterfaceOf(v3.E_Operation.Zero()) {
+				continue
+			}
+			annotationsCount++
+
+			path := fmt.Sprintf("/api/%s/%s", service.Desc.FullName(), method.GoName)
+
+			defaultHost := proto.GetExtension(service.Desc.Options(), annotations.E_DefaultHost).(string)
+
+			op, path2 := g.buildOperationV3(
+				d, operationID, service.GoName, comment, defaultHost, path, "*", inputMessage, outputMessage)
+
+			// Merge any `Operation` annotations with the current
+			proto.Merge(op, extOperation.(*v3.Operation))
+
+			g.addOperationToDocumentV3(d, op, path2, "POST")
+		}
+
+		if annotationsCount > 0 {
+			comment := g.filterCommentString(service.Comments.Leading)
+			d.Tags = append(d.Tags, &v3.Tag{Name: service.GoName, Description: comment})
+		}
+	}
+}
+
+// addSchemaForMessageToDocumentV3 adds the schema to the document if required
+func (g *OpenAPIv3Generator) addSchemaToDocumentV3(d *v3.Document, schema *v3.NamedSchemaOrReference) {
+	if contains(g.generatedSchemas, schema.Name) {
+		return
+	}
+	g.generatedSchemas = append(g.generatedSchemas, schema.Name)
+	d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, schema)
+}
+
+// addSchemasForMessagesToDocumentV3 adds info from one file descriptor.
+func (g *OpenAPIv3Generator) addSchemasForMessagesToDocumentV3(d *v3.Document, messages []*protogen.Message, edition descriptorpb.Edition) {
+	// For each message, generate a definition.
+	for _, message := range messages {
+		if message.Messages != nil {
+			g.addSchemasForMessagesToDocumentV3(d, message.Messages, edition)
+		}
+
+		schemaName := g.reflect.formatMessageName(message.Desc)
+
+		// Only generate this if we need it and haven't already generated it.
+		if !contains(g.reflect.requiredSchemas, schemaName) ||
+			contains(g.generatedSchemas, schemaName) {
+			continue
+		}
+
+		typeName := g.reflect.fullMessageTypeName(message.Desc)
+		messageDescription := g.filterCommentString(message.Comments.Leading)
+
+		// `google.protobuf.Value` and `google.protobuf.Any` have special JSON transcoding
+		// so we can't just reflect on the message descriptor.
+		if typeName == ".google.protobuf.Value" {
+			g.addSchemaToDocumentV3(d, wk.NewGoogleProtobufValueSchema(schemaName))
+			continue
+		} else if typeName == ".google.protobuf.Any" {
+			g.addSchemaToDocumentV3(d, wk.NewGoogleProtobufAnySchema(schemaName))
+			continue
+		} else if typeName == ".google.rpc.Status" {
+			anySchemaName := g.reflect.formatMessageName(anyProtoDesc)
+			g.addSchemaToDocumentV3(d, wk.NewGoogleProtobufAnySchema(anySchemaName))
+			g.addSchemaToDocumentV3(d, wk.NewGoogleRpcStatusSchema(schemaName, anySchemaName))
+			continue
+		}
+
+		// Build an array holding the fields of the message.
+		definitionProperties := &v3.Properties{
+			AdditionalProperties: make([]*v3.NamedSchemaOrReference, 0),
+		}
+
+		var required []string
+		for _, field := range message.Fields {
+			// Get the field description from the comments.
+			description := g.filterCommentString(field.Comments.Leading)
+			// Check the field annotations to see if this is a readonly or writeonly field.
+			inputOnly := false
+			outputOnly := false
+			isRequired := true
+			extension := proto.GetExtension(field.Desc.Options(), annotations.E_FieldBehavior)
+			if extension != nil {
+				switch v := extension.(type) {
+				case []annotations.FieldBehavior:
+					for _, vv := range v {
+						switch vv {
+						case annotations.FieldBehavior_OUTPUT_ONLY:
+							outputOnly = true
+						case annotations.FieldBehavior_INPUT_ONLY:
+							inputOnly = true
+						case annotations.FieldBehavior_OPTIONAL:
+							isRequired = false
+						}
+					}
+				default:
+					log.Printf("unsupported extension type %T", extension)
+				}
+			}
+
+			if edition == descriptorpb.Edition_EDITION_2023 {
+				if fieldOptions, ok := field.Desc.Options().(*descriptorpb.FieldOptions); ok {
+					if fieldOptions.GetFeatures().GetFieldPresence() == descriptorpb.FeatureSet_EXPLICIT {
+						isRequired = false
+					}
+				}
+			}
+
+			if isRequired {
+				required = append(required, g.reflect.formatFieldName(field.Desc))
+			}
+
+			// The field is either described by a reference or a schema.
+			fieldSchema := g.reflect.schemaOrReferenceForField(field.Desc)
+			if fieldSchema == nil {
+				continue
+			}
+
+			// If this field has siblings and is a $ref now, create a new schema use `allOf` to wrap it
+			wrapperNeeded := inputOnly || outputOnly || description != ""
+			if wrapperNeeded {
+				if _, ok := fieldSchema.Oneof.(*v3.SchemaOrReference_Reference); ok {
+					fieldSchema = &v3.SchemaOrReference{Oneof: &v3.SchemaOrReference_Schema{Schema: &v3.Schema{
+						AllOf: []*v3.SchemaOrReference{fieldSchema},
+					}}}
+				}
+			}
+
+			if schema, ok := fieldSchema.Oneof.(*v3.SchemaOrReference_Schema); ok {
+				schema.Schema.Description = description
+				schema.Schema.ReadOnly = outputOnly
+				schema.Schema.WriteOnly = inputOnly
+
+				// Merge any `Property` annotations with the current
+				extProperty := proto.GetExtension(field.Desc.Options(), v3.E_Property)
+				if extProperty != nil {
+					proto.Merge(schema.Schema, extProperty.(*v3.Schema))
+				}
+			}
+
+			definitionProperties.AdditionalProperties = append(
+				definitionProperties.AdditionalProperties,
+				&v3.NamedSchemaOrReference{
+					Name:  g.reflect.formatFieldName(field.Desc),
+					Value: fieldSchema,
+				},
+			)
+		}
+
+		schema := &v3.Schema{
+			Type:        "object",
+			Description: messageDescription,
+			Properties:  definitionProperties,
+			Required:    required,
+		}
+
+		// Merge any `Schema` annotations with the current
+		extSchema := proto.GetExtension(message.Desc.Options(), v3.E_Schema)
+		if extSchema != nil {
+			proto.Merge(schema, extSchema.(*v3.Schema))
+		}
+
+		// Add the schema to the components.schema list.
+		g.addSchemaToDocumentV3(d, &v3.NamedSchemaOrReference{
+			Name: schemaName,
+			Value: &v3.SchemaOrReference{
+				Oneof: &v3.SchemaOrReference_Schema{
+					Schema: schema,
+				},
+			},
+		})
+	}
+}

+ 235 - 0
generator/reflector.go

@@ -0,0 +1,235 @@
+// Copyright 2020 Google LLC. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package generator
+
+import (
+	"log"
+	"strings"
+
+	"google.golang.org/protobuf/reflect/protoreflect"
+
+	wk "git.ikuban.com/server/swagger-api/v2/generator/wellknown"
+	v3 "github.com/google/gnostic/openapiv3"
+)
+
+const (
+	protobufValueName = "GoogleProtobufValue"
+	protobufAnyName   = "GoogleProtobufAny"
+)
+
+type OpenAPIv3Reflector struct {
+	conf Configuration
+
+	requiredSchemas []string // Names of schemas which are used through references.
+}
+
+// NewOpenAPIv3Reflector creates a new reflector.
+func NewOpenAPIv3Reflector(conf Configuration) *OpenAPIv3Reflector {
+	return &OpenAPIv3Reflector{
+		conf: conf,
+
+		requiredSchemas: make([]string, 0),
+	}
+}
+
+func (r *OpenAPIv3Reflector) getMessageName(message protoreflect.MessageDescriptor) string {
+	prefix := ""
+	parent := message.Parent()
+
+	if _, ok := parent.(protoreflect.MessageDescriptor); ok {
+		prefix = string(parent.Name()) + "_" + prefix
+	}
+
+	return prefix + string(message.Name())
+}
+
+func (r *OpenAPIv3Reflector) formatMessageName(message protoreflect.MessageDescriptor) string {
+	typeName := r.fullMessageTypeName(message)
+
+	name := r.getMessageName(message)
+	if !*r.conf.FQSchemaNaming {
+		if typeName == ".google.protobuf.Value" {
+			name = protobufValueName
+		} else if typeName == ".google.protobuf.Any" {
+			name = protobufAnyName
+		}
+	}
+
+	if *r.conf.Naming == "json" {
+		if len(name) > 1 {
+			name = strings.ToUpper(name[0:1]) + name[1:]
+		}
+
+		if len(name) == 1 {
+			name = strings.ToLower(name)
+		}
+	}
+
+	if *r.conf.FQSchemaNaming {
+		package_name := string(message.ParentFile().Package())
+		name = package_name + "." + name
+	}
+
+	return name
+}
+
+func (r *OpenAPIv3Reflector) formatFieldName(field protoreflect.FieldDescriptor) string {
+	if *r.conf.Naming == "proto" {
+		return string(field.Name())
+	}
+
+	return field.JSONName()
+}
+
+// fullMessageTypeName builds the full type name of a message.
+func (r *OpenAPIv3Reflector) fullMessageTypeName(message protoreflect.MessageDescriptor) string {
+	name := r.getMessageName(message)
+	return "." + string(message.ParentFile().Package()) + "." + name
+}
+
+func (r *OpenAPIv3Reflector) responseContentForMessage(message protoreflect.MessageDescriptor) (string, *v3.MediaTypes) {
+	typeName := r.fullMessageTypeName(message)
+
+	if typeName == ".google.protobuf.Empty" {
+		return "200", wk.NewApplicationJsonMediaType(r.schemaOrReferenceForMessage(message))
+	}
+
+	if typeName == ".google.api.HttpBody" {
+		return "200", wk.NewGoogleApiHttpBodyMediaType()
+	}
+
+	return "200", wk.NewApplicationJsonMediaType(r.schemaOrReferenceForMessage(message))
+}
+
+func (r *OpenAPIv3Reflector) schemaReferenceForMessage(message protoreflect.MessageDescriptor) string {
+	schemaName := r.formatMessageName(message)
+	if !contains(r.requiredSchemas, schemaName) {
+		r.requiredSchemas = append(r.requiredSchemas, schemaName)
+	}
+	return "#/components/schemas/" + schemaName
+}
+
+// Returns a full schema for simple types, and a schema reference for complex types that reference
+// the definition in `#/components/schemas/`
+func (r *OpenAPIv3Reflector) schemaOrReferenceForMessage(message protoreflect.MessageDescriptor) *v3.SchemaOrReference {
+	typeName := r.fullMessageTypeName(message)
+
+	switch typeName {
+
+	case ".google.api.HttpBody":
+		return wk.NewGoogleApiHttpBodySchema()
+
+	case ".google.protobuf.Timestamp":
+		return wk.NewGoogleProtobufTimestampSchema()
+
+	case ".google.protobuf.Duration":
+		return wk.NewGoogleProtobufDurationSchema()
+
+	case ".google.type.Date":
+		return wk.NewGoogleTypeDateSchema()
+
+	case ".google.type.DateTime":
+		return wk.NewGoogleTypeDateTimeSchema()
+
+	case ".google.protobuf.FieldMask":
+		return wk.NewGoogleProtobufFieldMaskSchema()
+
+	case ".google.protobuf.Struct":
+		return wk.NewGoogleProtobufStructSchema()
+
+	case ".google.protobuf.Empty":
+		// Empty is closer to JSON undefined than null, so ignore this field
+		return wk.NewGoogleProtobufStructSchema() //&v3.SchemaOrReference{Oneof: &v3.SchemaOrReference_Schema{Schema: &v3.Schema{Type: "null"}}}
+	case ".google.protobuf.BoolValue":
+		return wk.NewBooleanSchema()
+
+	case ".google.protobuf.BytesValue":
+		return wk.NewBytesSchema()
+
+	case ".google.protobuf.Int32Value", ".google.protobuf.UInt32Value", ".google.protobuf.Int64Value", ".google.protobuf.UInt64Value":
+		return wk.NewIntegerSchema(getValueKind(message))
+
+	case ".google.protobuf.StringValue":
+		return wk.NewStringSchema()
+
+	case ".google.protobuf.FloatValue", ".google.protobuf.DoubleValue":
+		return wk.NewNumberSchema(getValueKind(message))
+
+	default:
+		ref := r.schemaReferenceForMessage(message)
+		return &v3.SchemaOrReference{
+			Oneof: &v3.SchemaOrReference_Reference{
+				Reference: &v3.Reference{XRef: ref}}}
+	}
+}
+
+func (r *OpenAPIv3Reflector) schemaOrReferenceForField(field protoreflect.FieldDescriptor) *v3.SchemaOrReference {
+	var kindSchema *v3.SchemaOrReference
+
+	kind := field.Kind()
+
+	switch kind {
+
+	case protoreflect.MessageKind:
+		if field.IsMap() {
+			// This means the field is a map, for example:
+			//   map<string, value_type> map_field = 1;
+			//
+			// The map ends up getting converted into something like this:
+			//   message MapFieldEntry {
+			//     string key = 1;
+			//     value_type value = 2;
+			//   }
+			//
+			//   repeated MapFieldEntry map_field = N;
+			//
+			// So we need to find the `value` field in the `MapFieldEntry` message and
+			// then return a MapFieldEntry schema using the schema for the `value` field
+			return wk.NewGoogleProtobufMapFieldEntrySchema(r.schemaOrReferenceForField(field.MapValue()))
+		} else {
+			kindSchema = r.schemaOrReferenceForMessage(field.Message())
+		}
+
+	case protoreflect.StringKind:
+		kindSchema = wk.NewStringSchema()
+
+	case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Uint32Kind,
+		protoreflect.Sfixed32Kind, protoreflect.Fixed32Kind:
+		kindSchema = wk.NewIntegerSchema(kind.String())
+
+	case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Uint64Kind,
+		protoreflect.Sfixed64Kind, protoreflect.Fixed64Kind:
+		kindSchema = wk.NewIntegerSchema(kind.String())
+	case protoreflect.EnumKind:
+		kindSchema = wk.NewEnumSchema(*&r.conf.EnumType, field)
+	case protoreflect.BoolKind:
+		kindSchema = wk.NewBooleanSchema()
+	case protoreflect.FloatKind, protoreflect.DoubleKind:
+		kindSchema = wk.NewNumberSchema(kind.String())
+
+	case protoreflect.BytesKind:
+		kindSchema = wk.NewBytesSchema()
+
+	default:
+		log.Printf("(TODO) Unsupported field type: %+v", r.fullMessageTypeName(field.Message()))
+	}
+
+	if field.IsList() {
+		kindSchema = wk.NewListSchema(kindSchema)
+	}
+
+	return kindSchema
+}

+ 64 - 0
generator/utils.go

@@ -0,0 +1,64 @@
+// Copyright 2020 Google LLC. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package generator
+
+import (
+	"strings"
+
+	"google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// contains returns true if an array contains a specified string.
+func contains(s []string, e string) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}
+
+// appendUnique appends a string, to a string slice, if the string is not already in the slice
+func appendUnique(s []string, e string) []string {
+	if !contains(s, e) {
+		return append(s, e)
+	}
+	return s
+}
+
+// singular produces the singular form of a collection name.
+func singular(plural string) string {
+	if strings.HasSuffix(plural, "ves") {
+		return strings.TrimSuffix(plural, "ves") + "f"
+	}
+	if strings.HasSuffix(plural, "ies") {
+		return strings.TrimSuffix(plural, "ies") + "y"
+	}
+	if strings.HasSuffix(plural, "s") {
+		return strings.TrimSuffix(plural, "s")
+	}
+	return plural
+}
+
+func getValueKind(message protoreflect.MessageDescriptor) string {
+	valueField := getValueField(message)
+	return valueField.Kind().String()
+}
+
+func getValueField(message protoreflect.MessageDescriptor) protoreflect.FieldDescriptor {
+	fields := message.Fields()
+	return fields.ByName("value")
+}

+ 55 - 0
generator/wellknown/mediatypes.go

@@ -0,0 +1,55 @@
+// Copyright 2020 Google LLC. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, softwis
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package wellknown
+
+import (
+	v3 "github.com/google/gnostic/openapiv3"
+)
+
+func NewGoogleApiHttpBodyMediaType() *v3.MediaTypes {
+	return &v3.MediaTypes{
+		AdditionalProperties: []*v3.NamedMediaType{
+			{
+				Name:  "*/*",
+				Value: &v3.MediaType{},
+			},
+		},
+	}
+}
+
+func NewApplicationJsonMediaType(schema *v3.SchemaOrReference) *v3.MediaTypes {
+	return &v3.MediaTypes{
+		AdditionalProperties: []*v3.NamedMediaType{
+			{
+				Name: "application/json",
+				Value: &v3.MediaType{
+					Schema: schema,
+				},
+			},
+		},
+	}
+}
+
+func NewApplicationJsonMediaTypeByEmpty() *v3.MediaTypes {
+	return &v3.MediaTypes{
+		AdditionalProperties: []*v3.NamedMediaType{
+			{
+				Name:  "application/json",
+				Value: &v3.MediaType{},
+			},
+		},
+	}
+}

+ 304 - 0
generator/wellknown/schemas.go

@@ -0,0 +1,304 @@
+// Copyright 2020 Google LLC. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, softwis
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package wellknown
+
+import (
+	v3 "github.com/google/gnostic/openapiv3"
+	"google.golang.org/protobuf/reflect/protoreflect"
+)
+
+func NewStringSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "string"}}}
+}
+
+func NewBooleanSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "boolean"}}}
+}
+
+func NewBytesSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "string", Format: "bytes"}}}
+}
+
+func NewIntegerSchema(format string) *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "integer", Format: format}}}
+}
+
+func NewNumberSchema(format string) *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "number", Format: format}}}
+}
+
+func NewEnumSchema(enum_type *string, field protoreflect.FieldDescriptor) *v3.SchemaOrReference {
+	schema := &v3.Schema{Format: "enum"}
+	if enum_type != nil && *enum_type == "string" {
+		schema.Type = "string"
+		schema.Enum = make([]*v3.Any, 0, field.Enum().Values().Len())
+		for i := 0; i < field.Enum().Values().Len(); i++ {
+			schema.Enum = append(schema.Enum, &v3.Any{
+				Yaml: string(field.Enum().Values().Get(i).Name()),
+			})
+		}
+	} else {
+		schema.Type = "integer"
+	}
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: schema}}
+}
+
+func NewListSchema(item_schema *v3.SchemaOrReference) *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{
+				Type:  "array",
+				Items: &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{item_schema}},
+			},
+		},
+	}
+}
+
+// google.api.HttpBody will contain POST body data
+// This is based on how Envoy handles google.api.HttpBody
+func NewGoogleApiHttpBodySchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "string"}}}
+}
+
+// google.protobuf.Timestamp is serialized as a string
+func NewGoogleProtobufTimestampSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "string", Format: "date-time"}}}
+}
+
+// google.protobuf.Duration is serialized as a string
+func NewGoogleProtobufDurationSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			// From: https://github.com/protocolbuffers/protobuf/blob/ece5ef6b9b6fa66ef4638335612284379ee4548f/src/google/protobuf/duration.proto
+			// In JSON format, the Duration type is encoded as a string rather than an
+			// object, where the string ends in the suffix "s" (indicating seconds) and
+			// is preceded by the number of seconds, with nanoseconds expressed as
+			// fractional seconds. For example, 3 seconds with 0 nanoseconds should be
+			// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should
+			// be expressed in JSON format as "3.000000001s", and 3 seconds and 1
+			// microsecond should be expressed in JSON format as "3.000001s".
+			//
+			// The fields of message google.protobuf.Duration are further described as:
+			// "int64 seconds"
+			// Signed seconds of the span of time. Must be from -315,576,000,000
+			// to +315,576,000,000 inclusive. Note: these bounds are computed from:
+			// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
+			// `int32 nanos`
+			// Signed fractions of a second at nanosecond resolution of the span
+			// of time. Durations less than one second are represented with a 0
+			// `seconds` field and a positive or negative `nanos` field. For durations
+			// of one second or more, a non-zero value for the `nanos` field must be
+			// of the same sign as the `seconds` field. Must be from -999,999,999
+			// to +999,999,999 inclusive.
+			//
+			// This leads to the regex below limiting range from -315.576,000,000s to 315,576,000,000s
+			// allowing -0.999,999,999s to 0.999,999,999s in the floating precision range.
+			// That full range cannot be expressed precisly in float64 as demonstrated in
+			// the example at https://go.dev/play/p/XNtuhwdyu8Y for your reference.
+			// So the well known type google.protobuf.Duration needs a string.
+			//
+			// Please note that JSON schemas duration format is NOT the same, as that uses
+			// a different syntax starting with "P", supports daylight saving times and other
+			// different features, so it is NOT compatible.
+			Schema: &v3.Schema{
+				Type:        "string",
+				Pattern:     `^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$`,
+				Description: "Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s",
+			},
+		},
+	}
+}
+
+// google.type.Date is serialized as a string
+func NewGoogleTypeDateSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "string", Format: "date"}}}
+}
+
+// google.type.DateTime is serialized as a string
+func NewGoogleTypeDateTimeSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "string", Format: "date-time"}}}
+}
+
+// google.protobuf.FieldMask masks is serialized as a string
+func NewGoogleProtobufFieldMaskSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "string", Format: "field-mask"}}}
+}
+
+// google.protobuf.Struct is equivalent to a JSON object
+func NewGoogleProtobufStructSchema() *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "object"}}}
+}
+
+// google.protobuf.Value is handled specially
+// See here for the details on the JSON mapping:
+//
+//	https://developers.google.com/protocol-buffers/docs/proto3#json
+//
+// and here:
+//
+//	https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Value
+func NewGoogleProtobufValueSchema(name string) *v3.NamedSchemaOrReference {
+	return &v3.NamedSchemaOrReference{
+		Name: name,
+		Value: &v3.SchemaOrReference{
+			Oneof: &v3.SchemaOrReference_Schema{
+				Schema: &v3.Schema{
+					Description: "Represents a dynamically typed value which can be either null, a number, a string, a boolean, a recursive struct value, or a list of values.",
+				},
+			},
+		},
+	}
+}
+
+// google.protobuf.Any is handled specially
+// See here for the details on the JSON mapping:
+//
+//	https://developers.google.com/protocol-buffers/docs/proto3#json
+func NewGoogleProtobufAnySchema(name string) *v3.NamedSchemaOrReference {
+	return &v3.NamedSchemaOrReference{
+		Name: name,
+		Value: &v3.SchemaOrReference{
+			Oneof: &v3.SchemaOrReference_Schema{
+				Schema: &v3.Schema{
+					Type:        "object",
+					Description: "Contains an arbitrary serialized message along with a @type that describes the type of the serialized message.",
+					Properties: &v3.Properties{
+						AdditionalProperties: []*v3.NamedSchemaOrReference{
+							{
+								Name: "@type",
+								Value: &v3.SchemaOrReference{
+									Oneof: &v3.SchemaOrReference_Schema{
+										Schema: &v3.Schema{
+											Type:        "string",
+											Description: "The type of the serialized message.",
+										},
+									},
+								},
+							},
+						},
+					},
+					AdditionalProperties: &v3.AdditionalPropertiesItem{
+						Oneof: &v3.AdditionalPropertiesItem_Boolean{
+							Boolean: true,
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+// google.rpc.Status is handled specially
+func NewGoogleRpcStatusSchema(name string, any_name string) *v3.NamedSchemaOrReference {
+	return &v3.NamedSchemaOrReference{
+		Name: name,
+		Value: &v3.SchemaOrReference{
+			Oneof: &v3.SchemaOrReference_Schema{
+				Schema: &v3.Schema{
+					Type:        "object",
+					Description: "The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).",
+					Properties: &v3.Properties{
+						AdditionalProperties: []*v3.NamedSchemaOrReference{
+							{
+								Name: "code",
+								Value: &v3.SchemaOrReference{
+									Oneof: &v3.SchemaOrReference_Schema{
+										Schema: &v3.Schema{
+											Type:        "integer",
+											Format:      "int32",
+											Description: "The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].",
+										},
+									},
+								},
+							},
+							{
+								Name: "message",
+								Value: &v3.SchemaOrReference{
+									Oneof: &v3.SchemaOrReference_Schema{
+										Schema: &v3.Schema{
+											Type:        "string",
+											Description: "A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.",
+										},
+									},
+								},
+							},
+							{
+								Name: "details",
+								Value: &v3.SchemaOrReference{
+									Oneof: &v3.SchemaOrReference_Schema{
+										Schema: &v3.Schema{
+											Type: "array",
+											Items: &v3.ItemsItem{
+												SchemaOrReference: []*v3.SchemaOrReference{
+													{
+														Oneof: &v3.SchemaOrReference_Reference{
+															Reference: &v3.Reference{
+																XRef: "#/components/schemas/" + any_name,
+															},
+														},
+													},
+												},
+											},
+											Description: "A list of messages that carry the error details.  There is a common set of message types for APIs to use.",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func NewGoogleProtobufMapFieldEntrySchema(value_field_schema *v3.SchemaOrReference) *v3.SchemaOrReference {
+	return &v3.SchemaOrReference{
+		Oneof: &v3.SchemaOrReference_Schema{
+			Schema: &v3.Schema{Type: "object",
+				AdditionalProperties: &v3.AdditionalPropertiesItem{
+					Oneof: &v3.AdditionalPropertiesItem_SchemaOrReference{
+						SchemaOrReference: value_field_schema,
+					},
+				},
+			},
+		},
+	}
+}

+ 3 - 4
go.mod

@@ -3,10 +3,12 @@ module git.ikuban.com/server/swagger-api/v2
 go 1.23.2
 
 require (
-	git.ikuban.com/server/gnostic/v2 v2.0.2
 	github.com/go-kratos/kratos/v2 v2.8.2
+	github.com/google/gnostic v0.7.0
 	github.com/gorilla/mux v1.8.1
 	github.com/xmkuban/utils v0.0.14
+	google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157
 	google.golang.org/grpc v1.64.0
 	google.golang.org/protobuf v1.34.1
 )
@@ -14,14 +16,11 @@ require (
 require (
 	github.com/go-kratos/aegis v0.2.0 // indirect
 	github.com/go-playground/form/v4 v4.2.0 // indirect
-	github.com/google/gnostic v0.7.0 // indirect
 	github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
 	golang.org/x/net v0.23.0 // indirect
 	golang.org/x/sys v0.18.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 0 - 2
go.sum

@@ -593,8 +593,6 @@ cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT
 cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
-git.ikuban.com/server/gnostic/v2 v2.0.2 h1:4eXB11plsSE3hnLWNCJ/MYpaZVtVsg1+1A2QGjNl+64=
-git.ikuban.com/server/gnostic/v2 v2.0.2/go.mod h1:BKeEVLgYEiEPH4D33nDpZ0GzVHCp3cEwxQMVhARd6PM=
 git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=

+ 1 - 1
service.go

@@ -3,8 +3,8 @@ package swagger_api
 import (
 	"context"
 	"fmt"
+	"git.ikuban.com/server/swagger-api/v2/generator"
 
-	"git.ikuban.com/server/gnostic/v2/protoc-gen-openapi/generator"
 	"github.com/go-kratos/kratos/v2/api/metadata"
 	"github.com/xmkuban/utils/utils"
 	"google.golang.org/protobuf/compiler/protogen"