net/goai: fix: #3660, support multiple file upload parameters for OpenAPIv3 (#3662)

This commit is contained in:
海亮 2024-06-25 21:01:58 +08:00 committed by GitHub
parent dba6c08548
commit e8a2629b19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 95 additions and 12 deletions

View File

@ -31,7 +31,42 @@ func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error)
return
}
// upload file request
type UploadReq struct {
g.Meta `path:"/upload" method:"POST" tags:"Upload" mime:"multipart/form-data" summary:"上传文件"`
Files []*ghttp.UploadFile `json:"files" type:"file" dc:"选择上传多文件"`
File *ghttp.UploadFile `p:"file" type:"file" dc:"选择上传文件"`
Msg string `dc:"消息"`
}
// upload file response
type UploadRes struct {
FilesName []string `json:"files_name"`
FileName string `json:"file_name"`
Msg string `json:"msg"`
}
// upload file
func (Hello) Upload(ctx context.Context, req *UploadReq) (res *UploadRes, err error) {
g.Log().Debugf(ctx, `receive say: %+v`, req)
res = &UploadRes{
Msg: req.Msg,
}
if req.File != nil {
res.FileName = req.File.Filename
}
if len(req.Files) > 0 {
var filesName []string
for _, file := range req.Files {
filesName = append(filesName, file.Filename)
}
res.FilesName = filesName
}
return
}
const (
// MySwaggerUITemplate is the custom Swagger UI template.
MySwaggerUITemplate = `
<!DOCTYPE html>
<html lang="en">
@ -55,6 +90,20 @@ const (
</script>
</body>
</html>
`
// OpenapiUITemplate is the OpenAPI UI template.
OpenapiUITemplate = `
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>test</title>
</head>
<body>
<div id="openapi-ui-container" spec-url="{SwaggerUIDocUrl}" theme="light"></div>
<script src="https://cdn.jsdelivr.net/npm/openapi-ui-dist@latest/lib/openapi-ui.umd.js"></script>
</body>
</html>
`
)
@ -67,5 +116,6 @@ func main() {
)
})
s.SetSwaggerUITemplate(MySwaggerUITemplate)
// s.SetSwaggerUITemplate(OpenapiUITemplate) // files support
s.Run()
}

View File

@ -54,7 +54,7 @@ type Schema struct {
MaxItems *uint64 `json:"maxItems,omitempty"`
Items *SchemaRef `json:"items,omitempty"`
Required []string `json:"required,omitempty"`
Properties Schemas `json:"properties,omitempty"`
Properties *Schemas `json:"properties,omitempty"`
MinProps uint64 `json:"minProperties,omitempty"`
MaxProps *uint64 `json:"maxProperties,omitempty"`
AdditionalProperties *SchemaRef `json:"additionalProperties,omitempty"`

View File

@ -73,6 +73,9 @@ func (oai *OpenApiV3) newSchemaRefWithGolangType(golangType reflect.Type, tagMap
if err := oai.tagMapToSchema(tagMap, schema); err != nil {
return nil, err
}
if oaiType == TypeArray && schema.Type == TypeFile {
schema.Type = TypeArray
}
}
schemaRef.Value = schema
switch oaiType {
@ -111,8 +114,7 @@ func (oai *OpenApiV3) newSchemaRefWithGolangType(golangType reflect.Type, tagMap
schemaRef.Value.Example = gconv.Bool(schemaRef.Value.Example)
}
// keep the example value as nil.
case
TypeArray:
case TypeArray:
subSchemaRef, err := oai.newSchemaRefWithGolangType(golangType.Elem(), nil)
if err != nil {
return nil, err
@ -123,8 +125,7 @@ func (oai *OpenApiV3) newSchemaRefWithGolangType(golangType reflect.Type, tagMap
schema.Enum = nil
}
case
TypeObject:
case TypeObject:
for golangType.Kind() == reflect.Ptr {
golangType = golangType.Elem()
}

View File

@ -14,8 +14,8 @@ type Schemas struct {
refs *gmap.ListMap // map[string]SchemaRef
}
func createSchemas() Schemas {
return Schemas{
func createSchemas() *Schemas {
return &Schemas{
refs: gmap.NewListMap(),
}
}
@ -26,7 +26,7 @@ func (s *Schemas) init() {
}
}
func (s *Schemas) Clone() Schemas {
func (s *Schemas) Clone() *Schemas {
newSchemas := createSchemas()
newSchemas.refs = s.refs.Clone()
return newSchemas

View File

@ -15,6 +15,7 @@ import (
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/net/goai"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/gmeta"
@ -466,6 +467,37 @@ func TestOpenApiV3_CommonRequest_SubDataField(t *testing.T) {
})
}
func TestOpenApiV3_CommonRequest_Files(t *testing.T) {
type Req struct {
g.Meta `path:"/upload" method:"POST" tags:"Upload" mime:"multipart/form-data" summary:"上传文件"`
Files []*ghttp.UploadFile `json:"files" type:"file" dc:"选择上传文件"`
File *ghttp.UploadFile `json:"file" type:"file" dc:"选择上传文件"`
}
type Res struct {
}
f := func(ctx context.Context, req *Req) (res *Res, err error) {
return
}
gtest.C(t, func(t *gtest.T) {
var (
err error
oai = goai.New()
)
err = oai.Add(goai.AddInput{
Path: "/upload",
Method: http.MethodGet,
Object: f,
})
t.AssertNil(err)
// fmt.Println(oai.String())
t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.net.goai_test.Req`).Value.Properties.Get("files").Value.Type, goai.TypeArray)
t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.net.goai_test.Req`).Value.Properties.Get("file").Value.Type, goai.TypeFile)
})
}
func TestOpenApiV3_CommonResponse(t *testing.T) {
type CommonResponse struct {
Code int `json:"code" description:"Error code"`
@ -1007,7 +1039,7 @@ func Test_EmbeddedStructAttribute(t *testing.T) {
b, err := json.Marshal(oai)
t.AssertNil(err)
t.Assert(b, `{"openapi":"3.0.0","components":{"schemas":{"github.com.gogf.gf.v2.net.goai_test.CreateResourceReq":{"properties":{"Name":{"description":"This is name.","format":"string","properties":{},"type":"string"},"Embedded":{"properties":{"Age":{"description":"This is embedded age.","format":"uint","properties":{},"type":"integer"}},"type":"object"}},"type":"object"}}},"info":{"title":"","version":""},"paths":null}`)
t.Assert(b, `{"openapi":"3.0.0","components":{"schemas":{"github.com.gogf.gf.v2.net.goai_test.CreateResourceReq":{"properties":{"Name":{"description":"This is name.","format":"string","type":"string"},"Embedded":{"properties":{"Age":{"description":"This is embedded age.","format":"uint","type":"integer"}},"type":"object"}},"type":"object"}}},"info":{"title":"","version":""},"paths":null}`)
})
}
@ -1031,7 +1063,7 @@ func Test_NameFromJsonTag(t *testing.T) {
b, err := json.Marshal(oai)
t.AssertNil(err)
t.Assert(b, `{"openapi":"3.0.0","components":{"schemas":{"github.com.gogf.gf.v2.net.goai_test.CreateReq":{"properties":{"nick_name":{"format":"string","properties":{},"type":"string"}},"type":"object"}}},"info":{"title":"","version":""},"paths":null}`)
t.Assert(b, `{"openapi":"3.0.0","components":{"schemas":{"github.com.gogf.gf.v2.net.goai_test.CreateReq":{"properties":{"nick_name":{"format":"string","type":"string"}},"type":"object"}}},"info":{"title":"","version":""},"paths":null}`)
})
// GET
gtest.C(t, func(t *gtest.T) {
@ -1052,7 +1084,7 @@ func Test_NameFromJsonTag(t *testing.T) {
b, err := json.Marshal(oai)
t.AssertNil(err)
fmt.Println(string(b))
t.Assert(b, `{"openapi":"3.0.0","components":{"schemas":{"github.com.gogf.gf.v2.net.goai_test.CreateReq":{"properties":{"nick_name":{"format":"string","properties":{},"type":"string"}},"type":"object"}}},"info":{"title":"","version":""},"paths":null}`)
t.Assert(b, `{"openapi":"3.0.0","components":{"schemas":{"github.com.gogf.gf.v2.net.goai_test.CreateReq":{"properties":{"nick_name":{"format":"string","type":"string"}},"type":"object"}}},"info":{"title":"","version":""},"paths":null}`)
})
}
@ -1188,6 +1220,6 @@ func Test_XExtension(t *testing.T) {
Object: req,
})
t.AssertNil(err)
t.Assert(oai.String(), `{"openapi":"3.0.0","components":{"schemas":{"github.com.gogf.gf.v2.net.goai_test.GetListReq":{"properties":{"Page":{"default":1,"description":"Page number","format":"int","properties":{},"type":"integer","x-sort":"1"},"Size":{"default":10,"description":"Size for per page.","format":"int","properties":{},"type":"integer","x-sort":"2"}},"type":"object","x-group":"User/Info"}}},"info":{"title":"","version":""},"paths":null}`)
t.Assert(oai.String(), `{"openapi":"3.0.0","components":{"schemas":{"github.com.gogf.gf.v2.net.goai_test.GetListReq":{"properties":{"Page":{"default":1,"description":"Page number","format":"int","type":"integer","x-sort":"1"},"Size":{"default":10,"description":"Size for per page.","format":"int","type":"integer","x-sort":"2"}},"type":"object","x-group":"User/Info"}}},"info":{"title":"","version":""},"paths":null}`)
})
}