接口文档-扩展Response

接口文档-扩展Response

一、概述

RESTful API设计中,不同的状态码表示不同的响应情况,例如 200 表示成功,201 表示创建成功,400 表示请求错误,401 表示未授权等。在GoFrame的OpenAPIv3接口文档中,默认情况下只会生成成功状态(通常是 200)的响应文档。

然而,在实际应用中,我们常常需要为一个API提供多种可能的响应状态,例如错误处理、权限验证失败等情况。为了满足这一需求,GoFrame在 goai 组件中提供了 IEnhanceResponseStatus 接口,开发者可以通过实现该接口来扩展响应结构体的信息,添加多种状态码的响应文档。

二、接口定义

IEnhanceResponseStatus 接口的相关定义如下:

type EnhancedStatusCode = int

type EnhancedStatusType struct {
    Response any
    Examples any
}

type IEnhanceResponseStatus interface {
    EnhanceResponseStatus() map[EnhancedStatusCode]EnhancedStatusType
}

三、接口字段说明

  • EnhancedStatusCode:表示 HTTP 状态码,例如 200、201、400、401、403、404、500 等。

  • EnhancedStatusType:包含两个字段:

    • Response:响应结构体,可以是任意类型(any)。你可以为它添加 g.Meta 标签来添加文档信息,如果设置了 mime 标签,该结构体会覆盖通用响应结构体的内容。
    • Examples:响应示例,可以是任意类型(any)。你可以使用错误码列表自动生成示例内容并显示在文档中,这样可以保证文档内容与实际业务内容的同步。
    • IEnhanceResponseStatus:接口定义,包含一个方法 EnhanceResponseStatus(),该方法返回一个映射,将 HTTP 状态码映射到相应的 EnhancedStatusType 类型。

四、使用方法

要使用 IEnhanceResponseStatus 接口,需要在响应结构体中实现 EnhanceResponseStatus() 方法。该方法返回一个映射,将 HTTP 状态码映射到相应的响应结构体和示例。

1. 实现接口

在响应结构体中实现 EnhanceResponseStatus() 方法,返回不同状态码对应的响应结构体和示例。

2. 设置响应状态码

可以在响应结构体的 g.Meta 标签中使用 status 属性来设置默认的响应状态码,例如 g.Meta status:“201”`。

3. 使用示例数据

可以在 EnhanceResponseStatus() 方法中返回示例数据,这些数据将会显示在生成的 OpenAPIv3 文档中。

五、完整示例

下面是一个完整的示例,演示如何使用 IEnhanceResponseStatus 接口来扩展响应结构体的信息:

package main

import (
    "context"

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

type StoreMessageReq struct {
    g.Meta  `path:"/messages" method:"post" summary:"Store a message"`
    Content string `json:"content"`
}
type StoreMessageRes struct {
    g.Meta `status:"201"`
    Id     string `json:"id"`
}
type EmptyRes struct {
    g.Meta `mime:"application/json"`
}

type CommonRes struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}

var StoreMessageErr = map[int]gcode.Code{
    500: gcode.New(1, "Server Dead", nil),
}

func (r StoreMessageRes) EnhanceResponseStatus() (resList map[int]goai.EnhancedStatusType) {
    examples := []interface{}{}
    example500 := CommonRes{
        Code:    StoreMessageErr[500].Code(),
        Message: StoreMessageErr[500].Message(),
        Data:    nil,
    }
    examples = append(examples, example500)
    return map[int]goai.EnhancedStatusType{
        403: {
            Response: EmptyRes{},
        },
        500: {
            Response: struct{}{},
            Examples: examples,
        },
    }
}

type Controller struct{}

func (c *Controller) StoreMessage(ctx context.Context, req *StoreMessageReq) (res *StoreMessageRes, err error) {
    return nil, gerror.NewCode(gcode.CodeNotImplemented)
}

func main() {
    s := g.Server()
    s.Group("/", func(group *ghttp.RouterGroup) {
        group.Bind(new(Controller))
    })
    oai := s.GetOpenApi()
    oai.Config.CommonResponse = CommonRes{}
    oai.Config.CommonResponseDataField = `Data`
    s.SetOpenApiPath("/api.json")
    s.SetSwaggerPath("/swagger")
    s.SetPort(8199)
    s.Run()
}

执行后,访问地址 http://127.0.0.1:8199/swagger 可以查看 swagger ui,访问 http://127.0.0.1:8199/api.json 可以查看对应的 OpenAPIv3 接口文档。其中生成的 OpenAPIv3 接口文档如下:

{
    "openapi": "3.0.0",
    "components": {
        "schemas": {
            "main.StoreMessageReq": {
                "properties": {
                    "content": {
                        "format": "string",
                        "type": "string"
                    }
                },
                "type": "object"
            },
            "main.StoreMessageRes": {
                "properties": {
                    "id": {
                        "format": "string",
                        "type": "string"
                    }
                },
                "type": "object"
            },
            "interface": {
                "properties": {},
                "type": "object"
            },
            "main.EmptyRes": {
                "properties": {},
                "type": "object"
            },
            "struct": {
                "properties": {},
                "type": "object"
            }
        }
    },
    "info": {
        "title": "",
        "version": ""
    },
    "paths": {
        "/messages": {
            "post": {
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/main.StoreMessageReq"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "properties": {
                                        "code": {
                                            "format": "int",
                                            "type": "integer"
                                        },
                                        "message": {
                                            "format": "string",
                                            "type": "string"
                                        },
                                        "data": {
                                            "properties": {
                                                "id": {
                                                    "format": "string",
                                                    "type": "string"
                                                }
                                            },
                                            "type": "object"
                                        }
                                    },
                                    "type": "object"
                                }
                            }
                        },
                        "description": ""
                    },
                    "403": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/main.EmptyRes"
                                }
                            }
                        },
                        "description": ""
                    },
                    "500": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "properties": {
                                        "code": {
                                            "format": "int",
                                            "type": "integer"
                                        },
                                        "message": {
                                            "format": "string",
                                            "type": "string"
                                        },
                                        "data": {
                                            "properties": {},
                                            "type": "object"
                                        }
                                    },
                                    "type": "object"
                                },
                                "examples": {
                                    "example 1": {
                                        "value": {
                                            "code": 1,
                                            "message": "Server Dead",
                                            "data": null
                                        }
                                    }
                                }
                            }
                        },
                        "description": ""
                    }
                },
                "summary": "Store a message"
            }
        }
    }
}

可以看到默认的响应状态码已经被更改为 201,并且响应示例也自动生成。

六、常见状态码使用场景

在实际应用中,不同的 HTTP 状态码用于表示不同的响应情况。以下是一些常见的状态码及其使用场景:

  • 200 OK:请求成功,返回请求的数据。
  • 201 Created:资源创建成功,通常用于 POST 请求。
  • 204 No Content:请求成功,但没有返回内容,通常用于 DELETE 请求。
  • 400 Bad Request:请求参数错误或格式不正确。
  • 401 Unauthorized:未授权,需要身份验证。
  • 403 Forbidden:已授权,但没有权限访问资源。
  • 404 Not Found:请求的资源不存在。
  • 500 Internal Server Error:服务器内部错误。

在实现 IEnhanceResponseStatus 接口时,可以根据实际业务需求,为这些不同的状态码提供相应的响应结构体和示例数据。

七、使用建议

  1. 结构体复用 :对于类似的响应结构,可以定义公共的结构体进行复用,减少代码重复。

  2. 错误码管理 :将错误码集中管理,例如使用 map 或常量来定义不同状态码对应的错误码,便于维护和扩展。

  3. 示例数据的真实性 :提供的示例数据应尽量真实,反映实际业务场景,便于前端开发者理解和处理。

  4. 文档同步更新 :当业务逻辑或错误码变更时,及时更新接口文档,保持文档与代码的一致性。

八、使用资源文件加载示例数据

在实际应用中,示例数据可能会比较大或复杂,直接在代码中编写可能会影响代码的可读性。GoFrame的goai组件提供了从外部文件加载示例数据的功能,支持从gres资源管理器或本地文件系统中读取示例数据,文件格式可以为json/xml/yaml/ini等gjson组件支持的格式。

1. 使用g.Meta标签指定示例文件

可以在响应结构体的g.Meta标签中使用resEg属性指定示例文件的路径:

type MyResponse struct {
    g.Meta `status:"201" resEg:"testdata/examples/201.json"`
    // ...其他字段
}

其中testdata/examples/201.json是示例文件的路径,可以是相对路径或绝对路径。

2. 示例文件格式

示例文件可以是一个JSON对象或JSON数组:

  • JSON对象 :每个键将作为示例的名称,值作为示例的内容。
{
  "example1": {
    "code": 0,
    "message": "Success",
    "data": { "id": 1 }
  },
  "example2": {
    "code": 1,
    "message": "Failed",
    "data": null
  }
}
  • JSON数组 :数组中的每个元素将作为一个示例,示例名称会自动生成为example 1、example 2等。
[
  {
    "code": 0,
    "message": "Success",
    "data": { "id": 1 }
  },
  {
    "code": 1,
    "message": "Failed",
    "data": null
  }
]

3. 使用gres资源管理器

GoFrame的goai组件会首先尝试从gres资源管理器中读取指定路径的文件,如果找不到,则会尝试从本地文件系统中读取。这使得可以将示例数据打包到程序中,方便分发和部署。

要使用gres资源管理器,需要先将示例文件打包到资源文件中:

package main

import (
    "github.com/gogf/gf/v2/os/gres"
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    // 打包资源文件
    gres.Dump()

    // 初始化服务器
    s := g.Server()
    // ...其他配置
    s.Run()
}

4. 完整示例

下面是一个使用资源文件加载示例数据的完整示例:

package main

import (
    "context"

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

type CreateUserReq struct {
    g.Meta  `path:"/users" method:"post" summary:"创建用户"`
    Name    string `json:"name" v:"required" dc:"用户名"`
    Email   string `json:"email" v:"required|email" dc:"电子邮箱"`
}

type CreateUserRes struct {
    g.Meta `status:"201" resEg:"testdata/examples/user/create_success.json"`
    Id     int    `json:"id" dc:"用户ID"`
}

func (r CreateUserRes) EnhanceResponseStatus() map[int]goai.EnhancedStatusType {
    return map[int]goai.EnhancedStatusType{
        400: {
            Response: struct{}{},
            // 从文件中加载错误示例
        },
        500: {
            Response: struct{}{},
            // 从文件中加载错误示例
        },
    }
}

type Controller struct{}

func (c *Controller) CreateUser(ctx context.Context, req *CreateUserReq) (res *CreateUserRes, err error) {
    // 实际业务逻辑
    return &CreateUserRes{Id: 1}, nil
}

func main() {
    s := g.Server()
    s.Group("/", func(group *ghttp.RouterGroup) {
        group.Bind(new(Controller))
    })
    s.SetOpenApiPath("/api.json")
    s.SetSwaggerPath("/swagger")
    s.Run()
}

其中testdata/examples/user/create_success.json文件内容可能是:

{
  "成功示例": {
    "code": 0,
    "message": "创建成功",
    "data": {
      "id": 1
    }
  }
}

这种方式可以使示例数据与代码分离,更容易维护和管理。

九、总结

GoFrame的 IEnhanceResponseStatus 接口提供了一种灵活、强大的方式来扩展 OpenAPIv3接口文档中的响应信息。通过实现该接口,开发者可以:

  1. 为API提供多种响应状态的文档,使文档更符合RESTful API设计规范。
  2. 自定义不同状态码的响应结构体,提供更精确的接口定义。
  3. 通过示例数据展示不同状态的响应格式,便于前端开发者理解和处理。
  4. 保持文档与代码的一致性,提高开发效率和协作质量。

通过合理使用 IEnhanceResponseStatus 接口,可以显著提升API文档的质量和完整性,为前后端开发者提供更好的开发体验。