doc-generate/internal/swagger/openapi3.go

507 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package swagger
import (
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-openapi/spec"
)
// convertSwagger2ToOpenAPI3 converts a Swagger 2.0 spec to OpenAPI 3.0
func convertSwagger2ToOpenAPI3(swagger2 *spec.Swagger) *openapi3.T {
openapi3Doc := &openapi3.T{
OpenAPI: "3.0.3",
Info: &openapi3.Info{
Title: swagger2.Info.Title,
Description: swagger2.Info.Description,
TermsOfService: swagger2.Info.TermsOfService,
Version: swagger2.Info.Version,
},
Servers: openapi3.Servers{},
Paths: &openapi3.Paths{},
}
// Convert extensions
if swagger2.Info.VendorExtensible.Extensions != nil {
openapi3Doc.Extensions = make(map[string]interface{})
for k, v := range swagger2.Info.VendorExtensible.Extensions {
openapi3Doc.Extensions[k] = v
}
}
// Add extensions from swagger root
if swagger2.Extensions != nil {
if openapi3Doc.Extensions == nil {
openapi3Doc.Extensions = make(map[string]interface{})
}
for k, v := range swagger2.Extensions {
openapi3Doc.Extensions[k] = v
}
}
// Convert Contact
if swagger2.Info.Contact != nil {
openapi3Doc.Info.Contact = &openapi3.Contact{
Name: swagger2.Info.Contact.Name,
URL: swagger2.Info.Contact.URL,
Email: swagger2.Info.Contact.Email,
}
}
// Convert License
if swagger2.Info.License != nil {
openapi3Doc.Info.License = &openapi3.License{
Name: swagger2.Info.License.Name,
URL: swagger2.Info.License.URL,
}
}
// Convert Servers from host, basePath, and schemes
if len(swagger2.Host) > 0 || len(swagger2.BasePath) > 0 {
schemes := swagger2.Schemes
if len(schemes) == 0 {
schemes = []string{"https"}
}
for _, scheme := range schemes {
serverURL := scheme + "://"
if len(swagger2.Host) > 0 {
serverURL += swagger2.Host
} else {
serverURL += "localhost"
}
if len(swagger2.BasePath) > 0 && swagger2.BasePath != "/" {
serverURL += swagger2.BasePath
}
openapi3Doc.Servers = append(openapi3Doc.Servers, &openapi3.Server{
URL: serverURL,
})
}
}
// Convert Paths
openapi3Doc.Paths = openapi3.NewPaths()
for path, pathItem := range swagger2.Paths.Paths {
newPathItem := &openapi3.PathItem{}
if pathItem.Get != nil {
newPathItem.Get = convertOperation(pathItem.Get)
}
if pathItem.Post != nil {
newPathItem.Post = convertOperation(pathItem.Post)
}
if pathItem.Put != nil {
newPathItem.Put = convertOperation(pathItem.Put)
}
if pathItem.Delete != nil {
newPathItem.Delete = convertOperation(pathItem.Delete)
}
if pathItem.Patch != nil {
newPathItem.Patch = convertOperation(pathItem.Patch)
}
if pathItem.Head != nil {
newPathItem.Head = convertOperation(pathItem.Head)
}
if pathItem.Options != nil {
newPathItem.Options = convertOperation(pathItem.Options)
}
openapi3Doc.Paths.Set(path, newPathItem)
}
// Convert Definitions to Components/Schemas
if len(swagger2.Definitions) > 0 {
openapi3Doc.Components = &openapi3.Components{
Schemas: make(openapi3.Schemas),
}
for name, schema := range swagger2.Definitions {
openapi3Doc.Components.Schemas[name] = convertSchemaToSchemaRef(&schema)
}
}
// Convert SecurityDefinitions to Components/SecuritySchemes
if len(swagger2.SecurityDefinitions) > 0 {
if openapi3Doc.Components == nil {
openapi3Doc.Components = &openapi3.Components{}
}
openapi3Doc.Components.SecuritySchemes = make(openapi3.SecuritySchemes)
for name, secDef := range swagger2.SecurityDefinitions {
openapi3Doc.Components.SecuritySchemes[name] = convertSecurityScheme(secDef)
}
}
return openapi3Doc
}
// convertOperation converts a Swagger 2.0 operation to OpenAPI 3.0
func convertOperation(op *spec.Operation) *openapi3.Operation {
newOp := &openapi3.Operation{
Tags: op.Tags,
Summary: op.Summary,
Description: op.Description,
OperationID: op.ID,
Parameters: openapi3.Parameters{},
Responses: &openapi3.Responses{},
Deprecated: op.Deprecated,
}
// Convert ExternalDocs
if op.ExternalDocs != nil {
newOp.ExternalDocs = &openapi3.ExternalDocs{
Description: op.ExternalDocs.Description,
URL: op.ExternalDocs.URL,
}
}
// Convert Parameters and RequestBody
var bodyParam *spec.Parameter
for _, param := range op.Parameters {
if param.In == "body" {
bodyParam = &param
} else {
newOp.Parameters = append(newOp.Parameters, convertParameter(&param))
}
}
// Convert body parameter to requestBody
if bodyParam != nil {
newOp.RequestBody = &openapi3.RequestBodyRef{
Value: convertBodyParameter(bodyParam, op.Consumes),
}
}
// Convert Responses
for code, response := range op.Responses.StatusCodeResponses {
newOp.Responses.Set(intToString(code), convertResponse(&response, op.Produces))
}
// Convert Security
if len(op.Security) > 0 {
newOp.Security = &openapi3.SecurityRequirements{}
for _, sec := range op.Security {
secReq := openapi3.SecurityRequirement{}
for key, scopes := range sec {
secReq[key] = scopes
}
*newOp.Security = append(*newOp.Security, secReq)
}
}
return newOp
}
// convertParameter converts a Swagger 2.0 parameter to OpenAPI 3.0
func convertParameter(param *spec.Parameter) *openapi3.ParameterRef {
schema := &openapi3.Schema{
Format: param.Format,
Default: param.Default,
}
if param.Type != "" {
schema.Type = &openapi3.Types{param.Type}
}
newParam := &openapi3.Parameter{
Name: param.Name,
In: param.In,
Description: param.Description,
Required: param.Required,
Schema: &openapi3.SchemaRef{Value: schema},
}
// Convert validation properties
if param.Maximum != nil {
max := *param.Maximum
newParam.Schema.Value.Max = &max
}
if param.Minimum != nil {
min := *param.Minimum
newParam.Schema.Value.Min = &min
}
if param.ExclusiveMaximum {
newParam.Schema.Value.ExclusiveMax = true
}
if param.ExclusiveMinimum {
newParam.Schema.Value.ExclusiveMin = true
}
if len(param.Enum) > 0 {
newParam.Schema.Value.Enum = param.Enum
}
// Handle array items
if param.Items != nil {
newParam.Schema.Value.Items = convertItemsToSchemaRef(param.Items)
}
return &openapi3.ParameterRef{Value: newParam}
}
// convertBodyParameter converts a body parameter to RequestBody
func convertBodyParameter(param *spec.Parameter, consumes []string) *openapi3.RequestBody {
if len(consumes) == 0 {
consumes = []string{"application/json"}
}
content := make(openapi3.Content)
for _, contentType := range consumes {
mediaType := &openapi3.MediaType{}
if param.Schema != nil {
mediaType.Schema = convertSchemaToSchemaRef(param.Schema)
}
content[contentType] = mediaType
}
return &openapi3.RequestBody{
Description: param.Description,
Required: param.Required,
Content: content,
}
}
// convertResponse converts a Swagger 2.0 response to OpenAPI 3.0
func convertResponse(response *spec.Response, produces []string) *openapi3.ResponseRef {
if len(produces) == 0 {
produces = []string{"application/json"}
}
newResp := &openapi3.Response{
Description: &response.Description,
}
if response.Schema != nil {
newResp.Content = make(openapi3.Content)
// 檢查是否有業務錯誤碼x-biz-codes
bizCodes, hasBizCodes := response.Extensions["x-biz-codes"]
if hasBizCodes {
// 使用 oneOf 來表示多個可能的回應類型
for _, contentType := range produces {
mediaType := &openapi3.MediaType{
Schema: createOneOfSchemaFromBizCodes(bizCodes),
}
newResp.Content[contentType] = mediaType
}
} else {
// 普通回應
for _, contentType := range produces {
mediaType := &openapi3.MediaType{
Schema: convertSchemaToSchemaRef(response.Schema),
}
newResp.Content[contentType] = mediaType
}
}
}
return &openapi3.ResponseRef{Value: newResp}
}
// createOneOfSchemaFromBizCodes 從業務錯誤碼創建 oneOf schema
func createOneOfSchemaFromBizCodes(bizCodesInterface interface{}) *openapi3.SchemaRef {
bizCodes, ok := bizCodesInterface.(map[string]BizCode)
if !ok {
// 嘗試轉換為 map[string]interface{}
bizCodesMap, ok := bizCodesInterface.(map[string]interface{})
if !ok {
return nil
}
// 轉換為 BizCode
bizCodes = make(map[string]BizCode)
for code, val := range bizCodesMap {
if valMap, ok := val.(map[string]interface{}); ok {
bizCode := BizCode{
Code: code,
}
if schema, ok := valMap["Schema"].(string); ok {
bizCode.Schema = schema
}
if desc, ok := valMap["Description"].(string); ok {
bizCode.Description = desc
}
bizCodes[code] = bizCode
}
}
}
if len(bizCodes) == 0 {
return nil
}
// 創建 oneOf schema
oneOfSchemas := make([]*openapi3.SchemaRef, 0, len(bizCodes))
for code, bizCode := range bizCodes {
// 創建每個業務錯誤碼的 schema 引用
ref := "#/components/schemas/" + bizCode.Schema
schemaRef := &openapi3.SchemaRef{
Ref: ref,
}
// 添加描述作為擴展
if bizCode.Description != "" {
schemaRef.Value = &openapi3.Schema{
Description: "業務錯誤碼 " + code + ": " + bizCode.Description,
}
}
oneOfSchemas = append(oneOfSchemas, schemaRef)
}
// 返回包含 oneOf 的 schema
return &openapi3.SchemaRef{
Value: &openapi3.Schema{
OneOf: oneOfSchemas,
},
}
}
// convertSchemaToSchemaRef converts a Swagger 2.0 schema to OpenAPI 3.0 SchemaRef
func convertSchemaToSchemaRef(schema *spec.Schema) *openapi3.SchemaRef {
if schema == nil {
return nil
}
// Handle $ref
if schema.Ref.String() != "" {
ref := schema.Ref.String()
// Convert #/definitions/ to #/components/schemas/
if len(ref) > 14 && ref[:14] == "#/definitions/" {
ref = "#/components/schemas/" + ref[14:]
}
return &openapi3.SchemaRef{Ref: ref}
}
newSchema := &openapi3.Schema{
Format: schema.Format,
Description: schema.Description,
Default: schema.Default,
Example: schema.Example,
Required: schema.Required,
}
// Convert Type from StringOrArray to *Types
if len(schema.Type) > 0 {
types := openapi3.Types(schema.Type)
newSchema.Type = &types
}
// Convert validation properties
if schema.Maximum != nil {
max := *schema.Maximum
newSchema.Max = &max
}
if schema.Minimum != nil {
min := *schema.Minimum
newSchema.Min = &min
}
if schema.ExclusiveMaximum {
newSchema.ExclusiveMax = true
}
if schema.ExclusiveMinimum {
newSchema.ExclusiveMin = true
}
if len(schema.Enum) > 0 {
newSchema.Enum = schema.Enum
}
// Convert properties
if len(schema.Properties) > 0 {
newSchema.Properties = make(openapi3.Schemas)
for name, prop := range schema.Properties {
newSchema.Properties[name] = convertSchemaToSchemaRef(&prop)
}
}
// Convert items
if schema.Items != nil && schema.Items.Schema != nil {
newSchema.Items = convertSchemaToSchemaRef(schema.Items.Schema)
}
// Convert additionalProperties
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil {
newSchema.AdditionalProperties = openapi3.AdditionalProperties{
Schema: convertSchemaToSchemaRef(schema.AdditionalProperties.Schema),
}
}
return &openapi3.SchemaRef{Value: newSchema}
}
// convertItemsToSchemaRef converts Swagger 2.0 items to OpenAPI 3.0 SchemaRef
func convertItemsToSchemaRef(items *spec.Items) *openapi3.SchemaRef {
if items == nil {
return nil
}
schema := &openapi3.Schema{
Format: items.Format,
}
if items.Type != "" {
schema.Type = &openapi3.Types{items.Type}
}
if items.Items != nil {
schema.Items = convertItemsToSchemaRef(items.Items)
}
return &openapi3.SchemaRef{Value: schema}
}
// convertSecurityScheme converts a Swagger 2.0 security definition to OpenAPI 3.0
func convertSecurityScheme(secDef *spec.SecurityScheme) *openapi3.SecuritySchemeRef {
newScheme := &openapi3.SecurityScheme{
Type: secDef.Type,
Description: secDef.Description,
Name: secDef.Name,
}
// Convert In to openapi3 format
switch secDef.In {
case "header":
newScheme.In = "header"
case "query":
newScheme.In = "query"
case "cookie":
newScheme.In = "cookie"
}
// Handle OAuth2
if secDef.Type == "oauth2" {
newScheme.Flows = &openapi3.OAuthFlows{}
switch secDef.Flow {
case "implicit":
newScheme.Flows.Implicit = &openapi3.OAuthFlow{
AuthorizationURL: secDef.AuthorizationURL,
Scopes: secDef.Scopes,
}
case "password":
newScheme.Flows.Password = &openapi3.OAuthFlow{
TokenURL: secDef.TokenURL,
Scopes: secDef.Scopes,
}
case "application":
newScheme.Flows.ClientCredentials = &openapi3.OAuthFlow{
TokenURL: secDef.TokenURL,
Scopes: secDef.Scopes,
}
case "accessCode":
newScheme.Flows.AuthorizationCode = &openapi3.OAuthFlow{
AuthorizationURL: secDef.AuthorizationURL,
TokenURL: secDef.TokenURL,
Scopes: secDef.Scopes,
}
}
}
return &openapi3.SecuritySchemeRef{Value: newScheme}
}
// intToString converts int to string for response codes
func intToString(code int) string {
if code < 0 || code > 999 {
return "200" // fallback to 200
}
hundreds := code / 100
tens := (code / 10) % 10
ones := code % 10
return string(rune('0'+hundreds)) + string(rune('0'+tens)) + string(rune('0'+ones))
}