Python API 类型系统的设计与演变(3)OpenAPI 与可解释性
- UID
- 1066743
|
Python API 类型系统的设计与演变(3)OpenAPI 与可解释性
OpenAPI 与可解释性对于在线服务的描述和定义,本文比较倾向于参考 OpenAPI 规范,原因是它对机器更加友好,有着严谨的 Spec 定义,有利于生成和分析,同时背后有谷歌、微软等商业公司和强大的社区支持。
相对于 、 等规范所强调的人类可读性(Human Readable), 更加注重定义的规范化和通用性,鼓励社区共同推进规范的演进,在本文写作时, 已经发布,一个欣欣向荣的社区也是影响本文选型的关键因素。
类型系统在这里的作用是,对在线服务的接口定义进行描述,并生成一个符合 OpenAPI 规范定义的 JSON 文档,以支持文档生成工具(比如 )、前端 Mock 工具(比如国内的 )、接口测试工具(比如下文提到的基于 的实现)和前端验证库的需要。
在 OpenAPI 规范中,与类型系统相关的部分主要集中在 paths、schema、data types 三个章节,本文主要实现 data types 章节中所描述的类型与 类型之间的映射,这里举几个特殊的例子。
表 1 OAS Data Type 与 Marshmallow Type 的映射OAS TypeOAS FormatMarshmallow描述 string email Email 电子邮件 string uuid UUID UUID integer enum Enum(Int) 上文中定义的枚举类型 string
List(Str) 字符串列表
在 的定义里,每一个类型(type)都有一个可选的格式(format)可以定义,通常是根据业务所需来定制,这里取 fields 类的类名(小写)作为 format 值。
这里有一个特例,对于容器类型,比如 Enum 和 List,它们的类型取决于它所包装的类型,对于在线服务,常常需要类型系统具有确定性,是不允许 Union 类型存在的,这样设计主要是为了减少序列化 / 反序列化的成本,同时简化代码的分支逻辑。
这里举例说明容器类型的类型定义是如何翻译成 的类型定义的:
清单 5. List(Int) 翻译为 OpenAPI/OAS 示例1
2
3
4
5
6
| {
"type": "array",
"items": {
"type": "integer"
}
}
|
清单 6. Enum(Int) 翻译为 OpenAPI/OAS 示例1
2
3
4
5
6
7
8
9
| {
"schema": {
"type": "integer",
"enum": [
400
404
]
}
}
|
接口测试与文档生成在完成了上述基础的工作之后,就要与测试框架进行集成了。
类型系统与测试框架集成的意义是什么呢?可以分两个类别来看待:
- 第一个类别是需要严格限定接口响应字段的类型,这个时候开发人员会在代码中对接口的响应做类型声明,那么在测试用例中,类型系统的作用自然就是对响应字段类型的校验了,本文称之为严格模式。
- 第二个类别是接口响应无类型声明,那么接口的响应定义就不再具备可解释性,而可解释性对自动化的文档生成是最重要的因素。本文所描述的在线业务处于这样一个阶段,所以在类型系统实现中主要解决的就是这个问题。
如果没有响应参数的类型定义,就需要推导响应的类型,类型推导的方式有两种,静态的和动态的(运行时)。
静态分析在 Python 2 中的实现难度比较高,因为大量的第三方库都没有明确的类型信息,同时许多要经过网络的上下游服务也都没有提供严格的定义,难以在这样复杂的环境中通过静态分析拿到接口响应类型信息。
由于团队有写接口测试的习惯,最终选择了在运行接口测试的时候,和 Python 的测试框架 py.test 集成,通过收集接口测试的返回值来做运行时的类型推导。
下面尽可能简单地描述一下一个真实的实现,本文使用 来做用例的定义,比如:
清单 7. 使用 Yaml 描述的测试用例示例1
2
3
4
5
6
7
8
| - uri: /echo
method: GET
desc: 测试 ECHO 服务
status: 200
params:
ping: "pong"
responses:
ping: "pong"
|
Pytest 提供了参数化的功能可以用来生成用例,apis 是用例定义的列表:
清单 8. 描述文件与 pytest 集成的示例1
2
3
4
5
| @pytest.mark.parametrize("case", apis)
def test_api(case, case_manager, mocker):
case_obj = case_manager.add(case)
case_obj.run(mocker)
print(case_obj.real_response)
|
用例执行后,用例的响应被保存下来,再尝试对每一个响应字段的值做一个简单的类型推导。
清单 9. 一种响应值类型推导的实现示例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
| pattern_inferer_map = {
date_pattern: {'type': 'string', 'format': 'date'},
datetime_pattern: {'type': 'string', 'format': 'date-time'},
ip_pattern: {'type': 'string', 'format': 'ip'},
uuid_pattern: {'type': 'string', 'format': 'uuid'},
base64_pattern: {'type': 'string', 'format': 'byte'},
// ...
}
def infer_value(value):
if isinstance(value, string_types):
for pattern, type_info in pattern_inferer_map.items():
if pattern.match(value):
return type_info
return {'type': 'string'}
elif isinstance(value, int):
return {'type': 'number', 'format': 'int64'}
elif isinstance(value, float):
return {'type': 'number', 'format': 'double'}
elif isinstance(value, bool):
return {'type': 'boolean'}
def inferer_response(response):
return {k: infer_value(v) for k, v in response.items()}
|
暴力地对 Python 类型和 OAS 的类型做一个映射,这样就用最简单的办法完成了一个接口响应的类型推断。
很容易看出,这样的类型推断会存在许多问题,比如 int 和 float 类型的精度无法表达,字符串类型的 format 可能会有误判,尤其依赖完备的测试用例等等。
但本文为什么仍然愿意推荐这种方法,因为它可以使用最小的成本,最大限度地满足研发人员的基本诉求——拿到接口相应的基本类型信息,提升可解释性,这是类型系统中非常重要的一部分。
小结就这样,本文通过重载服务框架的路由装饰器来收集 API 的参数类型信息,通过接口测试来收集 API 的响应类型信息,通过注册自定义的枚举类型和业务类型,再配合框架本身的属性,就可以生成定制化的、符合 OpenAPI 规范的文档了。
拥有类型系统的在线服务,在接口校验、异常处理、测试和文档生成等方面都有全方位的提升,满足了工程师们对一个服务在安全性和可解释性上的基本诉求,这是非常值得投入的一件事。 |
|
|
|
|
|