997 lines
23 KiB
Go
997 lines
23 KiB
Go
/*
|
|
* Copyright 2025 coze-dev Authors
|
|
*
|
|
* 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 openapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/getkin/kin-openapi/openapi2"
|
|
"github.com/getkin/kin-openapi/openapi2conv"
|
|
"github.com/getkin/kin-openapi/openapi3"
|
|
gonanoid "github.com/matoous/go-nanoid"
|
|
"github.com/mattn/go-shellwords"
|
|
postman "github.com/rbretecher/go-postman-collection"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
|
|
"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/logs"
|
|
"github.com/coze-dev/coze-studio/backend/types/errno"
|
|
)
|
|
|
|
func CurlToOpenapi3Doc(ctx context.Context, rawCURL string) (doc *model.Openapi3T, mf *entity.PluginManifest, err error) {
|
|
curlReq, err := parseCURL(ctx, rawCURL)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
rawURL := addHTTPProtocolHeadIfNeed(curlReq.RawURL)
|
|
|
|
urlSchema, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
doc = entity.NewDefaultOpenapiDoc()
|
|
doc.Servers = append(doc.Servers, &openapi3.Server{
|
|
URL: urlSchema.Scheme + "://" + urlSchema.Host,
|
|
})
|
|
|
|
operationID := gonanoid.MustID(6)
|
|
doc.Info.Title = fmt.Sprintf("curl_%s", operationID)
|
|
doc.Info.Description = curlReq.Method + ":" + urlSchema.Path
|
|
|
|
op := &openapi3.Operation{
|
|
OperationID: operationID,
|
|
Summary: curlReq.Method + ":" + urlSchema.Path,
|
|
Parameters: openapi3.Parameters{},
|
|
Responses: entity.DefaultOpenapi3Responses(),
|
|
}
|
|
|
|
if len(curlReq.Header) > 0 {
|
|
op, err = curlHeaderToOpenAPI(ctx, curlReq.Header, op)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
if len(urlSchema.Query()) > 0 {
|
|
op, err = curlQueryToOpenAPI(ctx, urlSchema.Query(), op)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
if len(curlReq.Header["Content-Type"]) > 0 {
|
|
mediaType := curlReq.Header["Content-Type"][0]
|
|
op, err = curlBodyToOpenAPI(ctx, mediaType, curlReq.Body, op)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
pathItem := &openapi3.PathItem{}
|
|
pathItem.SetOperation(strings.ToUpper(curlReq.Method), op)
|
|
doc.Paths = openapi3.Paths{
|
|
urlSchema.Path: pathItem,
|
|
}
|
|
|
|
fillNecessaryInfoForOpenapi3Doc(doc)
|
|
|
|
mf = entity.NewDefaultPluginManifest()
|
|
fillManifestWithOpenapiDoc(mf, doc)
|
|
|
|
return doc, mf, nil
|
|
}
|
|
|
|
type curlRequest struct {
|
|
RawURL string
|
|
Method string
|
|
Query url.Values
|
|
Header http.Header
|
|
Body any
|
|
|
|
dataToQuery bool
|
|
}
|
|
|
|
func parseCURL(_ context.Context, rawCURL string) (req *curlRequest, err error) {
|
|
lines, err := shellwords.Parse(rawCURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(lines) < 2 {
|
|
return nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"invalid curl command"))
|
|
}
|
|
|
|
req = &curlRequest{
|
|
Method: "",
|
|
Header: http.Header{},
|
|
Query: url.Values{},
|
|
Body: map[string]any{}, // TODO(@maronghong): 支持 array
|
|
dataToQuery: false,
|
|
}
|
|
|
|
length := len(lines)
|
|
|
|
for i := 0; i < length; {
|
|
line := strings.Trim(lines[i], "\n")
|
|
|
|
if urlSchema, ok := isValidHTTPURL(line); ok {
|
|
req.RawURL = line
|
|
req.Query = urlSchema.Query()
|
|
i++
|
|
continue
|
|
}
|
|
|
|
switch line {
|
|
case "-X", "--request":
|
|
i, err = req.parseCURLMethod(i, lines)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case "-G", "--get":
|
|
i++
|
|
req.dataToQuery = true
|
|
|
|
case "-H", "--header":
|
|
i, err = req.parseCURLHeader(i, lines)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case "-b", "--cookie":
|
|
i++
|
|
if i >= length {
|
|
return nil, fmt.Errorf("cookie not found")
|
|
}
|
|
req.Header.Add("Cookie", strings.TrimLeft(lines[i], " "))
|
|
|
|
case "-e", "--referer":
|
|
i++
|
|
if i >= length {
|
|
return nil, fmt.Errorf("referer not found")
|
|
}
|
|
req.Header.Add("Referer", strings.TrimLeft(lines[i], " "))
|
|
|
|
case "-A", "--user-agent":
|
|
i++
|
|
if i >= length {
|
|
return nil, fmt.Errorf("user-agent not found")
|
|
}
|
|
req.Header.Add("User-Agent", strings.TrimLeft(lines[i], " "))
|
|
|
|
default:
|
|
i++
|
|
continue
|
|
}
|
|
}
|
|
|
|
if req.RawURL == "" {
|
|
return nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"invalid request url, url must start with 'http://' or 'https://'"))
|
|
}
|
|
|
|
for i := 0; i < length; {
|
|
line := strings.Trim(lines[i], "\n")
|
|
|
|
switch line {
|
|
case "-d", "--data", "--data-urlencode":
|
|
i, err = req.parseCURLData(i, lines)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
i++
|
|
continue
|
|
}
|
|
}
|
|
|
|
if req.Method == "" {
|
|
req.Method = http.MethodGet
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func isValidHTTPURL(str string) (*url.URL, bool) {
|
|
p, err := url.Parse(str)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
if p.Host == "" {
|
|
return p, false
|
|
}
|
|
if p.Scheme == "http" || p.Scheme == "https" {
|
|
return p, true
|
|
}
|
|
return p, false
|
|
}
|
|
|
|
func (c *curlRequest) parseCURLMethod(curIdx int, lines []string) (nxtIdx int, err error) {
|
|
nxtIdx = curIdx + 2
|
|
if curIdx+1 >= len(lines) {
|
|
return 0, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"request method not found"))
|
|
}
|
|
|
|
c.Method = strings.ToUpper(lines[curIdx+1])
|
|
|
|
return nxtIdx, nil
|
|
}
|
|
|
|
func (c *curlRequest) parseCURLHeader(curIdx int, lines []string) (nxtIdx int, err error) {
|
|
nxtIdx = curIdx + 2
|
|
if curIdx+1 >= len(lines) {
|
|
return 0, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"header not found"))
|
|
}
|
|
|
|
nxtLine := lines[curIdx+1]
|
|
header := strings.SplitN(nxtLine, ":", 2)
|
|
|
|
if len(header) < 2 {
|
|
return 0, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"invalid header: %s", nxtLine))
|
|
}
|
|
|
|
c.Header.Add(strings.TrimLeft(header[0], " "), strings.TrimLeft(header[1], " "))
|
|
|
|
return nxtIdx, nil
|
|
}
|
|
|
|
func (c *curlRequest) parseCURLData(curIdx int, lines []string) (nxtIdx int, err error) {
|
|
if c.Method == "" && !c.dataToQuery {
|
|
c.Method = http.MethodPost
|
|
}
|
|
|
|
nxtIdx = curIdx + 2
|
|
if curIdx+1 >= len(lines) {
|
|
return 0, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"request body data not found"))
|
|
}
|
|
|
|
var mediaType string
|
|
ct := c.Header["Content-Type"]
|
|
if len(ct) > 0 {
|
|
mediaType = ct[0]
|
|
} else {
|
|
mediaType = model.MediaTypeFormURLEncoded
|
|
c.Header["Content-Type"] = append(c.Header["Content-Type"], mediaType)
|
|
}
|
|
|
|
data := lines[curIdx+1]
|
|
|
|
switch mediaType {
|
|
case model.MediaTypeFormURLEncoded:
|
|
err = c.decodeFormUrlEncodedDataBody(data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
case model.MediaTypeJson, model.MediaTypeProblemJson:
|
|
err = c.decodeJsonDataBody(data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
case model.MediaTypeYaml, model.MediaTypeXYaml:
|
|
err = c.decodeYamlDataBody(data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
default:
|
|
return 0, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"unsupported request media type '%s'", mediaType))
|
|
}
|
|
|
|
return nxtIdx, nil
|
|
}
|
|
|
|
func (c *curlRequest) decodeJsonDataBody(data string) error {
|
|
valMap := map[string]any{}
|
|
err := json.Unmarshal([]byte(data), &valMap)
|
|
if err != nil {
|
|
return errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"request body only supports 'object' type, err=%s", err))
|
|
}
|
|
|
|
if !c.dataToQuery {
|
|
c.Body = valMap
|
|
} else {
|
|
for k, v := range valMap {
|
|
if v == nil {
|
|
continue
|
|
}
|
|
c.Query.Add(k, fmt.Sprintf("%v", v))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *curlRequest) decodeYamlDataBody(data string) error {
|
|
valMap := map[string]any{}
|
|
err := yaml.Unmarshal([]byte(data), &valMap)
|
|
if err != nil {
|
|
return errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"request body only supports 'object' type, err=%s", err))
|
|
}
|
|
|
|
if !c.dataToQuery {
|
|
c.Body = valMap
|
|
} else {
|
|
for k, v := range valMap {
|
|
if v == nil {
|
|
continue
|
|
}
|
|
c.Query.Add(k, fmt.Sprintf("%v", v))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *curlRequest) decodeFormUrlEncodedDataBody(data string) error {
|
|
values, err := url.ParseQuery(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.dataToQuery {
|
|
for k, v := range values {
|
|
if len(v) == 0 {
|
|
continue
|
|
}
|
|
c.Query.Add(k, v[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
body := c.Body.(map[string]any)
|
|
for k, v := range values {
|
|
if len(v) == 0 {
|
|
continue
|
|
}
|
|
if body[k] == nil {
|
|
body[k] = v[0]
|
|
continue
|
|
}
|
|
|
|
item, ok := body[k].([]any)
|
|
if !ok {
|
|
item = []any{body[k]}
|
|
}
|
|
|
|
item = append(item, v[0])
|
|
body[k] = item
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func curlHeaderToOpenAPI(_ context.Context, header http.Header, op *openapi3.Operation) (newOP *openapi3.Operation, err error) {
|
|
for k, v := range header {
|
|
if k == "Content-Type" {
|
|
continue
|
|
}
|
|
|
|
paramSchema := &openapi3.Parameter{
|
|
In: openapi3.ParameterInHeader,
|
|
Name: k,
|
|
Description: k,
|
|
Required: true,
|
|
}
|
|
|
|
if len(v) > 1 {
|
|
paramSchema.Schema = &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeArray,
|
|
Items: &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeString,
|
|
Description: k,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
} else {
|
|
paramSchema.Schema = &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeString,
|
|
Description: k,
|
|
},
|
|
}
|
|
}
|
|
|
|
op.Parameters = append(op.Parameters, &openapi3.ParameterRef{
|
|
Value: paramSchema,
|
|
})
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
func curlQueryToOpenAPI(_ context.Context, queryParams url.Values, op *openapi3.Operation) (newOP *openapi3.Operation, err error) {
|
|
for k, v := range queryParams {
|
|
if v == nil {
|
|
continue
|
|
}
|
|
|
|
paramSchema := &openapi3.Parameter{
|
|
In: openapi3.ParameterInQuery,
|
|
Name: k,
|
|
Description: k,
|
|
Required: true,
|
|
}
|
|
|
|
if len(v) > 1 {
|
|
paramSchema.Schema = &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeArray,
|
|
Items: &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeString,
|
|
Description: k,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
} else {
|
|
paramSchema.Schema = &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeString,
|
|
Description: k,
|
|
},
|
|
}
|
|
}
|
|
|
|
op.Parameters = append(op.Parameters, &openapi3.ParameterRef{
|
|
Value: paramSchema,
|
|
})
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
func parseRequestToBodySchemaRef(ctx context.Context, desc string, value any) (*openapi3.SchemaRef, error) {
|
|
switch val := value.(type) {
|
|
case string:
|
|
return &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeString,
|
|
Description: desc,
|
|
},
|
|
}, nil
|
|
|
|
case bool:
|
|
return &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeBoolean,
|
|
Description: desc,
|
|
},
|
|
}, nil
|
|
|
|
case float64: // in most cases, it's integer
|
|
return &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeInteger,
|
|
Description: desc,
|
|
},
|
|
}, nil
|
|
|
|
case map[string]any:
|
|
properties := map[string]*openapi3.SchemaRef{}
|
|
required := make([]string, 0, len(val))
|
|
for k, subVal := range val {
|
|
sc, err := parseRequestToBodySchemaRef(ctx, k, subVal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sc == nil {
|
|
continue
|
|
}
|
|
required = append(required, k)
|
|
properties[k] = sc
|
|
}
|
|
|
|
if len(properties) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeObject,
|
|
Properties: properties,
|
|
Required: required,
|
|
},
|
|
}, nil
|
|
|
|
case []any:
|
|
if len(val) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
item, err := parseRequestToBodySchemaRef(ctx, desc, val[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeArray,
|
|
Description: desc,
|
|
Items: item,
|
|
},
|
|
}, nil
|
|
|
|
default:
|
|
logs.CtxWarnf(ctx, "unsupported type: %T", val)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func curlBodyToOpenAPI(ctx context.Context, mediaType string, bodyValue any, op *openapi3.Operation) (newOP *openapi3.Operation, err error) {
|
|
bodyValue, ok := bodyValue.(map[string]any) // TODO(@maronghong): 支持 array
|
|
if !ok {
|
|
return nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"request body only supports 'object' type"))
|
|
}
|
|
|
|
bodySchema, err := parseRequestToBodySchemaRef(ctx, "", bodyValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bodySchema == nil {
|
|
return op, nil
|
|
}
|
|
|
|
if mediaType == "" {
|
|
mediaType = model.MediaTypeJson
|
|
}
|
|
|
|
op.RequestBody = &openapi3.RequestBodyRef{
|
|
Value: &openapi3.RequestBody{
|
|
Content: map[string]*openapi3.MediaType{
|
|
mediaType: {
|
|
Schema: bodySchema,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
func PostmanToOpenapi3Doc(ctx context.Context, rawPostman string) (doc *model.Openapi3T, mf *entity.PluginManifest, err error) {
|
|
collection, err := postman.ParseCollection(bytes.NewBufferString(rawPostman))
|
|
if err != nil {
|
|
return nil, nil, errorx.New(errno.ErrPluginConvertProtocolFailed,
|
|
errorx.KV(errno.PluginMsgKey, err.Error()))
|
|
}
|
|
|
|
if len(collection.Items) == 0 {
|
|
return nil, nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"no request found in collection"))
|
|
}
|
|
|
|
item0 := collection.Items[0]
|
|
if item0 == nil || item0.Request == nil || item0.Request.URL == nil {
|
|
return nil, nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"invalid collection request schema"))
|
|
}
|
|
|
|
rawURL := addHTTPProtocolHeadIfNeed(collection.Items[0].Request.URL.Raw)
|
|
|
|
urlSchema, ok := isValidHTTPURL(rawURL)
|
|
if !ok {
|
|
return nil, nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"invalid request url '%s', url must start with 'http://' or 'https://'", rawURL))
|
|
}
|
|
|
|
doc = entity.NewDefaultOpenapiDoc()
|
|
doc.Servers = append(doc.Servers, &openapi3.Server{
|
|
URL: urlSchema.Scheme + "://" + urlSchema.Host,
|
|
})
|
|
doc.Info.Title = collection.Info.Name
|
|
doc.Info.Description = collection.Info.Description.Content
|
|
|
|
var buildOperation func(item *postman.Items) error
|
|
buildOperation = func(item *postman.Items) error {
|
|
if item == nil || item.Request == nil || item.Request.URL == nil {
|
|
return errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KV(errno.PluginMsgKey,
|
|
"invalid request schema"))
|
|
}
|
|
|
|
itemReq := item.Request
|
|
|
|
op := &openapi3.Operation{
|
|
OperationID: item.Name,
|
|
Summary: item.Description,
|
|
Parameters: openapi3.Parameters{},
|
|
Responses: entity.DefaultOpenapi3Responses(),
|
|
}
|
|
|
|
var mediaType string
|
|
op, mediaType, err = postmanHeaderToOpenAPI(ctx, itemReq.Header, op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
op, err = postmanQueryToOpenAPI(ctx, itemReq.URL.Query, op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
op, err = postmanBodyToOpenAPI(ctx, mediaType, itemReq.Body, op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pathItem := &openapi3.PathItem{}
|
|
pathItem.SetOperation(strings.ToUpper(string(item.Request.Method)), op)
|
|
path := "/" + strings.Join(item.Request.URL.Path, "/")
|
|
|
|
if doc.Paths[path] != nil {
|
|
return errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"duplicated tool '[%s]:%s'", itemReq.Method, path))
|
|
}
|
|
|
|
doc.Paths[path] = pathItem
|
|
|
|
for _, sub := range item.Items {
|
|
err = buildOperation(sub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
for _, item := range collection.Items {
|
|
err = buildOperation(item)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
fillNecessaryInfoForOpenapi3Doc(doc)
|
|
|
|
mf = entity.NewDefaultPluginManifest()
|
|
fillManifestWithOpenapiDoc(mf, doc)
|
|
|
|
return doc, mf, nil
|
|
}
|
|
|
|
func postmanHeaderToOpenAPI(_ context.Context, headers []*postman.Header, op *openapi3.Operation) (newOP *openapi3.Operation, mediaType string, err error) {
|
|
for _, header := range headers {
|
|
if header == nil {
|
|
continue
|
|
}
|
|
|
|
if header.Key == "Content-Type" {
|
|
mediaType = header.Value
|
|
}
|
|
|
|
desc := header.Description
|
|
if desc == "" {
|
|
desc = header.Key
|
|
}
|
|
|
|
op.Parameters = append(op.Parameters, &openapi3.ParameterRef{
|
|
Value: &openapi3.Parameter{
|
|
In: openapi3.ParameterInHeader,
|
|
Name: header.Key,
|
|
Description: desc,
|
|
Required: true,
|
|
Schema: &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Description: desc,
|
|
Type: openapi3.TypeString,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return op, mediaType, nil
|
|
}
|
|
|
|
func postmanQueryToOpenAPI(_ context.Context, queryParams []*postman.QueryParam, op *openapi3.Operation) (newOP *openapi3.Operation, err error) {
|
|
for _, queryParam := range queryParams {
|
|
if queryParam == nil {
|
|
continue
|
|
}
|
|
|
|
desc := ptr.FromOrDefault(queryParam.Description, "")
|
|
if desc == "" {
|
|
desc = queryParam.Key
|
|
}
|
|
|
|
op.Parameters = append(op.Parameters, &openapi3.ParameterRef{
|
|
Value: &openapi3.Parameter{
|
|
In: openapi3.ParameterInQuery,
|
|
Name: queryParam.Key,
|
|
Description: desc,
|
|
Required: true,
|
|
Schema: &openapi3.SchemaRef{
|
|
Value: &openapi3.Schema{
|
|
Type: openapi3.TypeString,
|
|
Description: desc,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
func postmanBodyToOpenAPI(ctx context.Context, mediaType string, body *postman.Body, op *openapi3.Operation) (newOP *openapi3.Operation, err error) {
|
|
if body == nil {
|
|
return op, nil
|
|
}
|
|
|
|
if body.Mode != "raw" && body.Mode != "urlencoded" {
|
|
return op, nil
|
|
}
|
|
|
|
if body.Mode == "raw" {
|
|
if body.Options == nil {
|
|
return op, nil
|
|
}
|
|
if body.Options.Raw.Language != "json" && body.Options.Raw.Language != "text" {
|
|
return op, nil
|
|
}
|
|
}
|
|
|
|
if mediaType == "" {
|
|
mediaType = model.MediaTypeJson
|
|
if body.Mode == "urlencoded" {
|
|
mediaType = model.MediaTypeFormURLEncoded
|
|
}
|
|
}
|
|
|
|
var valMap map[string]any
|
|
switch mediaType {
|
|
case model.MediaTypeJson, model.MediaTypeProblemJson:
|
|
valMap, err = decodeRequestJsonBody(body.Raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case model.MediaTypeYaml, model.MediaTypeXYaml:
|
|
valMap, err = decodeRequestYamlBody(body.Raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case model.MediaTypeFormURLEncoded:
|
|
valMap, err = decodePostmanRequestFormURLEncodedBody(body.URLEncoded)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
default:
|
|
return op, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"unsupported request media type '%s'", mediaType))
|
|
}
|
|
|
|
if len(valMap) == 0 {
|
|
return op, nil
|
|
}
|
|
|
|
bodySchema, err := parseRequestToBodySchemaRef(ctx, "", valMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bodySchema == nil {
|
|
return op, nil
|
|
}
|
|
|
|
op.RequestBody = &openapi3.RequestBodyRef{
|
|
Value: &openapi3.RequestBody{
|
|
Content: map[string]*openapi3.MediaType{
|
|
mediaType: {
|
|
Schema: bodySchema,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
func decodeRequestJsonBody(rawBody string) (body map[string]any, err error) {
|
|
valMap := map[string]any{}
|
|
err = json.Unmarshal([]byte(rawBody), &valMap)
|
|
if err != nil {
|
|
return nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"request body only supports 'object' type, err=%s", err))
|
|
}
|
|
|
|
return valMap, nil
|
|
}
|
|
|
|
func decodeRequestYamlBody(rawBody string) (body map[string]any, err error) {
|
|
valMap := map[string]any{}
|
|
err = yaml.Unmarshal([]byte(rawBody), &valMap)
|
|
if err != nil {
|
|
return nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"request body only supports 'object' type, err=%s", err))
|
|
}
|
|
|
|
return valMap, nil
|
|
}
|
|
|
|
func decodePostmanRequestFormURLEncodedBody(rawBody any) (body map[string]any, err error) {
|
|
valArr, ok := rawBody.([]any)
|
|
if !ok {
|
|
return nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"postman urlencoded body should be array type"))
|
|
}
|
|
|
|
body = map[string]any{}
|
|
for _, v := range valArr {
|
|
m, ok := v.(map[string]any)
|
|
if !ok {
|
|
return nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"postman urlencoded body should be array of 'object' type"))
|
|
}
|
|
|
|
key, ok := m["key"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
val, ok := m["value"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if body[key] == nil {
|
|
body[key] = val
|
|
continue
|
|
}
|
|
|
|
item, ok := body[key].([]any)
|
|
if !ok {
|
|
item = []any{body[key]}
|
|
}
|
|
|
|
item = append(item, val)
|
|
body[key] = item
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func SwaggerToOpenapi3Doc(_ context.Context, rawSwagger string) (doc *model.Openapi3T, mf *entity.PluginManifest, err error) {
|
|
doc2 := &openapi2.T{}
|
|
if err = json.Unmarshal([]byte(rawSwagger), doc2); err != nil {
|
|
err = yaml.Unmarshal([]byte(rawSwagger), doc2)
|
|
if err != nil {
|
|
return nil, nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"invalid swagger schema, err=%s", err))
|
|
}
|
|
}
|
|
|
|
doc3, err := openapi2conv.ToV3(doc2)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
doc = ptr.Of(model.Openapi3T(*doc3))
|
|
fillNecessaryInfoForOpenapi3Doc(doc)
|
|
|
|
mf = entity.NewDefaultPluginManifest()
|
|
fillManifestWithOpenapiDoc(mf, doc)
|
|
|
|
return doc, mf, nil
|
|
}
|
|
|
|
func ToOpenapi3Doc(_ context.Context, rawOpenAPI string) (doc *model.Openapi3T, mf *entity.PluginManifest, err error) {
|
|
loader := openapi3.NewLoader()
|
|
doc3, err := loader.LoadFromData([]byte(rawOpenAPI))
|
|
if err != nil {
|
|
return nil, nil, errorx.New(errno.ErrPluginConvertProtocolFailed, errorx.KVf(errno.PluginMsgKey,
|
|
"invalid openapi3 document, err=%s", err))
|
|
}
|
|
|
|
doc = ptr.Of(model.Openapi3T(*doc3))
|
|
fillNecessaryInfoForOpenapi3Doc(doc)
|
|
|
|
mf = entity.NewDefaultPluginManifest()
|
|
fillManifestWithOpenapiDoc(mf, doc)
|
|
|
|
return doc, mf, nil
|
|
}
|
|
|
|
func fillManifestWithOpenapiDoc(mf *entity.PluginManifest, doc *model.Openapi3T) {
|
|
if doc.Info == nil {
|
|
return
|
|
}
|
|
|
|
mf.NameForHuman = doc.Info.Title
|
|
mf.NameForModel = doc.Info.Title
|
|
mf.DescriptionForHuman = doc.Info.Description
|
|
mf.DescriptionForModel = doc.Info.Description
|
|
|
|
return
|
|
}
|
|
|
|
func addHTTPProtocolHeadIfNeed(url string) string {
|
|
if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") {
|
|
return url
|
|
}
|
|
return "http://" + url
|
|
}
|
|
|
|
func fillNecessaryInfoForOpenapi3Doc(doc *model.Openapi3T) {
|
|
if doc.Info == nil {
|
|
doc.Info = &openapi3.Info{
|
|
Title: "title is required",
|
|
Version: "v1",
|
|
Description: "description is required",
|
|
}
|
|
}
|
|
if doc.Info.Title == "" {
|
|
doc.Info.Title = "title is required"
|
|
}
|
|
if doc.Info.Description == "" {
|
|
doc.Info.Description = doc.Info.Title
|
|
}
|
|
if doc.Info.Version == "" {
|
|
doc.Info.Version = "v1"
|
|
}
|
|
|
|
for _, pathItem := range doc.Paths {
|
|
for _, op := range pathItem.Operations() {
|
|
if op.OperationID == "" {
|
|
op.OperationID = gonanoid.MustID(6)
|
|
}
|
|
|
|
if op.Summary == "" {
|
|
op.Summary = op.OperationID
|
|
}
|
|
|
|
if op.Responses != nil {
|
|
defaultResp := entity.DefaultOpenapi3Responses()
|
|
respRef := op.Responses[strconv.Itoa(http.StatusOK)]
|
|
if respRef == nil || respRef.Value == nil || respRef.Value.Content == nil {
|
|
op.Responses = defaultResp
|
|
respRef = op.Responses[strconv.Itoa(http.StatusOK)]
|
|
}
|
|
if respRef.Value.Content[model.MediaTypeJson] == nil {
|
|
respRef.Value.Content[model.MediaTypeJson] = defaultResp[strconv.Itoa(http.StatusOK)].Value.Content[model.MediaTypeJson]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|