Windmill 后端 Rust API 客户端解析:从 OpenAPI 自动生成到集成测试的轻量实现
【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill
本指南围绕 Windmill 开源仓库中的backend/windmill-api-client目录展开,剖析这个专供后端服务使用的 Rust API 客户端的设计定位、生成流程、核心类型与真实调用场景。读完本文,你将理解 Windmill 如何让后端各模块通过统一的 Rust 客户端与 api server 通信,掌握其Client/create_client/types三大核心结构的使用方式,并了解它在集成测试体系中的实际落地方式。
客户端定位:只服务于后端内部的 OpenAPI 客户端
Windmill 的仓库采用了“前端 SDK 与后端 SDK 分离”的架构。面向最终用户的开源 SDK 有 typescript-client、python-client、go-client 等,而本文的主角windmill-api-client则是一个只存在于后端内部、不对外发布的 Rust crate。
它的定位在 backend/windmill-api-client/README.md 中写得很清楚:
This holds an autogenerated OpenAPI client in Rust. It's exclusively used in the backend to talk to the api server.
即:这是一个基于 OpenAPI 自动生成的 Rust 客户端,专门用于后端各进程调用 API 服务器。因此它不会被依赖到任何前端或用户侧 SDK 中,而是被windmill-api-integration-tests、windmill-test-utils以及backend/tests/下的系列集成测试引用。
注意:README 中使用的相对链接
../windmill-api/对应仓库根目录下的 backend/windmill-api/,即 Windmill 的 API 服务实现。
生成流程:README 描述的自动更新机制
README 对生成流程的描述非常简洁,只有两条指令:
sh bundle.sh- 运行
sh bundle.sh即可更新bundled.json; - 生成的源码会自动随之更新;
- 前提是系统已安装
swagger-cli(用于完成 OpenAPI 文档的 bundle 操作)。
也就是说,这个客户端的代码形态是“从 OpenAPI 规范文件自动生成”的,开发者通常不需要手写接口调用代码。bundled.json是 API 规范的打包产物,客户端源码由它派生。
不过,从当前仓库的实际目录内容看,存在一些与 README 描述的差异,需要如实说明:
backend/windmill-api-client/目录下没有bundle.sh与bundled.json文件,当前可见的生成脚本是 build.sh;- build.sh 的内容为:
#!/bin/sh cd build_cargo cargo run --bin windmill_api_client_build- 而
build_cargo子目录在当前仓库中并未随仓库一起提交。
由此可以推断:README 反映的是这套客户端早期的“swagger-cli + bundle”生成工作流,而仓库当前保留的build.sh则指向一个未随仓库分发的独立构建工程(windmill_api_client_build二进制)。实际使用者应优先以仓库内真实存在的文件为准,README 中的bundle.sh流程可作为历史生成方式的参考。
工程结构与依赖
目录仅包含三个文件,结构非常精简:
backend/windmill-api-client/ ├── Cargo.toml # crate 清单与依赖 ├── README.md # 设计定位与生成说明 └── src/ └── lib.rs # 全部实现(约 770 行)backend/windmill-api-client/Cargo.toml 中声明的依赖同样精简:
[lib] name = "windmill_api_client" path = "./src/lib.rs" [dependencies] reqwest = { version = "0.12", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json.workspace = true urlencoding = "2"依赖选择与它的职责高度对应:
- reqwest 0.12 + json feature:负责异步 HTTP 请求与 JSON 序列化,是客户端的传输层;
- serde / serde_json:实现请求体与响应体的序列化/反序列化;
- urlencoding:对 workspace、path 等路径片段做 URL 编码,保证含特殊字符的资源路径可安全拼入 URL。
库名为windmill_api_client,外部通过use windmill_api_client::{Client, types}引用。
核心实现:一个手写的最小化客户端
需要特别指出的是,src/lib.rs 开头的文档注释(第 1–4 行)说明了现状:
Minimal Windmill API client for tests. This is a handwritten minimal client that provides just enough functionality for the integration tests. It replaces the auto-generated progenitor client.
即:当前lib.rs中的实现其实是一个手写的最小化客户端,只提供集成测试所需的最小功能,并取代了此前自动生成的 progenitor 客户端。这与 README 中“autogenerated OpenAPI client”的描述形成了有趣的演进关系——方向仍然是“为后端提供 API 客户端”,但实现方式已从自动生成切换到手工维护的精简版。因此下文的所有代码事实均以当前 lib.rs 实际内容为准。
Client 结构与工厂函数
核心结构Client只有两个字段(lib.rs#L10-L15):
#[derive(Clone)] pub struct Client { pub baseurl: String, pub client: reqwest::Client, }baseurl:API 服务器的基地址;client:可复用的reqwest::Client,通过Clone在测试中自由传递。
构造入口是顶层函数create_client(lib.rs#L174-L185):
pub fn create_client(base_url: &str, token: String) -> Client { let mut val = HeaderValue::from_str(&format!("Bearer {token}")).expect("header creation"); val.set_sensitive(true); let mut headers = HeaderMap::new(); headers.insert(AUTHORIZATION, val); let client = reqwest::ClientBuilder::new() .default_headers(headers) .build() .expect("client build"); Client::new_with_client(&format!("{}/api", base_url.trim_end_matches('/')), client) }它的关键行为有三点:
- Bearer Token 认证:将
{token}包装为Authorization: Bearer {token}请求头,并标记为sensitive(敏感值不进入日志); - 自动拼接
/api前缀:base_url去除末尾/后统一补上/api,因此调用方传入http://localhost:8000即可,无需关心 API 前缀细节; - 通过
Client::new_with_client组合出一个完整的Client。
错误模型
客户端统一使用一个自定义错误枚举(lib.rs#L187-L213):
pub enum Error { Request(reqwest::Error), // 网络/请求层错误 UnexpectedResponse(u16, String), // 非 2xx 响应,携带状态码与响应体 }其中UnexpectedResponse会把 HTTP 状态码和响应体文本一并返回,便于测试定位“接口返回了预期外的状态”这类问题;该枚举同时实现了From<reqwest::Error>、Display与std::error::Error,可直接通过?向上传播。
已实现的方法
impl Client块中(lib.rs#L17-L172)目前实现了五个方法,全部围绕集成测试的高频操作:
| 方法 | HTTP 动作 | 目标路径 | 说明 |
|---|---|---|---|
create_script | POST | /w/{workspace}/scripts/create | 新建脚本,返回脚本哈希(String) |
create_flow | POST | /w/{workspace}/flows/create | 新建流程,返回流程路径(String) |
get_flow_by_path | GET | /w/{workspace}/flows/get/{path} | 按路径读取流程,支持可选的with_starred_info查询参数 |
create_schedule | POST | /w/{workspace}/schedules/create | 为脚本/流程创建调度 |
update_schedule | POST | /w/{workspace}/schedules/update/{path} | 更新已有调度 |
list_workspaces | GET | /workspaces/list | 列出全部工作区 |
这些方法遵循同一套模式:用format!拼接 URL(workspace 与 path 均经urlencoding::encode处理)、reqwest发送请求、2xx 视为成功并解析 JSON(或直接取文本),否则构造Error::UnexpectedResponse。由于实现刻意保持最小化,目前并不包含完整的 OpenAPI 端点覆盖,这与“为集成测试提供刚刚够用的功能”的设计意图一致。
类型系统:覆盖脚本、流程与调度的请求/响应模型
types模块(lib.rs#L215-L770)承载了客户端所需的全部数据结构。
语言枚举:ScriptLang
ScriptLang(lib.rs#L219-L266)列出了 Windmill 当前支持的 22 种脚本语言,并通过serde(rename)映射到 API 使用的字符串:
python3、deno、go、bash、powershell、postgresql、mysql、bigquery、snowflake、mssql、oracledb、graphql、nativets、bun、php、rust、ansible、csharp、nu、java、ruby、duckdb。
此外还实现了FromStr(lib.rs#L268-L297),支持从字符串解析语言名;另有配套的RawScriptLanguage枚举用于流程中的原始脚本模块。
请求体:NewScript
NewScript(lib.rs#L348-L408)是创建脚本的请求体,字段覆盖了 Windmill 脚本的完整配置面:
- 必填字段:
content(脚本内容)、language(ScriptLang)、path、summary、description; - 调度/优先级:
priority、timeout、cache_ttl、concurrency_key、concurrent_limit、concurrency_time_window_s; - 运行控制:
dedicated_worker、delete_after_secs、restart_unless_cancelled、visible_to_runner_only、ws_error_handler_muted; - 依赖与参数:
lock、modules、schema(HashMap<String, serde_json::Value>)、envs; - 发布管理:
deployment_message、draft_only、parent_hash、is_template、tag、kind、auto_kind、has_preprocessor、on_behalf_of_email。
所有可选字段均使用#[serde(default, skip_serializing_if = "Option::is_none")](集合类字段为Vec::is_empty/HashMap::is_empty),保证未设置的字段不会出现在请求 JSON 中,保持请求体干净。
流程模型:OpenFlow 与 FlowModule
流程相关的类型是一个完整的递归结构(lib.rs#L498-L738):
OpenFlow:流程的顶层定义,含summary、description、schema与核心的value: FlowValue;FlowValue:流程体的配置,包括modules、failure_module、preprocessor_module、early_return、skip_expr、same_worker、并发与缓存控制等;FlowModule:单个模块,带id、summary、timeout、retry、skip_if、continue_on_error等公共属性,以及value: FlowModuleValue;FlowModuleValue:使用#[serde(untagged)]的枚举,覆盖 8 种模块形态——RawScript(内嵌脚本)、Script(引用脚本)、Flow(子流程)、ForLoop、WhileLoop、BranchOne、BranchAll、Identity,未知形态回退到serde_json::Value;InputTransform:同样是 untagged 枚举,表示模块输入既可以是Static静态值也可以是Javascript表达式;RawScript::new(content, language)提供了便捷构造器,自动补全type: "rawscript"。
这套类型与 Windmill 流程编辑器中的模块面板一一对应,通过 serde 的 untagged/flatten 机制直接映射 OpenFlow 的 JSON 结构。
调度与工作区
NewSchedule/EditSchedule(lib.rs#L413-L496):包含schedule(cron 表达式)、timezone、args、is_flow、script_path、path,以及一套完整的事件钩子:on_failure、on_failure_exact、on_failure_times、on_failure_extra_args、on_recovery、on_success等;Flow(lib.rs#L751-L758):get_flow_by_path的响应类型,用#[serde(flatten)]吸收未知额外字段;Workspace(lib.rs#L760-L769):list_workspaces的响应项,含id、name、owner。
在集成测试中的真实调用方式
windmill-api-client不是孤立存在的工具库,它在 Windmill 的集成测试体系中扮演“测试客户端”的角色。
最典型的用法在 backend/windmill-test-utils/src/lib.rs 的init_client(第 21–30 行)中:测试先用ApiServer::start拉起一个真实的 API 服务器,拿到随机端口,再调用windmill_api_client::create_client构造客户端:
let server = ApiServer::start(db).await.unwrap(); let port = server.addr.port(); let client = windmill_api_client::create_client( &format!("http://localhost:{port}"), "SECRET_TOKEN".to_string(), ); (client, port, server)随后测试用返回的Client依次调用create_script、create_flow、create_schedule,构造业务数据后再驱动 worker 执行,形成完整的端到端链路。同文件中还提供了init_client_agent_mode(第 32–45 行)用于 agent 模式的等价初始化。
在测试侧,消费该客户端的用例遍布多个测试文件:
- backend/tests/ci_tests.rs(如第 8 行
use windmill_api_client::types::{NewScript, ScriptLang})——CI 冒烟测试; - backend/tests/worker.rs、backend/tests/batch_rerun.rs、backend/tests/list_jobs.rs、backend/tests/workspace_export.rs 等——覆盖 worker 调度、批量重跑、作业列表、工作区导出等场景;
- backend/windmill-api-integration-tests/tests/ 下的
workspace_comparison.rs、triggers.rs、workspace_dependencies_git_sync.rs等——跨工作区比较、触发器、Git 同步依赖等专项验证。
从这些调用点可以看到:windmill-test-utils统一封装了“起服务器 + 建客户端”的样板逻辑,而windmill-api-client则被作为测试与真实 API 之间的唯一通信层复用,这正是 README 所说“exclusively used in the backend”的落地体现。
小结
backend/windmill-api-client是一个定位明确、实现克制的 Rust 后端内部 crate:
- 定位:专供 Windmill 后端与 api server 通信的 OpenAPI 客户端(详见 README);
- 演进:README 记载了 “
sh bundle.sh+ swagger-cli +bundled.json” 的自动生成工作流,而当前 lib.rs 是手写的最小化实现,取代了此前的 progenitor 自动生成客户端;两者共同的交付物都是这个供测试使用的统一客户端; - 实现:以
reqwest为传输层,create_client一键完成 Bearer 认证与/api前缀拼接,types模块完整覆盖脚本、流程、调度、工作区四类 API 的数据模型; - 价值:它把“如何调用 Windmill API”收敛成一套极小的 API 面,让
backend/tests/与windmill-api-integration-tests的数十个集成测试能以统一、可维护的方式与 API 服务器交互。
对于想在 Windmill 源码层面做二次开发或编写集成测试的工程师,windmill-api-client是一个理想的起点:类型定义集中在单一 lib.rs,API 路径与字段与 backend/windmill-api/ 中的路由一一对应,对照阅读即可快速理解 Windmill 后端的接口契约。
【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考