gogf / gf

GoFrame is a modular, powerful, high-performance and enterprise-class application development framework of Golang.
https://goframe.org
MIT License
11.72k stars 1.59k forks source link

net/goai: I want to customize the response 500 content #3747

Closed ivothgle closed 4 weeks ago

ivothgle commented 2 months ago

Go version

go1.22

GoFrame version

2.7.0

Can this bug be reproduced with the latest release?

Option Yes

What did you do?

    req.Server.GetOpenApi().Add(goai.AddInput{
        Object: req.Server.GetOpenApi().Config.CommonResponse,
    })
    req.Server.GetOpenApi().Components.Responses = goai.Responses{
        "InternalServerError": goai.ResponseRef{
            Value: &goai.Response{
                Description: "InternalServerError",
                Content: map[string]goai.MediaType{
                    "application/json": {Schema: &goai.SchemaRef{Ref: "model.DefaultHandlerResponse"}},
                },
            },
        },
    }

I want to customize the response 500 content,

output "500": { "$ref": "#/components/schemas/InternalServerError" } He should be "#/components/responses/InternalServerError", see https://swagger.io/docs/specification/describing-responses/

What did you see happen?

It is not possible to customize the reference response structure in OpenAI because its prefix is ​​always fixed

code is: https://github.com/gogf/gf/blob/6e5ce98d23c65fd7283c84382abfde62a29f9930/net/goai/goai.go#L227

What did you expect to see?

He should be "#/components/responses/InternalServerError", see https://swagger.io/docs/specification/describing-responses/

shuqingzai commented 2 months ago

GF 目前处理响应只有一个结果 200

See: https://github.com/gogf/gf/blob/6e5ce98d23c65fd7283c84382abfde62a29f9930/net/goai/goai_path.go#L239

无法自定义状态码,在一些 2xx 状态中代表不同含义,参考: 201 Created

需要根据这些状态码响应

  1. 创建成功,希望可以设置为 201
  2. 异步任务,延迟处理的,希望为 202

目前 GF 没有支持这个,有一个想法参考:

xxxRes.Meta 中设置 successStatusCodeerrorStatusCode 标签,框架可以解析且设置对应的 HTTP Status Code 文档

server:
  address:     ":8199"
  openapiPath: "/api.json"
  swaggerPath: "/swagger"
package main

import (
    "context"
    "net/http"

    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
    "github.com/gogf/gf/v2/net/goai"
)

type HelloCreateReq struct {
    g.Meta `path:"/hello" method:"POST"`

    Name string `v:"required" dc:"Your name"`
    Age  int    `v:"required|between:1,200" dc:"Your age"`
}

type HelloCreateRes struct {
    g.Meta `successStatusCode:"201" errorStatusCode:"400,500"`

    ID uint64 `json:"id,string" dc:"ID"`
}

type HelloGetReq struct {
    g.Meta `path:"/hello/{id}" method:"GET"`

    // ID ID
    ID uint64 `json:"id,string" dc:"ID" in:"path" v:"required"`
}

type HelloGetRes struct {
    g.Meta `errorStatusCode:"400,500"`

    ID   uint64 `json:"id,string" dc:"ID"`
    Name string `json:"name" dc:"Name"`
    Age  int    `json:"age" dc:"Age"`
}

type Hello struct{}

func (c *Hello) Create(ctx context.Context, r *HelloCreateReq) (*HelloCreateRes, error) {
    return &HelloCreateRes{ID: 1}, nil
}

func (c *Hello) Get(ctx context.Context, r *HelloGetReq) (*HelloGetRes, error) {
    return &HelloGetRes{
        ID:   r.ID,
        Name: "john",
        Age:  18,
    }, nil
}

func main() {
    s := g.Server()
    s.Use(ghttp.MiddlewareHandlerResponse)
    s.Group("/", func(group *ghttp.RouterGroup) {
        group.Bind(
            new(Hello),
        )
    })

    // 设置响应数据结构
    oai := s.GetOpenApi()
    oai.Config.CommonResponse = ghttp.DefaultHandlerResponse{}
    oai.Config.CommonResponseDataField = "Data"

    // 错误的响应
    dataResp := &goai.Schemas{}
    dataResp.Set("code", goai.SchemaRef{
        Value: &goai.Schema{
            Type:        "integer",
            Format:      "int32",
            Title:       "业务状态码",
            Description: "业务状态码",
        },
    })
    dataResp.Set("message", goai.SchemaRef{
        Value: &goai.Schema{
            Type:        "string",
            Title:       "业务状态描述",
            Description: "业务状态描述",
        },
    })
    dataResp.Set("data", goai.SchemaRef{
        Value: &goai.Schema{
            Type:        "object",
            Title:       "业务数据",
            Description: "业务数据",
            Nullable:    true,
        },
    })
    oai.Components.Schemas.Set("bizmodel.HTTPResponse", goai.SchemaRef{
        Value: &goai.Schema{
            Type:       "object",
            Properties: dataResp,
        },
    })
    // 错误的响应状态码匹配
    oai.Components.Responses = goai.Responses{
        // 也许我们需要将 key 的空格去掉???
        http.StatusText(http.StatusBadRequest): goai.ResponseRef{
            Value: &goai.Response{
                Description: "BadRequest",
                Content: map[string]goai.MediaType{
                    "application/json": {Schema: &goai.SchemaRef{Ref: "bizmodel.HTTPResponse"}},
                },
            },
        },
        http.StatusText(http.StatusNotFound): goai.ResponseRef{
            Value: &goai.Response{
                Description: "NotFound",
                Content: map[string]goai.MediaType{
                    "application/json": {Schema: &goai.SchemaRef{Ref: "bizmodel.HTTPResponse"}},
                },
            },
        },
        http.StatusText(http.StatusInternalServerError): goai.ResponseRef{
            Value: &goai.Response{
                Description: "InternalServerError",
                Content: map[string]goai.MediaType{
                    "application/json": {Schema: &goai.SchemaRef{Ref: "bizmodel.HTTPResponse"}},
                },
            },
        },
    }

    s.Run()
}
  1. HelloCreateRes 中设置 successStatusCode201,那么应该生成 201 文档,也设置了 400500 且这两个 response 的文档可以在文档的 components.responses 中通过 HTTP Status text 匹配到,也应该设置到文档中
  2. HelloGetRes 中没有设置 successStatusCode 则应该默认生成 200 的文档
Issues-translate-bot commented 2 months ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


GF currently processes responses with only one result 200

See: https://github.com/gogf/gf/blob/6e5ce98d23c65fd7283c84382abfde62a29f9930/net/goai/goai_path.go#L239

The status code cannot be customized and represents different meanings in some 2xx states. Reference: 201 Created

Need to respond according to these status codes

  1. Created successfully, hope it can be set to 201
  2. Asynchronous tasks, delayed processing, hope to be 202

Currently GF does not support this, here is an idea:

Currently GF does not support this. One idea is to set the successStatusCode or errorStatusCode tag in xxxRes.Meta and set the corresponding HTTP Status Code document

server:
  address: ":8199"
  openapiPath: "/api.json"
  swaggerPath: "/swagger"
package main

import (
"context"
"net/http"

"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/net/goai"
)

type HelloCreateReq struct {
g.Meta `path:"/hello" method:"POST"`

Name string `v:"required" dc:"Your name"`
Age int `v:"required|between:1,200" dc:"Your age"`
}

type HelloCreateRes struct {
g.Meta `successStatusCode:"201" errorStatusCode:"400,500"`

ID uint64 `json:"id,string" dc:"ID"`
}

type HelloGetReq struct {
g.Meta `path:"/hello/{id}" method:"GET"`

// ID ID
ID uint64 `json:"id,string" dc:"ID" in:"path" v:"required"`
}

type HelloGetRes struct {
g.Meta `errorStatusCode:"400,500"`

ID uint64 `json:"id,string" dc:"ID"`
Name string `json:"name" dc:"Name"`
Age int `json:"age" dc:"Age"`
}

type Hello struct{}

func (c *Hello) Create(ctx context.Context, r *HelloCreateReq) (*HelloCreateRes, error) {
return &HelloCreateRes{ID: 1}, nil
}

func (c *Hello) Get(ctx context.Context, r *HelloGetReq) (*HelloGetRes, error) {
return &HelloGetRes{
ID: r.ID,
Name: "john",
Age: 18,
}, nil
}

func main() {
s := g.Server()
s.Use(ghttp.MiddlewareHandlerResponse)
s.Group("/", func(group *ghttp.RouterGroup) {
group.Bind(
new(Hello),
)
})

//Set the response data structure
oai := s.GetOpenApi()
oai.Config.CommonResponse = ghttp.DefaultHandlerResponse{}
oai.Config.CommonResponseDataField = "Data"

//wrong response
dataResp := &goai.Schemas{}
dataResp.Set("code", goai.SchemaRef{
Value: &goai.Schema{
Type: "integer",
Format: "int32",
Title: "Business Status Code",
Description: "Business status code",
},
})
dataResp.Set("message", goai.SchemaRef{
Value: &goai.Schema{
Type: "string",
Title: "Business Status Description",
Description: "Business status description",
},
})
dataResp.Set("data", goai.SchemaRef{
Value: &goai.Schema{
Type: "object",
Title: "Business Data",
Description: "Business data",
Nullable: true,
},
})
oai.Components.Schemas.Set("bizmodel.HTTPResponse", goai.SchemaRef{
Value: &goai.Schema{
Type: "object",
Properties: dataResp,
},
})
// Wrong response status code match
oai.Components.Responses = goai.Responses{
// Maybe we need to remove the spaces from key? ? ?
http.StatusText(http.StatusBadRequest): goai.ResponseRef{
Value: &goai.Response{
Description: "BadRequest",
Content: map[string]goai.MediaType{
"application/json": {Schema: &goai.SchemaRef{Ref: "bizmodel.HTTPResponse"}},
},
},
},
http.StatusText(http.StatusNotFound): goai.ResponseRef{
Value: &goai.Response{
Description: "NotFound",
Content: map[string]goai.MediaType{
"application/json": {Schema: &goai.SchemaRef{Ref: "bizmodel.HTTPResponse"}},
},
},
},
http.StatusText(http.StatusInternalServerError): goai.ResponseRef{
Value: &goai.Response{
Description: "InternalServerError",
Content: map[string]goai.MediaType{
"application/json": {Schema: &goai.SchemaRef{Ref: "bizmodel.HTTPResponse"}},
},
},
},
}

s.Run()
}
  1. Set successStatusCode to 201 in HelloCreateRes, then the 201 document should be generated, 400 and 500 are also set, and the two response documents can be found in the components.responses' of the document is matched by HTTP Status text and should also be set to the document
  2. If successStatusCode is not set in HelloGetRes, a document of 200 should be generated by default
ivothgle commented 2 months ago

@shuqingzai 厉害,你这个也能实现自定义状态码 但我更想关注的是使用 goai.SchemaRef 不能引用到 #/components/responses/ 估计是个问题

shuqingzai commented 2 months ago

@shuqingzai 厉害,你这个也能实现自定义状态码 但我更想关注的是使用 goai.SchemaRef 不能引用到 #/components/responses/ 估计是个问题

@ivothgle

可以引用啊,需要手动加到 Schemas 中(参考我的示例代码中: oai.Components.Schemas.Set("bizmodel.HTTPResponse", goai.SchemaRef{....

你说的无法引用,是因为 GF 没法手动设置某个接口的多个状态码响应,只有一个 200 响应,需要支持配置多个响应状态码

实际上 Components.Responses 中已经可以引用了

image

ivothgle commented 2 months ago

@shuqingzai 我再换一个描述 我知道这样

'404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error' # 这里引用的是 #/components/schemas/xxxx 

但我想实现的是

'404':
          $ref: '#/components/responses/NotFound'  # 请问怎么生成这个引用链接呢 #/components/responses/xxx
          # 其实就比上面少了几行,更高的复用而已
shuqingzai commented 2 months ago

@shuqingzai 我再换一个描述 我知道这样

'404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error' # 这里引用的是 #/components/schemas/xxxx 

但我想实现的是

'404':
          $ref: '#/components/responses/NotFound'  # 请问怎么生成这个引用链接呢 #/components/responses/xxx
          # 其实就比上面少了几行,更高的复用而已

@ivothgle

懂了~~,这确实是一个 BUG ,schema 的引用前缀被写死了,应该需要判断,如果是根节点开头,不需要拼接前缀

但是问题的核心是 GF 没有对某个接口进行多个 response 的配置,所以还是我一开始说的,需要在 xxxRes 中定义 errorStatusCode 然后在 OpenAPI 文档解析生成时,自动读取自定义的 statusCode 匹配引用公共的 response schema 或者你有更好的实现方案也可以自行实现

因为即使你可以配置进去,也没法使用多个响应,一样没法用

我自己改了一点,可以适配,可以参考下

  1. 支持多个错误响应码文档
  1. 复用 response -- 这是你需要的 image

image

Issues-translate-bot commented 2 months ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


@shuqingzai Let me change the description. I know this

'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error' # The reference here is #/components/schemas/xxxx

But what I want to achieve is

'404':
$ref: '#/components/responses/NotFound' # How to generate this reference link #/components/responses/xxx
# Actually, it’s just a few lines less than the above, just for higher reuse.

@ivothgle

Got it~~, this is indeed a BUG. The reference prefix of the schema is hard-coded. It should be judged. If it starts with the root node, there is no need to splice the prefix.

But the core of the problem is that GF does not configure multiple responses for a certain interface, so as I said at the beginning, you need to define errorStatusCode in xxxRes and then automatically read the customized one when the OpenAPI document is parsed and generated. statusCode matches the public response schema or you can implement it yourself if you have a better implementation solution.

Because even if you can configure it, you can't use multiple responses, it still won't work.

I changed it a bit myself and it can be adapted. You can refer to it below.

  1. Support multiple error response code documents
  1. Reuse response -- this is what you need image

image

ivothgle commented 2 months ago

这里的核心只有schema 的引用前缀被写死,404,500 本来就是通用状态码 通过重写自定义 /api.json 即可完成高度自定义就行了

Issues-translate-bot commented 2 months ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


The core here is that only the reference prefix of the schema is hard-coded. 404 and 500 are originally universal status codes. By rewriting the custom /api.json, a high degree of customization can be achieved.

shuqingzai commented 2 months ago

这里的核心只有schema 的引用前缀被写死,404,500 本来就是通用状态码 通过重写自定义 /api.json 即可完成高度自定义就行了

@ivothgle 你的意思你拿到 /api.json 还要手动改这个文件?? 如果你要手动改那不是因为没法自动设置吗?下次再生成文档,继续手动改? 因为即使你定义了这些 response schema ,你也要在每个 API 路由下引用它,才会有效果,只是定义不引用不会有文档的

Issues-translate-bot commented 2 months ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


The core here is that only the reference prefix of the schema is hard-coded. 404 and 500 are originally universal status codes. By rewriting the custom /api.json, a high degree of customization can be achieved. @ivothgle Do you mean you have to manually modify this file after getting /api.json? ? If you want to change it manually, isn't it because it can't be set automatically? Generate the document next time and continue to modify it manually? Because even if you define these response schema, you have to quote it under each API route to have an effect. If you define it without quoting it, there will be no documentation.

ivothgle commented 2 months ago

千言万语不如代码一贴


func enhanceOpenAPIDoc(s *ghttp.Server) {
    s.BindHandler("/api.json", openapiSpec)
    s.BindHandler("/swagger-ui/", func(r *ghttp.Request) {
        r.Response.Write(MySwaggerUITemplate)
        r.ExitAll()
    })

    openapi := s.GetOpenApi()
    openapi.Config.CommonResponse = model.DefaultHandlerResponse{}
    openapi.Config.CommonResponseDataField = `Data`
    openapi.Config.IgnorePkgPath = true

    openapi.Security = &goai.SecurityRequirements{{"bearerAuth": []string{}}}
    openapi.Components.SecuritySchemes = goai.SecuritySchemes{
        "bearerAuth": goai.SecuritySchemeRef{
            Value: &goai.SecurityScheme{
                Type:         "http",
                Scheme:       "bearer",
                BearerFormat: "JWT",
            },
        },
    }
}

func genOpenapi(s *ghttp.Server) {
    var (
        ctx     = context.TODO()
        err     error
        methods []string
    )
    for _, item := range s.GetRoutes() {
        switch item.Type {
        case ghttp.HandlerTypeMiddleware, ghttp.HandlerTypeHook:
            continue
        }
        if item.Handler.Info.IsStrictRoute {
            methods = []string{item.Method}
            if gstr.Equal(item.Method, "ALL") {
                methods = ghttp.SupportedMethods()
            }
            for _, method := range methods {
                err = s.GetOpenApi().Add(goai.AddInput{
                    Path:   item.Route,
                    Method: method,
                    Object: item.Handler.Info.Value.Interface(),
                })
                if err != nil {
                    s.Logger().Fatalf(ctx, `%+v`, err)
                }
            }
        }
    }
}

func openapiSpec(req *ghttp.Request) {
    genOpenapi(req.Server)

    // 过滤免登录的接口
    si, _ := g.Cfg("secure").Get(req.GetCtx(), "secure.ignore")
    unauthenticated := make(map[string]struct{})

    for _, url := range si.Strings() {
        unauthenticated[url] = struct{}{}
    }
    for url, path := range req.Server.GetOpenApi().Paths {
        tag := "xxxx"
        if path.Get != nil {
            path.Get.Tags[0] = path.Get.Tags[0] + " - " + tag
            if _, ok := unauthenticated[url]; ok {
                path.Get.Security = &goai.SecurityRequirements{}
            }
        }
        if path.Post != nil {
            path.Post.Tags[0] = path.Post.Tags[0] + " - " + tag
            if _, ok := unauthenticated[url]; ok {
                path.Post.Security = &goai.SecurityRequirements{}
            }
        }
        if path.Put != nil {
            path.Put.Tags[0] = path.Put.Tags[0] + " - " + tag
            if _, ok := unauthenticated[url]; ok {
                path.Put.Security = &goai.SecurityRequirements{}
            }
        }
        if path.Delete != nil {
            path.Delete.Tags[0] = path.Delete.Tags[0] + " - " + tag
            if _, ok := unauthenticated[url]; ok {
                path.Delete.Security = &goai.SecurityRequirements{}
            }
        }
    }

    req.Response.Write(req.Server.GetOpenApi())
}

我都已经在操作 goai.Operation 了,就没必要往 gf 里面增加负担了,把前缀放开就可以了

shuqingzai commented 2 months ago

千言万语不如代码一贴

func enhanceOpenAPIDoc(s *ghttp.Server) {
  s.BindHandler("/api.json", openapiSpec)
  s.BindHandler("/swagger-ui/", func(r *ghttp.Request) {
      r.Response.Write(MySwaggerUITemplate)
      r.ExitAll()
  })

  openapi := s.GetOpenApi()
  openapi.Config.CommonResponse = model.DefaultHandlerResponse{}
  openapi.Config.CommonResponseDataField = `Data`
  openapi.Config.IgnorePkgPath = true

  openapi.Security = &goai.SecurityRequirements{{"bearerAuth": []string{}}}
  openapi.Components.SecuritySchemes = goai.SecuritySchemes{
      "bearerAuth": goai.SecuritySchemeRef{
          Value: &goai.SecurityScheme{
              Type:         "http",
              Scheme:       "bearer",
              BearerFormat: "JWT",
          },
      },
  }
}

func genOpenapi(s *ghttp.Server) {
  var (
      ctx     = context.TODO()
      err     error
      methods []string
  )
  for _, item := range s.GetRoutes() {
      switch item.Type {
      case ghttp.HandlerTypeMiddleware, ghttp.HandlerTypeHook:
          continue
      }
      if item.Handler.Info.IsStrictRoute {
          methods = []string{item.Method}
          if gstr.Equal(item.Method, "ALL") {
              methods = ghttp.SupportedMethods()
          }
          for _, method := range methods {
              err = s.GetOpenApi().Add(goai.AddInput{
                  Path:   item.Route,
                  Method: method,
                  Object: item.Handler.Info.Value.Interface(),
              })
              if err != nil {
                  s.Logger().Fatalf(ctx, `%+v`, err)
              }
          }
      }
  }
}

func openapiSpec(req *ghttp.Request) {
  genOpenapi(req.Server)

  // 过滤免登录的接口
  si, _ := g.Cfg("secure").Get(req.GetCtx(), "secure.ignore")
  unauthenticated := make(map[string]struct{})

  for _, url := range si.Strings() {
      unauthenticated[url] = struct{}{}
  }
  for url, path := range req.Server.GetOpenApi().Paths {
      tag := "xxxx"
      if path.Get != nil {
          path.Get.Tags[0] = path.Get.Tags[0] + " - " + tag
          if _, ok := unauthenticated[url]; ok {
              path.Get.Security = &goai.SecurityRequirements{}
          }
      }
      if path.Post != nil {
          path.Post.Tags[0] = path.Post.Tags[0] + " - " + tag
          if _, ok := unauthenticated[url]; ok {
              path.Post.Security = &goai.SecurityRequirements{}
          }
      }
      if path.Put != nil {
          path.Put.Tags[0] = path.Put.Tags[0] + " - " + tag
          if _, ok := unauthenticated[url]; ok {
              path.Put.Security = &goai.SecurityRequirements{}
          }
      }
      if path.Delete != nil {
          path.Delete.Tags[0] = path.Delete.Tags[0] + " - " + tag
          if _, ok := unauthenticated[url]; ok {
              path.Delete.Security = &goai.SecurityRequirements{}
          }
      }
  }

  req.Response.Write(req.Server.GetOpenApi())
}

我都已经在操作 goai.Operation 了,就没必要往 gf 里面增加负担了,把前缀放开就可以了

@ivothgle

你贴的代码都是 GF 已经实现的逻辑,我不明白为啥你还要重写一次 唯一的解释可能是:你需要为他的页面添加简单的 Basic Auth 认证,这其实也是 GF 不完善的点,没法为 OpenAPI 文档添加简单认证或添加中间件,应该是 GF 自身完善,而不是使用者自己实现,这是大部分通用的,而不是定制化需求

  1. 配置 OpenAPI 的路径或 UI 等都可以在配置文件或代码直接赋值配置即可
  2. genOpenapi 参考: https://github.com/gogf/gf/blob/6e5ce98d23c65fd7283c84382abfde62a29f9930/net/ghttp/ghttp_server_openapi.go#L17
  3. 对于 Security 的使用,你只需要定义出来,然后在 xxxReq 加上对应标签即可,也不需要像你这样循环为每个路由添加,因为 GF 内部是直接复用标签的,参考: https://github.com/gogf/gf/blob/6e5ce98d23c65fd7283c84382abfde62a29f9930/net/goai/goai_path.go#L164C56-L164C69 类似下面这样: image

image

  1. 对于你的结论:为 GF 添加负担?这些都是它自带的功能,怎么就添加负担了???而且像你这样每个都是自己重写,那才是添加自己的负担吧
  2. 我提出的点是:GF 不支持为某个 Path 添加更多的响应状态码 ,按你的想法就是重新遍历一次所有的 Paths 然后自己手动添加,不是不行,而是我认为这应该是 GF 内置支持的功能,因为它已经支持 OpenAPI ,只是支持不够完整,需要更加完善而已,观点不同,哈哈 😄
ivothgle commented 2 months ago

你说的都对!

Issues-translate-bot commented 2 months ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


Everything you said is right!

gqcn commented 1 month ago

@ivothgle @shuqingzai 大家好,关于OpenAPIv3这块的支持,原本设计的想法是框架层面提供通用的、常用的、易用的实现,大概80%的能力即可。一些扩展的能力可以通过获取到OpenAPIv3对象后(通过GeOpentAPI方法)由开发者自己去扩展,也能很容易扩展OpenAPIv3的实现。如果在扩展这里存在问题,比如扩展能力暴露的接口不足,也欢迎参与社区建设共同来完善❤️。

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


@ivothgle @shuqingzai Hello everyone, regarding the support of OpenAPIv3, the original design idea is to provide a universal, commonly used and easy-to-use implementation at the framework level, which can achieve about 80% of the capabilities. Some extended capabilities can be extended by developers themselves by obtaining the OpenAPIv3 object (through the GeOpentAPI method), and the implementation of OpenAPIv3 can also be easily extended. If there are problems in the expansion, such as insufficient interfaces exposed by the expansion capabilities, you are welcome to participate in community building to improve it ❤️.

github-actions[bot] commented 1 month ago

Hello @ivothgle. We like your proposal/feedback and would appreciate a contribution via a Pull Request by you or another community member. We thank you in advance for your contribution and are looking forward to reviewing it! 你好 @ivothgle。我们喜欢您的提案/反馈,并希望您或其他社区成员通过拉取请求做出贡献。我们提前感谢您的贡献,并期待对其进行审查。

ivothgle commented 1 month ago

@gqcn 获取到了 GeOpentAPI 对象,却因为代码写死了无法扩展,唉

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


@gqcn obtained the GeOpentAPI object, but it cannot be expanded because the code is written to death, alas.

gqcn commented 1 month ago

@gqcn 获取到了 GeOpentAPI 对象,却因为代码写死了无法扩展,唉

你好,是否有个别属性没有公开,请具体描述一下呢?

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


@gqcn obtained the GeOpentAPI object, but it cannot be expanded because the code is hard-coded, alas.

Hello, are there any individual attributes that have not been made public? Please describe them in detail?

UncleChair commented 1 month ago

@gqcn 强哥好,我个人觉得目前goai模块的主要问题可能还是对于非200状态码的处理不是很理想。诚然在全200状态下文档的支持度已经很好了,但对于非200派的而言实际上还是需要其他状态码管理的,我们非200派维护一些东西会比较拧巴哈哈哈哈。目前添加状态码或者是状态码的具体example都需要进行很长一串结构体的构造,例如下面这个比较丑陋的实现:

func AddResponseStatus(openapi *goai.OpenApiV3, path string, method string, contentType string, object interface{}, status string, description string) {
    // Add schema
    openapi.Add(goai.AddInput{
        Object: object,
    })
    location := strings.ReplaceAll(reflect.TypeOf(object).PkgPath(), "/", ".")
    name := reflect.TypeOf(object).Name()
    target := findMethod(openapi.Paths[path], method)
    // Prevent duplicate
    if _, exist := target.Responses[status]; exist {
        return
    }
    target.Responses[status] = goai.ResponseRef{
        Value: &goai.Response{
            Content: goai.Content{
                contentType: goai.MediaType{
                    Schema: &goai.SchemaRef{
                        Ref: location + "." + name,
                    },
                },
            },
            Description: description,
        },
    }
}

然后再添加examples的schema,最后的api.json会类似这样:

"/api/v1/auth/login": {
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/project.api.auth.v1.LoginReq"
              }
            }
          }
        },
        "responses": {
          "200": { some default data },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/project.api.CommonRes"
                },
                "examples": {
                  "Code 401: login failed": {
                    "value": {
                      "code": 401,
                      "message": "incorrect Username or Password",
                      "data": null
                    }
                  }
                }
              }
            },
            "description": ""
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/project.api.CommonRes"
                },
                "examples": {
                  "Code 1: User not exist": {
                    "value": {
                      "code": 1,
                      "message": "User not exist",
                      "data": null
                    }
                  },
                  "Code 2: Wrong password": {
                    "value": {
                      "code": 2,
                      "message": "Wrong password",
                      "data": null
                    }
                  },
                  "Code 3: Not allowed": {
                    "value": {
                      "code": 3,
                      "message": "Not allowed",
                      "data": null
                    }
                  }
                }
              }
            },
            "description": ""
          }
        },
        "summary": "User Login",
        "tags": [
          "Auth"
        ]
      }
    },

为了添加非200的状态码往往都需要进行类似这样的操作,并且由于他们和规范路由并不存在强关联,很多时候很容易在文档中漏掉某些状态码,进而导致文档实际上并不完全可用。同时虽然已经封装了添加的函数,但每次添加状态都会调用一次,导致最后实际上还是会在api包里写一大坨东西进去。这块要说是项目本身的问题确实也有一些,但既然GoFrame已经有了相对比较完善的goai文档管理机制,我觉得实际上可以暴露一些类似的方法给到开发者,或者是直接在规范路由的g.Meta里解决这个问题。

如果暴露方法的话我个人认为对于非200派而言最重要的应该有两个,一个是上面提到的添加状态码schema,另一个是添加examples(对应特定状态码下的error code)。这样的话goai本身不需要太多改动,只需要让开发者自行决定schema是什么样,然后再注入到文档里就可以了。

另一种方案可能是在现有的tag基础上添加一个defaultStatus和一个errorStatus,这样可以更改默认响应状态为2xx,或者添加对应的错误状态码列表。对于examples的管理我没有什么好的想法,目前我会用添加一个这样的status list:

var LoginRes403 = map[int]gcode.Code{
    1: gcode.New(1, "User not exist", nil),
    2: gcode.New(2, "Wrong password", nil),
    3: gcode.New(3, "Not allowed", nil),
}

然后通过解析这个变量来生成对应的examples,这样不仅方便,也更容易管理对应api的错误状态。不过这个方法和commen response的耦合比较大,可能对于框架来说不是一个好的解决方案。

GoFrame的规范路由真的是一个很棒的解决方案,我个人会比较重视这块,因为前端调用和后端排障很多时候都需要一份可靠的文档,而在规范路由的情况下大部分时间开发者也都不需要花费过多的精力在api文档的维护上。强哥可以费心看看这块还有没有优化的空间,我go的经验比较少所以上面提到的一些方案可能都比较僵硬。

gqcn commented 1 month ago

https://github.com/gogf/gf/issues/3747#issuecomment-2406906280

@UncleChair 框架原本在设计自动API接口文档生成的时候,就没有考虑不同返回状态码的接口文档生成,因为我们的初衷是代码文档一致性维护、自动化生成、只满足大部分的场景。

不同返回状态码数据结构定义

如果需要实现不同状态码的代码文档一致性维护,我粗略想了一下,其实可以参考g.Meta的模式,设计专门的文档化数据结构来实现,代码来举个例子,例如:

type XxxRes struct {
    g.Meta `path:"/xxx"` // 这里是接口返回的主要描述,针对2xx的状态码
    goai.Status403 *Status403Struct // 这个是状态码403时的数据结构描述
    goai.Status500 *Status500Struct // 这个是状态码500时的数据结构描述
    // 以此类推
}

其中goai.Status*goai包提供的常量预定义。

有更好的建议欢迎一起讨论。

复杂example的维护

那么文档的example怎么来实现呢?example通常是大块代码,那么可以结合资源管理的能力来实现!资源管理是将静态资源打包到二进制文件中一起发布的能力( https://goframe.org/pages/viewpage.action?pageId=1114671 ),并且在goframe框架下,该能力是整合了工具链自动化实现的( https://goframe.org/pages/viewpage.action?pageId=1115788 ),开发者几乎不用关心细节。我来举个例子:

type XxxReq struct {
    g.Meta `path:"/xxx" example:"resource/example/xxx/200.json"` // 通过example标签指定资源管理器中的具体example代码文件
    goai.Status403 Status403Struct `example:"resource/example/xxx/403.json"` 
    goai.Status500 Status500Struct `example:"resource/example/xxx/500.json"`
    // 以此类推
}

并且这个能力在开发时是自动读取的本地文件,在编译发布时是从资源管理器读取的(编译发布即可无需做额外操作),和资源管理器设计的能力一致。

同时,这些能力需要在goai中进行增强。感兴趣的小伙伴可以一起参与贡献哈。

@ivothgle @shuqingzai

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


https://github.com/gogf/gf/issues/3747#issuecomment-2406906280

@UncleChair When the framework originally designed the automatic API interface document generation, it did not consider the generation of interface documents with different return status codes, because our original intention was to maintain the consistency of code documents and automatically generate them, which only satisfies most scenarios.

Different return status code data structure definitions

If you need to maintain the consistency of code documents for different status codes, I gave it a rough thought. In fact, you can refer to the g.Meta mode and design a specialized documented data structure to achieve this. Here is an example of the code, for example:

type XxxReq struct {
    g.Meta `path:"/xxx"` // Here is the main description of the interface, for the 2xx status code
    goai.Status403 Status403Struct // This is the data structure description when status code 403
    goai.Status500 Status500Struct // This is the data structure description when the status code is 500
    // and so on
}

And this capability is automatically read from local files during development, and is read from the resource manager during compilation and release (no additional operations are required for compilation and release), which is consistent with the capability designed by the resource manager.

Maintenance of complex examples

So how to implement the example of the document? Example is usually a large block of code, so it can be achieved by combining resource management capabilities! Resource management is the ability to package static resources into binary files and publish them together (https://goframe.org/pages/viewpage.action?pageId=1114671), and under the goframe framework, this ability is integrated with the tool chain It is implemented automatically (https://goframe.org/pages/viewpage.action?pageId=1115788), and developers hardly need to care about the details. Let me give you an example:

type XxxReq struct {
    g.Meta `path:"/xxx" example:"resource/example/xxx/200.json"` // Specify the specific example code file in the resource manager through the example tag
    goai.Status403 Status403Struct `example:"resource/example/xxx/403.json"`
    goai.Status500 Status500Struct `example:"resource/example/xxx/500.json"`
    // and so on
}

At the same time, these capabilities need to be enhanced in goai. Interested friends can participate and contribute together.

UncleChair commented 1 month ago

@gqcn 感谢强哥的思路,结合源码的一些内容我这边也想了一个关于不同返回状态码数据结构定义的方案:

举个例子就是:

type XXXReq struct {
    g.Meta      `status:"201"  example:"xxx.json"`
    Status      string    `json:"status" des:"server status" eg:"OK!"`
    Status404Error *Error404 `status:"404" example:"xxx.json"`
}

这样有一个好处就是goai不需要维护一些保留字段来保证可以解析到对应的结构体,也不会和用户现有的字段产生冲突(因为只将包含status的字段解析为某个状态下的schema),设置状态码也比较方便。同时status字段也可以用于默认返回状态码的设置,解析req就可以拿到(这块尝试写了个简单的实现),也能保证满足用户所有2xx的需求

不知道这样从框架的角度是否是一个好的实现

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


@gqcn Thanks to Brother Qiang for his ideas. Combining some contents of the source code, I also thought of a solution for defining different return status code data structures:

  • Add a status tag to gtag
  • Directly declare fields containing the status tag when defining the return structure
  • Ignore fields containing status tag when goai structToSchema
  • Parse and add all fields containing status tag as structure definitions under specific status codes

An example is:

type XXXReq struct {
g.Meta `status:"201" example:"xxx.json"`
Status string `json:"status" des:"server status" eg:"OK!"`
Status404Error *Error404 `status:"404" example:"xxx.json"`
}

One advantage of this is that goai does not need to maintain some reserved fields to ensure that the corresponding structure can be parsed, and it will not conflict with the user's existing fields (because only fields containing status are parsed into schema in a certain state) , it is also more convenient to set the status code. At the same time, the status field can also be used to set the default return status code, which can be obtained by parsing req (I tried to write a simple [implementation](https://github.com/gogf/gf/compare/master.. .UncleChair:gf:goai/http_status_enhance)), which can also ensure that all 2xx needs of users are met

I don’t know if this is a good implementation from a framework perspective.

gqcn commented 1 month ago

https://github.com/gogf/gf/issues/3747#issuecomment-2408832814

使用status tag是个不错的主意(另外这个应该写在XxxRes返回数据结构中,不是XxxReq请求结构中,我示例写得有问题),至于要不要在返回结构体中通过定义属性来表示不同返回的数据结构,我不确实这是否是一个好的设计,这一点最好大家一起讨论下。

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


https://github.com/gogf/gf/issues/3747#issuecomment-2408832814

It is a good idea to use status tag (in addition, this should be written in the XxxRes return data structure, not the XxxReq request structure. There is something wrong with my example). As for whether to define attributes in the return structure To represent different returned data structures, I am not sure whether this is a good design. It is best for everyone to discuss this together.

UncleChair commented 1 month ago

#3747 (comment)

定义太多不同的返回数据结构可能确实会造成一些使用上的混乱,其实最好还是在有common response的情况下自动复用,我也是更倾向于直接复用。不过考虑到可能用户的response handler会有定制的错误结构体需求,可能实现一个类似override的流程会更好一些?

目前的构想是当包含 status 标签的字段被解析时,如果该字段本身是空的就直接复用common response;如果包含额外的字段就直接优先将该字段解析并覆盖返回结构。或者和原来的逻辑一样,在包含mime标签时直接忽略common response。这样既能满足一些定制化的需求,也能保证在一般情况下的使用便捷。

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


#3747 (comment)

Defining too many different return data structures may indeed cause some confusion in use. In fact, it is best to automatically reuse when there is a common response. I also prefer direct reuse. However, considering that the user's response handler may have customized error structure requirements, it may be better to implement a process similar to override?

The current idea is that when a field containing the status tag is parsed, if the field itself is empty, the common response will be reused directly; if it contains additional fields, the field will be parsed first and the return structure will be overwritten. This can not only meet some customized needs, but also ensure the convenience of use under normal circumstances. However, there may not be any way to return a pure status code without any data structure, hahaha.

gqcn commented 1 month ago

https://github.com/gogf/gf/issues/3747#issuecomment-2412642714

@UncleChair 我主要担忧的是,定义这些状态码返回数据结构对业务数据结构的侵入性,开发者在编写和使用业务数据结构的时候还需要关心这些非业务相关的数据结构,体验会很不好。但如果是在common response中定义这些不同状态码的数据结构就没问题了,因为common response对业务数据结构是无感知的。

我不确定有没有这种脱离common response公共输出标准数据结构对应的业务场景。如果没有的话事情就更简单了,咱们可以根据不同的状态码定义不同的common response

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


https://github.com/gogf/gf/issues/3747#issuecomment-2412642714

@UncleChair My main concern is that defining these status code return data structures is intrusive to business data structures. Developers also need to care about these non-business-related data structures when writing and using business data structures, and the experience will be very bad. . But if you define the data structures of these different status codes in common response, there will be no problem, because common response is unaware of the business data structure.

I'm not sure if there is such a business scenario that breaks away from the common response public output standard data structure. If not, things would be simpler. Can we define different common response based on different status codes?

UncleChair commented 1 month ago

#3747 (comment)

@gqcn 在req里定义状态码返回数据结构确实是会对业务理解造成一定的影响,不过我感觉应该也不完全是坏事。目前来说的话我会把req作为返回数据的一个 范例,也就是这里面包含了所有可能的数据结构和信息,但我不一定需要去填充他,其实gmeta也比较好的体现了这一点。而对于response结构这基本上就是一个单纯的范例,开发者可以很明确的看到哪些response和哪些api是有关联的,同时再展开一点的话甚至也可以通过tag拿到examples来直接进行错误状态管理(比较类似laravel里的exceptions,某个状态下的某些信息),这样其实对api的整体管理是有好处的。

对于开发者编写和使用业务数据结构这块我能想到的缺点可能就是字段会在scan的时候和某些dao的字段重复,不过我相信这应该是命名规范或者说是设计问题,心智负担确实是有一些,但在项目的一些设计下其实是完全可控的,就比如规定使用statusxxxRes这种字段来命名之类的。更重要的是其实通过这样的方式,返回状态也被api纳入了管理之下,不管是通过tag解析json example还是其它一些方法,在这种情况下一个请求的所有状态和它所在的api是可以有强关联的,这其实无形中减少了开发者的管理负担。

common response 里定义错误结构也是一个可以的解决方案,不过还是有用户不使用 common response 的问题,扩展能力感觉可能也不太够。因为基于 common response 去做结构定义的话,首先抛开可能比较edge的脱离标准输出的情况,在req里去引用对应的response也还是会比较困难,如果不想加字段的话可能就会有非常大一坨meta数据丢在那,包括examples的管理,这样感觉也还是会写成类似的方案,只不过是通过tag来标注所有麻烦的信息。

不过话又说回来req的结构比较干净的话看起来确实比较舒服,用起来表面上的心智负担也会更低,不过确实需要一个更有效地可以将错误状态和api关联起来的方案。或者干脆给req加个返回状态和example的方法怎么样?例如这样:

type XXXReq {}
func (r *XXXReq ) Status() map[string]interface{}{
    return (状态码和数据结构)
}

解析的时候就可以直接添加了,错误管理也可以用相同的方法拿到数据,同时也保证了业务数据结构的清晰。

Issues-translate-bot commented 1 month ago

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


#3747 (comment)

@gqcn Defining the status code return data structure in req will indeed have a certain impact on business understanding, but I feel it is not entirely a bad thing. For now, I will use req as an example of the returned data, which means that it contains all possible data structures and information, but I don't necessarily need to fill it. In fact, gmeta also reflects it better. at this point. As for the response structure, this is basically a simple example. Developers can clearly see which responses are related to which APIs. At the same time, if you expand it a little more, you can even get examples through tags to directly manage the error status. (Compared to exceptions in Laravel, certain information in a certain state), this is actually good for the overall management of the API.

The disadvantage I can think of for developers writing and using business data structures may be that the fields will be repeated with the fields of some DAOs during scanning. However, I believe this should be a naming convention or a design issue. The mental burden is indeed There are some, but they are actually completely controllable under some designs of the project, such as stipulating the use of fields such as statusxxxRes for naming. More importantly, in this way, the return status is also managed by the API. Whether it is parsing json example through tags or some other methods, in this case all the status of a request and the API where it is located can be If there is a strong correlation, this actually reduces the management burden on developers.

Defining error structures in common response is also a possible solution, but there are still problems with users not using common response, and the scalability may not be enough. Because if you define the structure based on common response, first of all, aside from the possible edge of being separated from the standard output, it will still be difficult to reference the corresponding response in the req. If you don't want to add fields, it may be very large. A bunch of meta data is thrown there, including the management of examples. It feels like a similar solution will still be written, but all the troublesome information will be marked through tags.

But then again, if the structure of req is relatively clean, it does look more comfortable, and the mental load on the surface will be lower when using it. However, a more effective solution is needed to associate error status with the API. Or how about simply adding a method to req that returns status and example? For example:

type XXXReq {}
func (r *XXXReq ) Status() map[string]interface{}{
    return (status code and data structure)
}

It can be added directly during parsing, and error management can also use the same method to obtain data, while also ensuring a clear business data structure.

gqcn commented 1 month ago

https://github.com/gogf/gf/issues/3747#issuecomment-2414441181

@UncleChair 通过接口实现的方式来定义不同状态码的返回数据结构是个很棒的想法👍🏻!这样既能解决不同状态码扩展的问题,也能避免对业务数据结构的侵入。这里有几个点需要明确一下:

@shuqingzai @ivothgle @hailaz @wln32 @oldme-git 大家一起看看有没更好的建议没有呢?