EAML(EasyOps API Modeling Language) 是参考了 grpc, swagger, raml 等现有业界方案后定制化的 API 描述语言。
1.0 vs 2.0
- 大大简化了消息定义方式,减少 50%左右的重复消息定义
- 以领域模型对象为核心定义 model, 并附带校验规则,上层接口消费
- 消息内置于 request 和 response 内部,不需要显式声明 message, 定义更自然
- 将原先分散于各个组件的接口定义统一到契约中心
定义规范的目的
- 标准化消息结构定义
- 标准化接口定义
- 增加消息约束校验能力
- 支撑 API 全面测试
- 支撑 RPC 框架功能强化
接口间用结构化的消息来进行远程调用。
目录结构
contract-center
├── CONTRIBUTING.md
├── README.md
├── easyops
│ ├── api # api 接口契约
│ │ ├── README.md
│ │ └── cd # 持续交付产品 cd
│ │ ├── package # 包模型
│ │ │ ├── batch_update_package_permission.yaml
│ │ │ ├── create.yaml
│ │ │ ├── delete.yaml
│ │ │ ├── get_package_detail.yaml
│ │ │ ├── get_package_list.yaml
│ │ │ ├── get_package_permission.yaml
│ │ │ ├── get_package_user_variable.yaml
│ │ │ ├── init_package_permission.yaml
│ │ │ ├── search.yaml
│ │ │ ├── update.yaml
│ │ │ ├── update_package_permission.yaml
│ │ │ └── upsert_package_user_variables.yaml
│ │ └── version # 版本模型
│ │ ├── create_version_with_sign.yaml
│ │ ├── delete_version.yaml
│ │ ├── get_version_detail.yaml
│ │ ├── get_version_list.yaml
│ │ ├── get_version_permission.yaml
│ │ ├── update.yaml
│ │ ├── update_version_env_type.yaml
│ │ └── update_version_permission.yaml
│ ├── brick # 构件契约
│ │ ├── README.md
│ │ ├── cd
│ │ └── common
│ │ ├── creator.yaml
│ │ ├── deleter.yaml
│ │ ├── editor.yaml
│ │ ├── explorer.yaml
│ │ ├── selector.yaml
│ │ └── viewer.yaml
│ ├── model # 模型定义
│ │ ├── cd # 持续交付 cd 产品
│ │ │ ├── instance.yaml
│ │ │ ├── package.yaml
│ │ │ ├── package_ext.yaml
│ │ │ ├── version.yaml
│ │ │ └── white_permission_user.yaml
│ │ └── common
│ └── type # 类型定义
│ ├── datetime.yaml
│ ├── email.yaml
│ ├── env_type.yaml
│ ├── file_path.yaml
│ ├── guid.yaml
│ ├── ip.yaml
│ ├── page.yaml
│ ├── page_size.yaml
│ ├── password.yaml
│ ├── user_name.yaml
│ ├── version.yaml
│ └── white_permisson_type.yaml
└── schema # json-schema, 对 brick, interface, model, type 的定义进行约束
├── brick.json
├── interface.json
├── model.json
└── type.json
模型定义
模型是将我们产品中核心对象抽象出来,按核心对象进行定义,可以基本按数据库的设计,也可以有一些扩展模型。 一个示例定义(model/cd/package.yaml)如下:
_version_: 2.0 # 框架协议版本
_kind_: model # 契约类型为 model
name: Package # 契约名称
description: 包模型 # 模型简要描述
fields: # 模型字段定义
- name: packageId # 字体名称
type: guid # 字段定义
description: 包 ID # 字段描述
- name: name
type: string
description: 包名称
validate: # 字段校验规则
gte: 1
lte: 45
- name: type
type: env_type
description: 版本类型 1 开发, 3 测试, 7 预发布, 15 生产
- name: cId
type: int
description: 包分类
- name: source
type: string
description: 包文件源
- name: repoId
type: string
description: repoId
- name: repoPath
type: string
description: repoPath
- name: memo
type: string
description: 备注说明
validate:
gte: 1
- name: creator
type: user_name
description: 创建者
- name: org
type: int
description: org
- name: category
type: string
description: 包分类标签
- name: icon
type: string
description: 包图标
- name: style
type: string
description: 包图标样式(颜色)
- name: ctime
type: datetime
description: ctime
- name: mtime
type: datetime
description: mtime
- name: authUsers
type: user_name
description: authUsers
- name: installPath
type: file_path
description: 安装路径
- name: platform
type: string
description: 平台
模型文件命名规范
命名规则
- 小写字母、下划线
- 模型的定义上线后,只能修改字段的描述,如果有字段类型的调整或者添加字段(不允许删除字段),需要增加新模型文件,如 package_2.yaml
示例: model/cd/package.yaml
name - 消息名称
命名规则
- 大写驼峰
name: Package
description - 消息描述
示例: description: 包模型
imports - 引用的模型
模型定义可以引用其它模型的定义,需要显式 import 到文件, 路径使用 / 分隔 示例:model/package_ext.yaml, 引用同级下的 Version 模型字段
_version_: 2.0
_kind_: model
name: PackageExt
description: 包模型
import:
- easyops/model/cd/version
fields:
- name: lastVersionInfo
type: object
fields:
- ref: Version.ctime
- ref: Version.name
- ref: Version.versionId
description: 最新版本信息
- name: instanceCount
type: int
description: 包实例数量
fields - 字段定义
name:字段名 命名规范:小写驼峰 原因是 Go 和 JavaScript 的命名风格都是驼峰为主流。见 MixedCaps
type:字段类型, 有三种来源, 基本内置类型、自定义类型、自定义模型
- a. 目前内置支持的字段类型
| 字段类型 | 对应到 go 的类型 | 对应到 typescript 的类型 | 备注 |
|---|---|---|---|
| int | int32 | number | |
| int64 | int64 | number | 契约版本 2.2 支持 |
| string | string | string | |
| float | float | number | |
| bool | bool | boolean | |
| file | - | File | 上传的文件 |
| map | protobuf.types.Struct (key 是 string, 值是 protobuf.types.Value 的结构体) | Record<string,any> | 固定结构的返回下不推荐使用,不然会写得比较恶心,慎用,如果想用,这里有一些便捷的方式来做 Struct 和 map[string]interface{}的互转: https://git.easyops.local/easyops-go/kit/tree/master/gogoprotobuf/protostruct |
| (type)[] | type 类型的数组,type 可以是上面说的的任意类型,定义方式可参考示例 | (type)[] | - |
b. 在 easyops/type 下自定义的变量
_version_: 2.0
_kind_: type
name: email
type: string
description: 电子邮箱地址
validate:
pattern: "^([A-Za-z0-9_\-\.\u4e00-\u9fa5])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,8})$/"c. 新的组合类型,object
_version_: 2.0
_kind_: model
name: PackageExt
description: 包模型
fields:
- name: lastVersionInfo
type: object
fields:
- ref: Version.ctime
- ref: Version.name
- ref: Version.versionId
description: 最新版本信息
- name: instanceCount
type: int
description: 包实例数量
ref 只能引用 model 中已经定义的类型 如果引用 model 中的所有字段, 可以是 Package.*
enum 枚举,只作为字段的约束手段,不再是独立类型。
特别注意,自定义类型的时候,至少要有 enum 或者 validate 一种约束。- name: platform
type: string
enum: ["linux", "windows", "others"]
description: 平台validate - 框架当前支持的校验方法
| pattern | 正则表达式 | 正则匹配 |
|---|---|---|
| gte | 大于等于的值 | 如果是字符串,就是字符串长度大于等于。如果是数字,就是数字大于等于。 如果是数组, 就是数组长度大于等于 |
| lte | 小于等于的值 | 如果是字符串,就是字符串长度小于等于。 如果是数字,就是数字小于等于。 如果是数组, 就是数组长度小于等于 |
| gt | 大于的值 | 如果是字符串,就是字符串长度大于。如果是数字,就是数字大于。如果是数组, 就是数组长度大于 |
| lt | 小于的值 | 如果是字符串,就是字符串长度小于。 如果是数字,就是数字小于。如果是数组, 就是数组长度小于 |
- 自定义 type
_version_: 2.0
_kind_: type
name: white_permission_type
type: string
enum: [deleteAuthorizers, readAuthorizers, updateAuthorizers]
description: 白色单权限类型
接口定义
一个示例接口定义(api/cd/create.yaml)如下:
_version_: 2.0
_kind_: "interface"
version: 1.0
name: "Create"
description: "创建包"
import:
- easyops/model/cd/package
endpoint:
method: "POST"
uri: "/package"
request:
type: object
fields:
- ref: Package.name
- ref: Package.type
- ref: Package.cId
- ref: Package.memo
- ref: Package.installPath
- ref: Package.platform
- ref: Package.source
- ref: Package.category
- ref: Package.icon
- ref: Package.style
required:
- Package.name
- Package.type
- Package.cId
- Package.memo
- Package.installPath
- Package.platform
response:
type: Package
required:
- Package.packageId
- Package.name
- Package.type
- Package.cId
- Package.memo
- Package.installPath
- Package.platform
- Package.source
- Package.category
- Package.icon
- Package.style
name - 接口名
接口名命名规则
- 大写字母开头驼峰命名
- CRUD 对应的命名
| HTTP VERB | 命名 | 备注 |
|---|---|---|
| GET 列表 | ListXxx | |
| GET 详情 | GetXxx | |
| PUT | UpdateXxx | |
| DELETE | DeleteXxx | 不能为 Delete, Delete 为 js 关键字 |
| POST | CreateXxx | |
| PATCH | PatchXx |
接口文件名规范
将 4.1 介绍的接口名转成小写下划线.yaml , 如 list_process_log.yaml
version: 接口版本
description: 接口描述
endpoint: HTTP 资源
| 字段名 | 备注 | 是否必填 |
|---|---|---|
| method | http 方法名 (get, post, put, delete),另外提供一个 list 的方法,给拉取列表使用 | 是 |
| uri | http uri, 中间有变量的话用 [:变量名]表示, 如 /v1/task/:task_id | 是 |
| ext_fields | ext_fields 指定 request 的 fields 里面的字段名来指定作为 POST 请求 body 或 GET 请求 url params 的字段定义。 如果不填,POST 请求默认用整个 request 作为 body 的数据结构定义, GET 请求默认用整个 request 作为 url params 的数据结构定义。这个字段的示例在 https://git.easyops.local/snippets/13 特殊场景下(比如请求类型是 map,uri 里面又带有参数)才会用到。 | 否 |
request - HTTP 请求定义
| 字段名 | 备注 | 是否必填 |
|---|---|---|
| type | 请求体类型, 可以为 object, model 中定义的类型、file | 是 |
| fields | type 为 object 时,需要定义 fields 字段 | 否 |
| required | 字段是否必须 | 否 |
| default | 字段的默认值,仅在这个字段是可选时生效 | 否 |
request 为空,则为 ~
type
type 代表请求消息的类型, 可以是 model 中的定义的具体类型 ,可以是自定义的 object 类型
a. 当请求等于 model 中定义的所有字段,type 的类型为 model 中定义的具体类型 (大部分情况下都是 object) 示例:
request:
type: Package
fields:
- ref: Package.name
- ref: Package.type
- ref: Package.cId
- ref: Package.memo
- ref: Package.installPath
- ref: Package.platform
- ref: Package.source
- ref: Package.category
- ref: Package.icon
- ref: Package.style
required:
- Package.name
- Package.type
- Package.cId
- Package.memo
- Package.installPath
- Package.platformb. 当返回的所有字段不是在同一个 model 中定义的,请求的 type 类型为 object 自定义类型
request:
type: object
fields:
- ref: Package.packageId
- name: permissionType
type: white_permission_type
description: 包权限类型
required:
- Package.packageId
fields
当 type 类型为 object 时,需要自定义 fields 字段,定义请求的所有字段,这里的字段类型可以是自定义类型, 也可以引用模型中已定义的字段
当类型为 object 时,定义的详情参见 model 中的 object 定义. 示例如下:
request:
type: object
fields:
- ref: Package.packageId # 引用 Package 模型的 packageId 字段
- name: permissionType # 自定义字段
type: white_permission_type
description: 包权限类型
required:
- Package.packageId
response - HTTP 返回定义
- 字段同 request 定义
- 框架或 sdk 会默认在 response 的 object 外封装一层
{ "code": 0, "error": "", "message": "", "data": response }
- 如果想自定义整个 response_message, 可以在 response 里面加上
wrapper: false参数, wrapper 默认为 true。 eaml 版本 2.1 带有这个特性。
custom_controller
这种一般都是接收了框架不支持的参数, 比如直接 post 一个数组过来,这时候需要配置这个设置
_version_: 2.0
_kind_: "interface"
version: 1.0
name: "GetPackageList"
description: "批量获取包信息"
import:
- easyops/model/artifact/package
- easyops/model/artifact/package_ext
endpoint:
method: "POST"
uri: "/package/list"
request:
type: string[]
response:
type: object
fields:
- ref: Package.* # reference package model
- ref: PackageExt.*
required:
- Package.*
- PackageExt
- PackageExt.*
custom_controller: true
private
接口是否私有,默认为 false. 如果是私有,不会向外部生成 api 文档
deprecated
接口是否被废弃,默认为 false
接口定义与 HTTP REST 的转换
新的后台框架采用消息契约编程模式,所以 HTTP REST API 的一些字段(body, uri 带的资源参数, query_parameter) 都要在通讯的消息定义中能找得到,以下是转换规则
POST
不支持 http 的 query parameter(查询参数)
PUT
同 5.1,注意,PUT 方法只支持对象全量更新操作,不要提供只更新部分属性的功能,否则会有坑。
实在要提供只更新部分属性的功能, 参考 google 的 fieldmask 设计。
DELETE
uri 中的参数名称必须在消息中定义 http 的 query parameter(查询参数)名称必须在接口中定义 返回时消息为 null
GET 列表
http 的 query parameter(查询参数)名称必须在消息中定义 返回时建议的消息结构为
| 字段名 | 字段类型 | 校验规则 | 是否必填 | 备注 |
|---|---|---|---|---|
| page | int | gte: 1 | 是 | 要取的 page 数字 |
| page_size | int | gte: 1, lte: 3000 | 是 | 要取的 page_size |
| total | int | 否 | 数据总数,不用取总数的时候就不用 | |
| list | 消息数组,如 Book[] | 是 | 资源的列表 |
GET 详情
uri 中的参数名称必须在消息中定义 http 的 query parameter(查询参数)名称必须在消息中定义
API 版本号
API 版本号要在 uri 里面体现出来, 如 /v1/xxx /v2/xxx