Skip to main content

EasyOps API 定义规范 V2.0

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 的类型备注
intint32number
int64int64number契约版本 2.2 支持
stringstringstring
floatfloatnumber
boolboolboolean
file-File上传的文件
mapprotobuf.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
PUTUpdateXxx
DELETEDeleteXxx不能为 Delete, Delete 为 js 关键字
POSTCreateXxx
PATCHPatchXx

接口文件名规范

将 4.1 介绍的接口名转成小写下划线.yaml , 如 list_process_log.yaml

version: 接口版本

description: 接口描述

endpoint: HTTP 资源

字段名备注是否必填
methodhttp 方法名 (get, post, put, delete),另外提供一个 list 的方法,给拉取列表使用
urihttp uri, 中间有变量的话用 [:变量名]表示, 如 /v1/task/:task_id
ext_fieldsext_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
fieldstype 为 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.platform
  • b. 当返回的所有字段不是在同一个 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(查询参数)名称必须在消息中定义 返回时建议的消息结构为

字段名字段类型校验规则是否必填备注
pageintgte: 1要取的 page 数字
page_sizeintgte: 1, lte: 3000要取的 page_size
totalint数据总数,不用取总数的时候就不用
list消息数组,如 Book[]资源的列表

GET 详情

uri 中的参数名称必须在消息中定义 http 的 query parameter(查询参数)名称必须在消息中定义

API 版本号

API 版本号要在 uri 里面体现出来, 如 /v1/xxx /v2/xxx