gf icon indicating copy to clipboard operation
gf copied to clipboard

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

Open ivothgle opened this issue 1 year ago • 36 comments

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/

ivothgle avatar Aug 28 '24 06:08 ivothgle

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 的文档

shuqingzai avatar Aug 30 '24 02:08 shuqingzai

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

Issues-translate-bot avatar Aug 30 '24 02:08 Issues-translate-bot

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

ivothgle avatar Aug 30 '24 09:08 ivothgle

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

@ivothgle

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

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

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

image

shuqingzai avatar Aug 30 '24 11:08 shuqingzai

@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 avatar Sep 02 '24 02:09 ivothgle

@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. 支持多个错误响应码文档
  • https://github.com/shuqingzai/gf/commit/d8227affc7939ae051733a6ff97e9203c0e5595d
  • https://github.com/shuqingzai/gf/commit/c3d86a6c26e7350f9bb5dc3162f89db6376d0c05
  1. 复用 response -- 这是你需要的 image

image

shuqingzai avatar Sep 02 '24 07:09 shuqingzai

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
  • https://github.com/shuqingzai/gf/commit/d8227affc7939ae051733a6ff97e9203c0e5595d
  • https://github.com/shuqingzai/gf/commit/c3d86a6c26e7350f9bb5dc3162f89db6376d0c05
  1. Reuse response -- this is what you need image

image

Issues-translate-bot avatar Sep 02 '24 07:09 Issues-translate-bot

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

ivothgle avatar Sep 03 '24 01:09 ivothgle

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.

Issues-translate-bot avatar Sep 03 '24 01:09 Issues-translate-bot

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

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

shuqingzai avatar Sep 03 '24 01:09 shuqingzai

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.

Issues-translate-bot avatar Sep 03 '24 01:09 Issues-translate-bot

千言万语不如代码一贴


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 avatar Sep 04 '24 00:09 ivothgle

千言万语不如代码一贴

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

shuqingzai avatar Sep 04 '24 02:09 shuqingzai

你说的都对!

ivothgle avatar Sep 04 '24 03:09 ivothgle

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


Everything you said is right!

Issues-translate-bot avatar Sep 04 '24 03:09 Issues-translate-bot

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

gqcn avatar Sep 25 '24 09:09 gqcn

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 ❤️.

Issues-translate-bot avatar Sep 25 '24 09:09 Issues-translate-bot

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。我们喜欢您的提案/反馈,并希望您或其他社区成员通过拉取请求做出贡献。我们提前感谢您的贡献,并期待对其进行审查。

github-actions[bot] avatar Sep 25 '24 09:09 github-actions[bot]

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

ivothgle avatar Sep 28 '24 07:09 ivothgle

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.

Issues-translate-bot avatar Sep 28 '24 07:09 Issues-translate-bot

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

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

gqcn avatar Oct 05 '24 06:10 gqcn

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?

Issues-translate-bot avatar Oct 05 '24 06:10 Issues-translate-bot

@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的经验比较少所以上面提到的一些方案可能都比较僵硬。

UncleChair avatar Oct 11 '24 08:10 UncleChair

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

gqcn avatar Oct 12 '24 05:10 gqcn

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.

Issues-translate-bot avatar Oct 12 '24 05:10 Issues-translate-bot

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

  • 添加一个status tag 到gtag里
  • 在定义返回结构体时直接声明包含status tag的字段
  • 在goai structToSchema时忽略包含status tag的字段
  • 将包含status tag的所有字段解析并添加为特定状态码下的结构定义

举个例子就是:

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的需求

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

UncleChair avatar Oct 13 '24 05:10 UncleChair

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.

Issues-translate-bot avatar Oct 13 '24 05:10 Issues-translate-bot

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

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

gqcn avatar Oct 14 '24 13:10 gqcn

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.

Issues-translate-bot avatar Oct 14 '24 13:10 Issues-translate-bot

#3747 (comment)

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

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

UncleChair avatar Oct 15 '24 01:10 UncleChair