OpenAPI学习笔记
OpenAPI是一套API规范( OpenAPI Specification ,OAS),用于定义RESTful API的接口。OpenAPI最初来自SmartBear的Swagger规范。
OpenAPI 目前的版本是3.0,当前Swagger和OpenAPI的关系是:
- OpenAPI是一套规范
- Swagger是实现OpenAPI规范的工具集,包括:
- Swagger Editor:允许你使用YAML语言在浏览器中编写规范,并实时查看生成的API文档
- Swagger UI:从OAS兼容的API动态生成一套Web的美观的API文档
- Swagger Codegen:用于生成OpenAPI的客户端库(SDK)、服务器桩代码、文档
- Swagger Parser:从Java解析Open API定义的独立库
- Swagger Core:一个Java库,用于创建、消费、使用OpenAPI定义
- Swagger Inspector:从现有的API生成OpenAPI定义、验证API的测试工具
- SwaggerHub:OpenAPI的API设计、文档平台
Swagger并非唯一支持OpenAPI的工具,到OpenAPI-Specification的GitHub页面可以看到相关工具的列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
{ // 基本信息 "swagger": "2.0", "info": { "title": "Kubernetes", "version": "v1.12.1" }, // API端点列表 "paths": { // 端点 "/api/": { // 操作 "get": { "description": "get available API versions", // 支持的MIME类型 "consumes": [ "application/json", "application/yaml", "application/vnd.kubernetes.protobuf" ], "produces": [ "application/json", "application/yaml", "application/vnd.kubernetes.protobuf" ], // 支持的协议 "schemes": [ "https" ], "tags": [ "core" ], // 操作的唯一标识 "operationId": "getCoreAPIVersions", // 请求参数说明 "parameters": [ { "type": "string", "description": "Username ", "name": "username", // 这是URL路径变量 "in": "path", "required": true }, { "name": "user", // 这是请求体参数,通常是映射到模型的JSON "in": "body", "required": true, "schema": { "type": "object", "$ref": "#/definitions/models.User" } }, { "type": "integer", "name": "size", // 这是请求参数 "in": "query" } ], // 响应说明,每个状态码对应一个元素 "responses": { "200": { "description": "OK", // 响应的Schema "schema": { "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.APIVersions" } }, "401": { "description": "Unauthorized" } } } } } // Schema定义列表 "definitions": { // Schema名以域名倒写形式 "io.k8s.apimachinery.pkg.apis.meta.v1.APIVersions": { "description": "APIVersions lists the versions that are available, to allow clients to discover the API at /api, which is the root path of the legacy v1 API.", // 必须属性 "required": [ "versions", "serverAddressByClientCIDRs" ], // 属性规格列表 "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", "type": "string" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", "type": "string" }, "serverAddressByClientCIDRs": { "description": "a map of client CIDR to server address that is serving this group. This is to help clients reach servers in the most network-efficient way possible. Clients can use the appropriate server address as per the CIDR that they match. In case of multiple matches, clients should use the longest matching CIDR. The server returns only those CIDRs that it thinks that the client can match. For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.", "type": "array", "items": { "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ServerAddressByClientCIDR" } }, "versions": { "description": "versions are the api versions that are available.", "type": "array", "items": { "type": "string" } } }, // K8S扩展字段 "x-kubernetes-group-version-kind": [ { "group": "", "kind": "APIVersions", "version": "v1" } ] } } } |
这是一个Node.js开发的工具,可以将Swagger 2.0转换为OAS 3.0,执行下面的命令安装:
1 |
npm install -g swagger2openapi |
调用格式: swagger2openapi source-spec.json [options]
常用选项说明:
1 2 3 4 5 6 7 8 9 10 11 12 |
--resolveInternal # 是否解析内部引用 --warnProperty # 警告扩展配置,默认x-s2o-warning -e, --encoding # 输入输出编码,默认utf8 -f, --fatal # 如果引用解析失败,则终止转换 -i, --indent # JSON缩进,默认4 -o, --outfile # 输出到文件而非标准输出 -p, --patch # 尝试修复源定义中的错误 -r, --resolve # 是否解析外部引用 -t, --targetVersion # OAS版本,默认3.0.0 -u, --url # 源的URL -w, --warnOnly # 遇到不可修复错误时发出警告而非报错 -y, --yaml # 输出为YAML格式而非JSON |
通过某些工具,可以从既有代码中生成Swagger API文档。
该工具能够将Go Annotations转换为Swagger 2.0文档,为多种流行的Go Web框架(例如Gin)提供了插件,从而快速和既有Web项目集成。
执行下面的命令安装到GOPATH下:
1 |
go get -u github.com/swaggo/swag/cmd/swag |
在项目根目录,使用 swag init命令可以解析Go代码中的注解并且生成docs目录、docs/docs.go。你需要提供General API annotations,如果这些注解没有存放在根目录的main.go文件中,需要用 -g来指定Go文件路径:
1 |
swag init -g pkg/route/routers.go |
使用swag init命令之后,你需要导入生成的docs包以及swaggo的另外两个包:
1 2 3 |
_ "github.com/gmemcc/myproject/docs" import "github.com/swaggo/gin-swagger" // gin-swagger middleware import "github.com/swaggo/files" // swagger embed files |
General API annotations可用字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// @title Swagger Example API // @version 1.0 // @description This is a sample server celler server. // @termsOfService http://swagger.io/terms/ // @contact.name API Support // @contact.url http://www.swagger.io/support // @contact.email support@swagger.io // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost:8080 // @BasePath /api/v1 // @query.collection.format multi // @securityDefinitions.basic BasicAuth // @securityDefinitions.apikey ApiKeyAuth // @in header // @name Authorization // @securitydefinitions.oauth2.application OAuth2Application // @tokenUrl https://example.com/oauth/token // @scope.write Grants write access // @scope.admin Grants read and write access to administrative information // @securitydefinitions.oauth2.implicit OAuth2Implicit // @authorizationurl https://example.com/oauth/authorize // @scope.write Grants write access // @scope.admin Grants read and write access to administrative information // @securitydefinitions.oauth2.password OAuth2Password // @tokenUrl https://example.com/oauth/token // @scope.read Grants read access // @scope.write Grants write access // @scope.admin Grants read and write access to administrative information // @securitydefinitions.oauth2.accessCode OAuth2AccessCode // @tokenUrl https://example.com/oauth/token // @authorizationurl https://example.com/oauth/authorize // @scope.admin Grants read and write access to administrative information // @x-extension-openapi {"example": "value on a json format"} |
示例:
1 2 3 4 5 6 7 8 |
// @BasePath /myproject/apis/v2 // @version 2.0.0 // @title Myproject API // @description my project // @contact.name alex // @contact.email myproject@gmem.cc // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html |
swag init生成的docs包,导出了变量 SwaggerInfo,通过此变量你可以编程式的设置各种字段(和上面这种注释方式等效):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import ( "github.com/gin-gonic/gin" "github.com/swaggo/files" "github.com/swaggo/gin-swagger" "./docs" // docs is generated by Swag CLI, you have to import it. ) func main() { // programmatically set swagger info docs.SwaggerInfo.Title = "Swagger Example API" docs.SwaggerInfo.Description = "This is a sample server Petstore server." docs.SwaggerInfo.Version = "1.0" docs.SwaggerInfo.Host = "petstore.swagger.io" docs.SwaggerInfo.BasePath = "/v2" docs.SwaggerInfo.Schemes = []string{"http", "https"} |
为了在当前Web服务(的HTTP服务器)中查看API文档,需要注册路由,以gin为例:
1 2 3 4 5 6 7 |
r := gin.New() // use ginSwagger middleware to serve the API docs r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) r.Run() } |
要生成API,你需要在控制器代码中添加 API Operation annotations。这些注解需要加在controller代码中,所谓controller代码,就是包含所有路由处理函数的包,以gin为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package controller import ( "fmt" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/swaggo/swag/example/celler/httputil" "github.com/swaggo/swag/example/celler/model" ) // 下面就是一份API Operation annotations // ShowAccount godoc // @Summary Show a account // @Description get string by ID // @ID get-string-by-int // @Accept json // @Produce json // @Param id path int true "Account ID" // @Success 200 {object} model.Account // @Header 200 {string} Token "qwerty" // @Failure 400,404 {object} httputil.HTTPError // @Failure 500 {object} httputil.HTTPError // @Failure default {object} httputil.DefaultError // 这个仅仅影响生成的Swagger API文档,你还需要调用gin的接口,注册下面这个处理函数(控制器)的路由 // @Router /accounts/{id} [get] func (c *Controller) ShowAccount(ctx *gin.Context) { id := ctx.Param("id") aid, err := strconv.Atoi(id) if err != nil { httputil.NewError(ctx, http.StatusBadRequest, err) return } account, err := model.AccountOne(aid) if err != nil { httputil.NewError(ctx, http.StatusNotFound, err) return } ctx.JSON(http.StatusOK, account) } |
这些注解仅仅影响生成的Swagger API文档,不会对Web服务的逻辑产生任何影响。
运行Web服务,可以在http://localhost:port/swagger/index.html查看Swagger 2.0 API文档。访问/swagger/doc.json可以获得JSON格式的API
OAS是REST API的描述格式,在一个OpenAPI文件中,你可以定义完整的接口规格,包括:
- 可用API端点列表,针对每个端点允许的操作
- 操作的输入、输出参数
- 身份验证方法
- 附属信息,包括使用条款、License
关于OAS的编写,需要注意:
- 可以用YAML、JSON两种格式编写
- 所有关键字都是大小写敏感的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# OpenAPI的版本,可用版本 3.0.0, 3.0.1, 3.0.2 openapi: 3.0.0 info: title: '标题' description: '描述' version: '此OAS的版本,示例0.1.9' # 提供此API的服务器地址列表 servers: - url: http://api.example.com/v1 description: Optional server description, e.g. Main (production) server - url: http://staging-api.example.com description: Optional server description, e.g. Internal staging server for testing # API 端点列表 paths: # 端点 /users /users: # 端点/users的GET方法 get: # 描述性信息 summary: Returns a list of users. description: Optional extended description in CommonMark or HTML. # 响应规格说明 responses: # 200响应说明 '200': # status code description: A JSON array of user names # 200响应是JSON格式 content: application/json: # JSON的Schema schema: # 数组类型 type: array items: type: string # 端点/users的POST方法 post: summary: Creates a user. # 请求体 requestBody: required: true content: application/json: schema: type: object properties: username: type: string responses: '201': description: Created # 端点/user/{userId}的POST方法 # {} 中的是路径变量 /user/{userId}: get: summary: Returns a user by ID. # 参数说明 parameters: - name: userId # 这是一个路径变量 in: path required: true description: The ID of the user to return. # Schema中可以包含字段的验证规则 schema: type: integer format: int64 minimum: 1 responses: '200': description: A user object. content: application/json: schema: # 对象类型 type: object # 包含属性声明 properties: id: type: integer format: int64 example: 4 name: type: string example: Jessica Smith # 异常处理 '400': description: The specified user ID is invalid (not a number). '404': description: A user with the specified ID was not found. default: description: Unexpected error |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
components: # 定义了一个名为User的Schema schemas: User: properties: id: type: integer name: type: string # User包含两个属性,都是必须属性 required: - id - name |
下面的API引用上述Schema:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
paths: /users/{userId}: get: summary: Returns a user by ID. parameters: - in: path name: userId required: true type: integer responses: '200': description: OK content: application/json: schema: # 引用Schema $ref: '#/components/schemas/User' |
OAS中的所有API端点,相对于Base URL,假设Base URL为https://api.example.com/v1则端点/users的访问路径为https://api.example.com/v1/users。
Open API 3.0允许在servers数组中声明多个Base URL。
API服务器的URL中,可以包含变量:
1 2 3 4 5 6 7 8 9 10 |
- url: https://{customerId}.saas-app.com:{port}/v2 variables: customerId: default: demo description: Customer ID assigned by the service provider port: enum: - '443' - '8443' default: '443' |
你可以在路径级别,甚至操作(方法) 级别,覆盖servers数组:
1 2 3 4 5 |
/ping: get: servers: - url: https://echo.example.com description: Override base path for the GET /ping operation |
servers数组中的URL可以是相对URL,这种情况下,URL相对于提供OpenAPI定义的Web服务器。
举例来说,如果Open API定义存放在http://localhost:3001/openapi.yaml,则servers元素/v2解析为http://localhost:3001/v2
请求或者响应的媒体类型,在content元素下声明:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 200响应,媒体类型为JSON paths: /employees: get: summary: Returns a list of employees. responses: '200': # Response description: OK content: # Response body application/json: # Media type schema: # Must-have type: object # Data type |
可以指定多种媒体类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
paths: /employees: get: responses: '200': description: OK content: # JSON application/json: schema: $ref: '#/components/schemas/Employee' # XML application/xml: schema: $ref: '#/components/schemas/Employee' |
如果需要为多种媒体类型指定相同的Schema,可以使用 */*或者 application/*这样的通配符:
1 2 3 4 5 6 7 8 9 10 11 |
paths: /info/logo: get: responses: '200': description: OK content: image/*: schema: type: string format: binary |
在OAS中,路径即端点(资源),操作即HTTP方法。
路径可以包括路径变量,例如:
1 2 3 |
/users/{id} /organizations/{orgId}/members/{memberId} /report.{format} |
路径的description支持多行文本,还允许使用Markdown语法。
OpenAPI 3.0支持的HTTP方法包括:get, post, put, patch, delete, head, options, trace。每个路径可以支持多个操作。
OpenAPI 3.0支持通过路径、查询字符串、请求头、Cookie传递参数。对于POST/PUT/PATCH请求,还可以通过请求体传递数据。
这种参数不得定义在路径中,下面是错误的用法:
1 2 |
paths: /users?role={role}: |
下面则是正确的用法:
1 2 3 4 5 6 7 8 9 10 |
paths: /users: get: parameters: - in: query name: role schema: type: string enum: [user, poweruser, admin] required: true |
你可以为操作指定一个标识符:
1 2 3 |
/users: get: operationId: getUsers |
此标识符必须在OAS中是唯一的。 operationId的使用场景包括:
- 某些代码生成器使用operationId生成对应的方法名
参数需要定义在operation或path的parameters字段下。每个参数包括名字、位置、数据类型(通过schema或content定义)等属性。
参数可以通过以下HTTP元素传递:
- 路径参数
- 查询参数
- 请求头参数
- Cookie参数
1 2 3 4 5 6 7 8 9 10 11 |
/users/{id}: get: parameters: - in: path # 必须和路径变量{}中的一样 name: id required: true schema: type: integer minimum: 1 description: The user ID |
路径参数可以是数组,或者对象。这种参数在URL中的串行化形式可以是:
- 路径风格展开,分号分隔,示例: /map/point;x=50;y=20
- 标签展开,点号前缀,示例: /color.R=100.G=200.B=150
- 简单形式,逗号分隔,示例: /users/12,34,56
具体使用哪种串行化风格,通过style、explode关键字指定。
这是最常见的参数形式,出现在URL的?后面。
1 2 3 4 5 6 7 8 9 |
parameters: - in: query name: offset schema: type: integer - in: query name: limit schema: type: integer |
RFC 3986规定 :/?#[]@!$&'()*+,;=为URI特殊字符。如果查询参数中包含这些字符,必须以%HEX形式编码。
1 2 3 4 5 6 7 8 9 10 11 |
paths: /ping: get: summary: Checks if the server is alive parameters: - in: header name: X-Request-ID schema: type: string format: uuid required: true |
1 2 3 4 5 6 7 8 9 10 11 |
parameters: - in: cookie name: debug schema: type: integer enum: [0, 1] default: 0 - in: cookie name: csrftoken schema: type: string |
使用default关键字可以为optional参数提供默认值:
1 2 3 4 5 6 7 8 |
parameters: - in: query name: offset schema: type: integer minimum: 0 default: 0 required: false |
要描述复杂参数的内容,可以使用schema或者content。这两个关键字是互斥的,用于不同的场景下。
大部分情况下,可以考虑使用schema,使用schema可以描述原始类型、串行化为字符串的对象、简单数组。对象、数组的串行化方法在style、explode关键字中定义:
1 2 3 4 5 6 7 8 9 |
- in: query name: color schema: type: array items: type: string # 串行化为 color=blue,black,brown 形式 style: form explode: false |
style、explode无法满足的复杂串行化需求,可以使用content:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
parameters: - in: query name: filter content: # 细粒度的控制如何串行化为JSON application/json: schema: type: object properties: type: type: string color: type: string |
可以为某个参数指定可选值的集合,这些值的类型必须和参数类型匹配:
1 2 3 4 5 6 7 8 9 |
parameters: - in: query name: status schema: type: string enum: - available - pending - sold |
常量参数 —— 仅仅包含一个枚举值的参数:
1 2 3 4 5 6 7 8 |
parameters: - in: query name: rel_date required: true schema: type: string enum: - now |
OpenAPI 3.0允许仅仅有名字,而没有值的参数,在URL中表现为 /path?parm的形式:
1 2 3 4 5 6 |
parameters: - in: query name: metadata schema: type: boolean allowEmptyValue: true |
你也可以在schema中声明nullable属性:
1 2 3 4 |
schema: type: integer format: int32 nullable: true |
可以将一个参数标记为已经废弃:
1 2 3 4 5 6 7 |
- in: query name: format required: true schema: type: string enum: [json, xml, yaml] deprecated: true |
所谓公共参数,即一个path下,所有方法都使用的参数定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/user/{id}: parameters: - in: path name: id schema: type: integer required: true description: The user ID get: ... patch: ... delete: |
OpenAPI支持以多种身份验证方式保护API:
- 基于Authorization头的身份认证:
- 不记名令牌(Bearer)
- 基本认证
- 请求头、Cookie、查询字符串中的API Key
- OAuth2
- OpenID连接发现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
securitySchemes: BasicAuth: type: http scheme: basic BearerAuth: type: http scheme: bearer ApiKeyAuth: type: apiKey in: header name: X-API-Key OpenID: type: openIdConnect openIdConnectUrl: https://example.com/.well-known/openid-configuration OAuth2: type: oauth2 flows: authorizationCode: authorizationUrl: https://example.com/oauth/authorize tokenUrl: https://example.com/oauth/token scopes: read: Grants read access write: Grants write access admin: Grants access to admin operations |
为方法添加security字段:
1 2 3 4 5 |
security: - ApiKeyAuth: [] - OAuth2: - read - write |
引用Schema:
1 2 |
schema: $ref: '#/components/schemas/User' |
本地引用: #/components/schemas/user
远程引用:
- 当前服务器下其它文件: $ref: 'document.json'
- 文件中的元素: $ref: 'document.json#/myElement'
- 其它服务器中的元素: $ref: http://path/to/your/resource.json#myElement
OpenAPITools/openapi-generator项目,用于从OpenAPI Spec(2和3版本)自动生成客户端库、服务器代码存根、文档以及配置文件。
openapi-generator广泛的支持各种常用语言和框架,对于Go来说,支持的Web框架包括Gin、Echo等。
用于生成OpenAPI 3.0的Go样板代码,以Echo为默认的Web框架。该库致力于尽量的简单化,而非通用化,它不会为所有OpenAPI Schemas生成强类型的Go代码。
默认情况下oapi-codegen会生成包括客户端、服务器、类型定义、内嵌Swagger Spec在内的所有代码。对应命令行选项
-generate=types,client,server,spec。
下面我们看看从宠物商店的OpenAPI Spec生成Go样板代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
openapi: "3.0.0" info: version: 1.0.0 title: Swagger Petstore description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification termsOfService: http://swagger.io/terms/ contact: name: Swagger API Team email: apiteam@swagger.io url: http://swagger.io license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html servers: - url: http://petstore.swagger.io/api paths: /pets: get: description: | Returns all pets from the system that the user has access to Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. operationId: findPets parameters: - name: tags in: query description: tags to filter by required: false style: form schema: type: array items: type: string - name: limit in: query description: maximum number of results to return required: false schema: type: integer format: int32 responses: '200': description: pet response content: application/json: schema: type: array items: $ref: '#/components/schemas/Pet' default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' post: description: Creates a new pet in the store. Duplicates are allowed operationId: addPet requestBody: description: Pet to add to the store required: true content: application/json: schema: $ref: '#/components/schemas/NewPet' responses: '200': description: pet response content: application/json: schema: $ref: '#/components/schemas/Pet' default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' /pets/{id}: get: description: Returns a user based on a single ID, if the user does not have access to the pet operationId: find pet by id parameters: - name: id in: path description: ID of pet to fetch required: true schema: type: integer format: int64 responses: '200': description: pet response content: application/json: schema: $ref: '#/components/schemas/Pet' default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' delete: description: deletes a single pet based on the ID supplied operationId: deletePet parameters: - name: id in: path description: ID of pet to delete required: true schema: type: integer format: int64 responses: '204': description: pet deleted default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Pet: allOf: - $ref: '#/components/schemas/NewPet' - type: object required: - id properties: id: type: integer format: int64 NewPet: type: object required: - name properties: name: type: string tag: type: string Error: type: object required: - code - message properties: code: type: integer format: int32 message: type: string |
通过下面的命令生成样板代码:
1 2 |
go get github.com/deepmap/oapi-codegen/cmd/oapi-codegen oapi-codegen petstore-expanded.yaml > petstore.gen.go |
OpenAPI的 /components/schemas段中定义了可复用对象类型,oapi-codegen会生成对应的Go结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Type definition for component schema "Error" type Error struct { Code int32 `json:"code"` Message string `json:"message"` } // Type definition for component schema "NewPet" type NewPet struct { Name string `json:"name"` Tag *string `json:"tag,omitempty"` } // Type definition for component schema "Pet" type Pet struct { // Embedded struct due to allOf(#/components/schemas/NewPet) NewPet // Embedded fields due to inline allOf schema Id int64 `json:"id"` } // Type definition for component schema "Pets" type Pets []Pet |
对于在handler中定义的inline类型,只会生成内联的、匿名的Go结构。这会导致重复的代码,应该避免。
对于paths中的每一个元素,都会生成一个Go Handler函数:
1 2 3 4 5 6 7 8 9 10 |
type ServerInterface interface { // (GET /pets) FindPets(ctx echo.Context, params FindPetsParams) error // (POST /pets) AddPet(ctx echo.Context) error // (DELETE /pets/{id}) DeletePet(ctx echo.Context, id int64) error // (GET /pets/{id}) FindPetById(ctx echo.Context, id int64) error } |
请求参数通过以下方式传递:
- 大部分情况下,函数被编解码到echo.Context中
- 路径变量作为Handler函数的参数
- 其它请求头参数、查询参数、Cookie参数被存放在params变量中,例如:
12345// Parameters object for FindPetstype FindPetsParams struct {Tags *[]string `json:"tags,omitempty"`Limit *int32 `json:"limit,omitempty"` // 可选参数,作为指针}
使用命令行选项 -generate server可以为Echo生成Handlers注册函数:
1 2 3 4 5 6 7 8 9 |
func RegisterHandlers(router codegen.EchoRouter, si ServerInterface) { wrapper := ServerInterfaceWrapper{ Handler: si, } router.GET("/pets", wrapper.FindPets) router.POST("/pets", wrapper.AddPet) router.DELETE("/pets/:id", wrapper.DeletePet) router.GET("/pets/:id", wrapper.FindPetById) } |
使用下面的代码注册Handlers到Echo服务器:
1 2 3 4 5 6 |
func SetupHandler() { var myApi PetStoreImpl // 这是你的服务器端实现 e := echo.New() petstore.RegisterHandlers(e, &myApi) ... } |
类似的,使用命令行选项 -generate chi-server可以生成Chi或net/http的Handlers注册函数。
OpenAPI默认隐含additionalProperties=true,也就是说请求中提供的任何没有明确定义的字段都应该被接受。由于在Go语言中处理这种动态属性需要大量样板代码,oapi-codegen默认假设additionalProperties=false,要改变此默认行为你需要修改OpenAPI Schema:
1 2 3 4 5 6 7 8 9 10 |
NewPet: required: - name properties: name: type: string tag: type: string additionalProperties: type: string |
这样会生成如下结构:
1 2 3 4 5 6 |
// NewPet defines model for NewPet. type NewPet struct { Name string `json:"name"` Tag *string `json:"tag,omitempty"` AdditionalProperties map[string]string `json:"-"` } |
使用命令行选项 -generate=client可以生成客户端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// The interface specification for the client above. type ClientInterface interface { // FindPets request FindPets(ctx context.Context, params *FindPetsParams, reqEditors ...RequestEditorFn) (*http.Response, error) // AddPet request with JSON body AddPet(ctx context.Context, body NewPet, reqEditors ...RequestEditorFn) (*http.Response, error) // DeletePet request DeletePet(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) // FindPetById request FindPetById(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) } // Client which conforms to the OpenAPI3 specification for this service. type Client struct { // The endpoint of the server conforming to this interface, with scheme, // https://api.deepmap.com for example. Server string // HTTP client with any customized settings, such as certificate chains. Client http.Client // A callback for modifying requests which are generated before sending over // the network. RequestEditors []func(ctx context.Context, req *http.Request) error } |
每个OpenAPI Schema中定义的操作都对应了一个客户端函数。
此工具专门用于从K8S模型类生成Open API模型(以Go结构的形式存放在GetOpenAPIDefinitions函数中)。
下载并安装:
1 2 3 4 5 |
git clone https://github.com/kubernetes/kube-openapi.git export GOPROXY=https://goproxy.io export GO111MODULE=on go install cmd/openapi-gen/openapi-gen.go |
要为指定的包生成模型,执行命令:
1 2 3 4 5 6 |
# 输入包的导入路径 openapi-gen -i git.pacloud.io/pks/helm-operator/pkg/apis/pks/v1 # 输出包的导入路径 -p git.pacloud.io/pks/helm-operator/pkg/apis/pks/v1 # 生成的函数所在文件的名称前缀 -O zz_generated.openapi |
你的CRD通常会引用K8S核心库中的模型,因此需要同时为它们生成模型:
1 2 |
openapi-gen -i k8s.io/api/core/v1 -p git.pacloud.io/pks/helm-operator/pkg/apis/core/v1 openapi-gen -i k8s.io/apimachinery/pkg/apis/meta/v1 -p git.pacloud.io/pks/helm-operator/pkg/apis/meta/v1 |
使用Operator Framework开发CRD的控制器时,你可以使用注释 +k8s:openapi-gen=true来生成Open API模型,生成的Schema示例如下:
下面的代码调用GetOpenAPIDefinitions函数,生成Swagger 2.0.0 API定义的JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
package main import ( "encoding/json" "fmt" pksv1 "git.pacloud.io/pks/helm-operator/pkg/apis/pks/v1" "github.com/go-openapi/spec" "k8s.io/kube-openapi/pkg/common" "log" "os" "strings" ) func main() { if len(os.Args) <= 1 { log.Fatal("version required") } version := os.Args[1] if !strings.HasPrefix(version, "v") { version = "v" + version } oAPIDefs := pksv1.GetOpenAPIDefinitions(func(name string) spec.Ref { return spec.MustCreateRef("#/definitions/" + common.EscapeJsonPointer(swaggify(name))) }) defs := spec.Definitions{} for defName, val := range oAPIDefs { defs[swaggify(defName)] = val.Schema } swagger := spec.Swagger{ SwaggerProps: spec.SwaggerProps{ Swagger: "2.0", Definitions: defs, Paths: &spec.Paths{Paths: map[string]spec.PathItem{}}, Info: &spec.Info{ InfoProps: spec.InfoProps{ Title: "Helm Release", Version: version, }, }, }, } jsonBytes, err := json.MarshalIndent(swagger, "", " ") if err != nil { log.Fatal(err.Error()) } fmt.Println(string(jsonBytes)) } func swaggify(name string) string { name = strings.Replace(name, "git.pacloud.io/pks/helm-operator/pkg/apis", "yun.gmem.cc", -1) parts := strings.Split(name, "/") hostParts := strings.Split(parts[0], ".") for i, j := 0, len(hostParts)-1; i < j; i, j = i+1, j-1 { hostParts[i], hostParts[j] = hostParts[j], hostParts[i] } parts[0] = strings.Join(hostParts, ".") return strings.Join(parts, ".") } |
生成的Swagger API,仅仅包含模型信息,也就是definitions字段,但是不包含API端点(paths)。
如果需要生成API端点,则需要启动一个API Server并将需要生成API端点的模型注册进去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
// k8s.io/*包的API非常不稳定,本样例基于kubernetes 1.13.9 package main import ( "context" "encoding/json" "flag" "fmt" pkscorev1 "git.pacloud.io/pks/helm-operator/pkg/apis/core/v1" pksmetav1 "git.pacloud.io/pks/helm-operator/pkg/apis/meta/v1" pksv1 "git.pacloud.io/pks/helm-operator/pkg/apis/pks/v1" "github.com/go-openapi/spec" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/watch" openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" genericoptions "k8s.io/apiserver/pkg/server/options" "k8s.io/klog" "k8s.io/kube-openapi/pkg/builder" "k8s.io/kube-openapi/pkg/common" "net" "os" ) // 这个Storage是资源CRUD操作的提供者 type StandardStorage struct { cfg ResourceInfo } // 强制它实现以下接口 var _ rest.GroupVersionKindProvider = &StandardStorage{} var _ rest.Scoper = &StandardStorage{} var _ rest.StandardStorage = &StandardStorage{} // GroupVersionKindProvider func (r *StandardStorage) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind { return r.cfg.gvk } // Scoper func (r *StandardStorage) NamespaceScoped() bool { return r.cfg.namespaceScoped } // Getter func (r *StandardStorage) New() runtime.Object { return r.cfg.obj } func (r *StandardStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { return r.New(), nil } func (r *StandardStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { return r.New(), nil } // Lister func (r *StandardStorage) NewList() runtime.Object { return r.cfg.list } func (r *StandardStorage) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { return r.NewList(), nil } // CreaterUpdater func (r *StandardStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { return r.New(), true, nil } // GracefulDeleter func (r *StandardStorage) Delete(ctx context.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) { return r.New(), true, nil } // CollectionDeleter func (r *StandardStorage) DeleteCollection(ctx context.Context, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { return r.NewList(), nil } // Watcher func (r *StandardStorage) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) { return nil, nil } type ResourceInfo struct { gvk schema.GroupVersionKind obj runtime.Object list runtime.Object namespaceScoped bool } type TypeInfo struct { GroupVersion schema.GroupVersion Resource string Kind string NamespaceScoped bool } type Config struct { Scheme *runtime.Scheme Codecs serializer.CodecFactory Info spec.InfoProps OpenAPIDefinitions []common.GetOpenAPIDefinitions Resources []TypeInfo } func (c *Config) GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { out := map[string]common.OpenAPIDefinition{} for _, def := range c.OpenAPIDefinitions { for k, v := range def(ref) { out[k] = v } } return out } func RenderOpenAPISpec(cfg Config) (string, error) { // 总是需要向Scheme注册corev1、metav1 metav1.AddToGroupVersion(cfg.Scheme, schema.GroupVersion{Version: "v1"}) unversioned := schema.GroupVersion{Group: "", Version: "v1"} cfg.Scheme.AddUnversionedTypes(unversioned, &metav1.Status{}, &metav1.APIVersions{}, &metav1.APIGroupList{}, &metav1.APIGroup{}, &metav1.APIResourceList{}, ) // API Server选项 options := genericoptions.NewRecommendedOptions("/registry/pks.yun.gmem.cc", cfg.Codecs.LegacyCodec(), &genericoptions.ProcessInfo{}) options.SecureServing.BindPort = 6445 options.Etcd = nil options.Authentication = nil options.Authorization = nil options.CoreAPI = nil options.Admission = nil // 自动生成的服务器证书的存放目录 options.SecureServing.ServerCert.CertDirectory = "/tmp/helm-operator" // 启动的API Server,监听的地址 publicAddr := "localhost" ips := []net.IP{net.ParseIP("127.0.0.1")} // 尝试自动生成服务器证书 if err := options.SecureServing.MaybeDefaultWithSelfSignedCerts(publicAddr, nil, ips); err != nil { klog.Fatal(err) } // API Server配置 serverConfig := genericapiserver.NewRecommendedConfig(cfg.Codecs) if err := options.ApplyTo(serverConfig, cfg.Scheme); err != nil { return "", err } serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(cfg.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(cfg.Scheme)) serverConfig.OpenAPIConfig.Info.InfoProps = cfg.Info // 完成配置, 创建服务器 genericServer, err := serverConfig.Complete().New("helm-operator-server", genericapiserver.NewEmptyDelegate()) if err != nil { return "", err } // 这里处理本服务器需要Serve的资源列表 table := map[schema.GroupVersion]map[string]rest.Storage{} { for _, ti := range cfg.Resources { // 对于每一种资源,根据其GV寻找存储库 var resmap map[string]rest.Storage if m, found := table[ti.GroupVersion]; found { resmap = m } else { // 如果找不到,则创建存储库(每个GV一个存储库) resmap = map[string]rest.Storage{} table[ti.GroupVersion] = resmap } gvk := ti.GroupVersion.WithKind(ti.Kind) // 创建这种资源的一个对象 obj, err := cfg.Scheme.New(gvk) if err != nil { return "", err } // 创建这种资源的列表对象 list, err := cfg.Scheme.New(ti.GroupVersion.WithKind(ti.Kind + "List")) if err != nil { return "", err } // 为资源创建存储,并Put到它的GV的存储库中 resmap[ti.Resource] = &StandardStorage{ResourceInfo{ // GVK信息 gvk: gvk, // 资源和资源列表的原型 obj: obj, list: list, // 提示此资源是否命名空间化 namespaceScoped: ti.NamespaceScoped, }} } } for gv, resmap := range table { // 为每个组创建API组信息 apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(gv.Group, cfg.Scheme, metav1.ParameterCodec, cfg.Codecs) storage := map[string]rest.Storage{} for r, s := range resmap { storage[r] = s } apiGroupInfo.VersionedResourcesStorageMap[gv.Version] = storage // 并安装到此服务器 if err := genericServer.InstallAPIGroup(&apiGroupInfo); err != nil { return "", err } } // 构件API规范,也就是Swagger 2.0 Spec spec, err := builder.BuildOpenAPISpec(genericServer.Handler.GoRestfulContainer.RegisteredWebServices(), serverConfig.OpenAPIConfig) if err != nil { return "", err } data, err := json.MarshalIndent(spec, "", " ") if err != nil { return "", err } return string(data), nil } func main() { flag.Parse() if len(os.Args) <= 1 { panic("version required") } version := os.Args[1] scheme := runtime.NewScheme() // 将我们需要处理的类型加入到scheme scheme.AddKnownTypeWithName(schema.GroupVersion{Group: "pks.yun.gmem.cc", Version: "v1"}.WithKind("Release"), &pksv1.Release{}) scheme.AddKnownTypeWithName(schema.GroupVersion{Group: "pks.yun.gmem.cc", Version: "v1"}.WithKind("ReleaseList"), &pksv1.Release{}) scheme.AddKnownTypeWithName(schema.GroupVersion{Group: "pks.yun.gmem.cc", Version: "v1"}.WithKind("WatchEvent"), &pksv1.Release{}) spec, err := RenderOpenAPISpec(Config{ Info: spec.InfoProps{ Version: version, Title: "Helm Operator OpenAPI", }, Scheme: scheme, Codecs: serializer.NewCodecFactory(scheme), OpenAPIDefinitions: []common.GetOpenAPIDefinitions{ pkscorev1.GetOpenAPIDefinitions, pksmetav1.GetOpenAPIDefinitions, pksv1.GetOpenAPIDefinitions, }, Resources: []TypeInfo{ { GroupVersion: schema.GroupVersion{Group: "pks.yun.gmem.cc", Version: "v1"}, Kind: "Release", Resource: "Release", NamespaceScoped: true, }, }, }) if err != nil { klog.Fatal(err.Error()) } fmt.Println(spec) } |
可以使用下面的插件:
1 2 3 |
plugins { id 'org.hidetake.swagger.generator' version '2.18.1' } |
根据API规格的版本,选择依赖:
1 2 3 4 5 |
dependencies { swaggerCodegen 'io.swagger:swagger-codegen-cli:2.4.2' // Swagger Codegen V2 swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.5' // Swagger Codegen V3 swaggerCodegen 'org.openapitools:openapi-generator-cli:3.3.4' // penAPI Generator } |
控制代码生成的配置片段:
1 2 3 4 5 6 7 8 9 |
swaggerSources { petstore { inputFile = file('petstore.yaml') code { language = 'spring' configFile = file('config.json') } } } |
执行命令: ./gradlew generateSwaggerCode,将自动生成代码到 build/swagger-code-petstore目录。
configFile字段可以指定一个外部(本质上就是 swagger-codegen-cli generate的)配置文件,格式如下:
1 2 3 4 5 6 |
{ "library": "spring-mvc", "modelPackage": "example.model", "apiPackage": "example.api", "invokerPackage": "example" } |
对于language=java,常用配置项列表:
配置项 | 说明 |
sortParamsByRequiredFlag | 排序风发参数,将必须参数放在前面 |
ensureUniqueParams | 是否一个操作内,保证参数名的唯一性 |
allowUnicodeIdentifiers | 是否允许Unicode标识符 |
modelPackage | 生成的模型类的包名 |
apiPackage | 生成的API类的包名 |
invokerPackage | 生成的代码的根包名 |
groupId | 生成的POM的信息 |
artifactId | |
artifactVersion | |
artifactDescription | |
sourceFolder | 源码目录 |
localVariablePrefix | 局部变量前缀 |
serializableModel | 生成的模型是否实现Serializable接口 |
fullJavaUtil | 如果生成Java代码,那么是否用全限定名称引用java.util下的类 |
withXml | 生成的类是否添加序列化为XML的支持 |
dateLibrary |
Java日期库: joda 用于遗留应用 |
java8 | 使用Java 8提供的API |
disableHtmlEscaping | 使用JSON时禁止HTML转译,如果使用byte[]字段应该设置为true |
useGzipFeature | 是否使用Gzip编码请求 |
useRuntimeException | 是否使用RuntimeException代替Exception |
library |
库模板,对于language=java,默认okhttp-gson。可选: jersey1,基于JacksonJSON串行化 |
Leave a Reply