# API Contract Validation
本文档说明如何验证 OpenLark 的 typed API 实现是否和飞书开放平台官方接口契约一致。
这套校验补充 `tools/validate_apis.py` 的覆盖率口径。覆盖率只回答“接口文件是否存在”,contract validation 进一步回答“实现里的 HTTP endpoint、request 字段和 response 字段是否和官方文档一致”。
## 1. 校验层级
### 1.1 Endpoint 离线校验
默认推荐入口:
```bash
just api-contracts
```
等价命令:
```bash
python3 tools/validate_api_contracts.py --all-crates --strict endpoint
```
该模式不访问网络,使用仓库根目录的 `api_list_export.csv` 作为官方快照,检查:
- Rust 实现文件是否存在;
- `ApiRequest::get/post/put/patch/delete(...)` 方法是否匹配 CSV 中的 HTTP method;
- Rust endpoint 常量或简单 `format!` 路径是否匹配 CSV 中的 `/open-apis/...` path。
报告输出:
- `reports/api_contracts/summary.md`
- `reports/api_contracts/summary.json`
- `reports/api_contracts/crates/<crate>.md`
- `reports/api_contracts/crates/<crate>.json`
CI 启用本层(全仓离线 endpoint)与 token 层(`openlark-security`,见 §1.4)两个 strict gate。
### 1.2 Endpoint live 校验
需要确认当前官网详情页是否和 checked-in CSV 快照一致时,使用:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-ai \
--live-endpoints \
--strict endpoint \
--report-dir /tmp/openlark-api-contracts-live-endpoints
```
该模式会访问飞书文档详情接口,读取 `schema.apiSchema.httpMethod` 和 `schema.apiSchema.path`,再和 Rust 实现比较。它适合人工抽样或 API 变动排查,不适合作为默认本地快检。
### 1.3 Field live 校验
字段级校验必须显式打开 live 模式:
```bash
just api-contract-fields openlark-ai 5
```
等价命令:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-ai \
--fields \
--live-fields \
--max-field-apis 5 \
--report-dir reports/api_contract_fields
```
该模式会读取当前官网详情页中的结构化 schema,并和 Rust 结构体的可序列化字段比较。当前覆盖:
- request body 顶层字段:`schema.apiSchema.requestBody.content.*.schema.properties`
- response data 顶层字段:`schema.apiSchema.responses.200.content.application/json.schema.properties.data.properties`
request 字段解析支持:
- `#[serde(rename = "...")]`
- `#[serde(rename_all = "camelCase")]`
- `Option<T>` optionality
- required request field missing
如果需要让字段漂移直接返回非零退出码,使用 strict 入口:
```bash
just api-contract-fields-strict openlark-ai 5
```
或:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-ai \
--fields \
--live-fields \
--max-field-apis 5 \
--strict fields
```
### 1.4 Token 类型 live 校验
核对 Rust 实现里 `.with_supported_access_token_types(...)` 声明的 token 类型,是否被
飞书官方文档「请求头 → Authorization」接受。这是 [#511](https://github.com/foxzool/openlark/issues/511)
acs / security_and_compliance 批量误配(误设 `App`/`app_access_token`)的防回归手段。
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-security \
--tokens \
--strict tokens \
--report-dir /tmp/openlark-api-contracts-tokens
```
oracle 取值(优先级递减):
1. 官网详情 payload 的 `schema.apiSchema.security.supportedAccessToken`(结构化,多数页面)。
2. 部分 `server-docs` 风格页(如 acs `user/get`、`user/list`)的 detail payload 不含该字段,
回退到抓取文档 `.md` 源的「请求头 Authorization」行(SPA 页正文来源,见
`docs/api-spec-accuracy-audit.md` 的核对方法节)。注意:该回退对每个 detail payload 缺标注
的接口会多一次 `.md` HTTP 抓取,全量核对时网络调用量会相应放大。
判定规则(被测对象 = Rust 声明的有效 token 集合,未显式声明时取默认 `[User, Tenant]`):
- Rust 集合与官方集合**不相交** → `ERROR`(运行时注入的 token 必被飞书拒绝)。
- 官方未标注 → `UNVERIFIED`(无法核对,不阻塞)。
- 实现文件缺失 → `WARN`。
- 存在交集(SDK 至少能选出一种官方接受的 token)→ 无 finding。
- 声明 `None`(自行管理鉴权、bypass token cache)但源码手动注入
`Authorization: Bearer <self.token_field>` 的端点(如 OIDC `authen/v1/user_info/get`),
按实际注入的 token 类型核对,而非 `none_access_token`——避免把「手动注入 user token」
误判为 disjoint `ERROR`。真正无鉴权(声明 `None` 且无手动注入)对要求 token 的文档仍报 `ERROR`。
与 live 校验一致,该维度访问网络。CI 对 `openlark-security` 与 `openlark-auth` 启用
`--strict tokens`,作为 [#511](https://github.com/foxzool/openlark/issues/511) acs /
security_and_compliance 误配的回归 gate(与 endpoint strict gate 同级;auth 覆盖 OIDC token
端点)。全仓 live 核对因每个接口都要抓取详情 payload、调用量过大,未纳入 CI;其他 crate 用
`just api-contract-tokens <crate>` 人工抽样。
## 2. 单 crate 使用
只验证一个 crate 的 endpoint:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-docs \
--strict endpoint
```
输出默认写到 `reports/api_contracts/`。如需避免污染本地报告目录:
```bash
python3 tools/validate_api_contracts.py \
--crate openlark-docs \
--strict endpoint \
--report-dir /tmp/openlark-api-contracts-docs
```
## 3. 结果解读
报告中的 severity:
| `ERROR` | 已确认 contract drift;strict 模式会失败 |
| `WARN` | 实现缺失、解析不到或低噪声风险;endpoint strict 当前不因 warning 失败 |
| `UNVERIFIED` | 官方详情或实现形态无法机器确认,需要人工判断 |
常见 finding code:
| `E_ENDPOINT_METHOD_MISMATCH` | Rust `ApiRequest::*` 方法和官方 method 不一致 |
| `E_ENDPOINT_PATH_MISMATCH` | Rust endpoint path 和官方 path 不一致 |
| `W_ENDPOINT_UNRESOLVED` | validator 暂时无法解析实现里的 endpoint 表达式 |
| `W_IMPLEMENTATION_FILE_MISSING` | CSV 期望的 API 文件不存在 |
| `E_REQUIRED_REQUEST_FIELD_MISSING` | 官网 required request field 在 Rust 请求结构体中缺失 |
| `W_OPTIONAL_REQUEST_FIELD_MISSING` | 官网 optional request field 在 Rust 请求结构体中缺失 |
| `W_REQUIRED_REQUEST_FIELD_OPTIONAL` | 官网 required 字段在 Rust 中建模为 `Option<T>` |
| `W_RESPONSE_FIELD_MISSING` | 官网 response `data` 字段在 Rust 响应模型中缺失 |
| `E_ACCESS_TOKEN_TYPE_MISMATCH` | Rust 声明的 token 类型全被官方文档拒绝(不相交,鉴权必失败) |
| `U_ACCESS_TOKEN_UNANNOTATED` | 官方文档未标注 `supportedAccessToken`/Authorization,token 类型无法核对 |
| `U_OFFICIAL_DETAIL_FETCH_FAILED` | live 模式无法获取官网详情 payload |
## 4. 当前已知验证证据
`openlark-ai` 的字段 live 烟测能发现真实漂移:
```bash
just api-contract-fields openlark-ai 1
```
当前报告会指出两个真实漂移:
- 官网 request body 要求 `multipart/form-data` 字段 `file`,而 Rust 实现是 `file_token, is_async`。
- 官网 response `data` 下有 `bank_card`,而 Rust 响应模型中是 `parsing_result` 及其派生字段。
这说明字段级 validator 已经能用官网当前结构化 schema 验出 request 和 response 的实现不一致。
## 5. 与覆盖率校验的关系
- `just api-coverage`:验证 API 文件覆盖率和缺失 API backlog。
- `just api-contracts`:验证已实现 API 的 endpoint contract。
- `just api-contract-fields`:抽样验证 request/response 字段是否和当前官网一致。
推荐日常顺序:
1. `just api-coverage`
2. `just api-contracts`
3. 对可疑 crate 跑 `just api-contract-fields <crate> <N>`
## 6. 当前限制
- 字段级校验目前只覆盖 request body 顶层字段和 response `data` 顶层字段。
- 嵌套字段、query/path 参数还未纳入 strict 比较。
- endpoint 解析对复杂 `to_url()` enum 或动态拼接会给出 `W_ENDPOINT_UNRESOLVED`,不会在 endpoint strict 模式下失败。
- live 模式依赖飞书官网详情接口,适合抽样和排查,不应替代离线快检。