cnb 0.2.0

CNB (cnb.cool) API client for Rust — typed, async, production-ready
Documentation

cnb

Crates.io Documentation License: MIT Rust 1.75+

非官方 Rust SDK,覆盖 CNB (Cloud Native Build) 平台 公开的 OpenAPI 全部 241 个端点(28 个资源 模块、271 个数据模型)。0.2.0 是破坏性升级——请阅读 CHANGELOG 中的迁移指南。

特性

  • 🔐 Bearer Token 鉴权 + CNB_TOKEN 环境变量自动回退
  • 🧱 全部端点类型化(请求 DTO + 响应 struct,仅 spec 缺失 schema 时降级为 serde_json::Value
  • 🧰 查询参数 Builder(告别 Some(1), None, None, None, None…
  • 💥 结构化错误:保留状态码、原始响应体、code / message / request_id
  • 🔁 幂等方法的指数退避重试,自动尊重 429 Retry-After
  • ⚡ 完全异步(基于 tokio + reqwest 0.12),TLS 默认 rustls
  • 📊 可选 tracing:记录每次调用的 method / path / status / 耗时(绝不打印 token)
  • 📈 可选 stream:通用分页 helper,把 list_* 端点变成 Stream
  • 🚦 资源模块按 feature 拆分,按需开启

安装

[dependencies]
cnb = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

最小依赖(仅启用需要的资源):

[dependencies]
cnb = { version = "0.2", default-features = false,
        features = ["rustls-tls", "retry", "repositories", "issues"] }

快速开始

use cnb::ApiClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 自动读取 CNB_TOKEN 环境变量。
    let client = ApiClient::new()?;

    // 当前用户信息(类型化响应)。
    let me = client.users().get_user_info().await?;
    println!("{me:#?}");

    // 仓库详情(路径参数 + 类型化响应)。
    let repo = client.repositories().get_by_id("owner/repo".into()).await?;
    println!("{repo:#?}");

    Ok(())
}

鉴权

优先级(高 → 低) 方式
1 ApiClient::builder().token("ghp_xxx").build()?
2 环境变量 CNB_TOKEN (默认行为)
3 匿名(ApiClient::builder().no_token().build()? 或环境无 token)
let client = cnb::ApiClient::builder()
    .token("ghp_xxx")              // 显式 token,优先级最高
    .timeout(std::time::Duration::from_secs(15))
    .user_agent("my-app/1.0")
    .build()?;

调用风格:类型化 + Builder

每个查询参数被打包成对应的 XxxQuery Builder,配合类型化响应:

use cnb::repositories::GetReposQuery;

let query = GetReposQuery::new()
    .page(1i64)
    .page_size(20i64);

let repos: Vec<cnb::models::Repos4User> =
    client.repositories().get_repos(&query).await?;

写请求体使用类型化 DTO:

use cnb::models::PostIssueForm;

let body = PostIssueForm {
    title: Some("Bug: foo bar".into()),
    body: Some("Reproduction steps:".into()),
    ..PostIssueForm::default()
};

let issue = client.issues().create_issue("owner/repo".into(), &body).await?;

错误处理

use cnb::{ApiClient, ApiError};

match client.repositories().get_by_id("does/not-exist".into()).await {
    Ok(repo) => println!("{repo:#?}"),
    Err(ApiError::Http { status: 404, request_id, .. }) => {
        eprintln!("not found (request_id={request_id:?})");
    }
    Err(ApiError::Http { status, code, message, body, .. }) => {
        eprintln!("HTTP {status} ({code:?}): {message}\nraw: {body}");
    }
    Err(ApiError::Reqwest(e)) => eprintln!("transport: {e}"),
    Err(e) => eprintln!("other: {e}"),
}

重试

默认对 GET/HEAD/PUT/DELETE/OPTIONS 的 5xx / 408 / 429 / 网络错误进行最多 3 次 指数退避重试,并尊重 Retry-After。需要自定义:

use cnb::{ApiClient, RetryConfig};
use std::time::Duration;

let client = ApiClient::builder()
    .retry(RetryConfig {
        max_attempts: 5,
        base_delay: Duration::from_millis(100),
        max_delay: Duration::from_secs(10),
    })
    .build()?;

分页(stream feature)

cnb = { version = "0.2", features = ["stream"] }
use cnb::{pagination::paginate, repositories::GetReposQuery, ApiClient};
use futures::StreamExt;

let client = ApiClient::new()?;
let repos = client.repositories();
let stream = paginate(50, |page, page_size| {
    let repos = repos.clone();
    async move {
        let q = GetReposQuery::new().page(page).page_size(page_size);
        repos.get_repos(&q).await
    }
});
futures::pin_mut!(stream);
while let Some(repo) = stream.next().await {
    let repo = repo?;
    println!("{repo:?}");
}

Tracing(tracing feature)

cnb = { version = "0.2", features = ["tracing"] }
tracing_subscriber::fmt()
    .with_env_filter("cnb=debug")
    .init();

let client = cnb::ApiClient::new()?;
let _ = client.users().get_user_info().await?;
// DEBUG cnb api call method=GET path=/user status=200 elapsed_ms=87

Token 永远不会出现在日志里——Authorization 头被标记为 sensitive。

Cargo Features

Feature 默认 说明
rustls-tls 使用 rustls 作为 TLS 后端(无需系统 OpenSSL)
native-tls 使用系统 TLS(OpenSSL / Secure Transport)
retry 启用自动重试逻辑
tracing 启用 tracing instrumentation
stream 启用 pagination::paginateStream adaptors
all-resources 启用所有资源模块
<resource> 单独启用某个资源(repositoriesissuesgit …)

rustls-tlsnative-tls 互斥,需要切换时设置 default-features = false

资源模块速览

activities · ai · artifactory · assets · badge · build · charge · event · followers · git · git_settings · issues · knowledge_base · members · missions · organizations · pulls · rank · registries · releases · repo_code_issue · repo_contributor · repo_labels · repositories · search · security · starring · users · wiki · workspace

完整方法签名见 docs.rs/cnb

示例

examples/ 目录提供 7 个可运行示例:

CNB_TOKEN=xxx cargo run --example auth_with_env
CNB_TOKEN=xxx cargo run --example list_repos
CNB_TOKEN=xxx cargo run --example create_issue -- owner/name
CNB_TOKEN=xxx cargo run --example git_branches  -- owner/name
CNB_TOKEN=xxx cargo run --example error_handling
CNB_TOKEN=xxx RUST_LOG=cnb=debug \
              cargo run --features tracing --example tracing_setup
CNB_TOKEN=xxx cargo run --features stream --example pulls_pagination -- owner/name 1

测试

集成测试基于 wiremock,覆盖鉴权、错误体解析、 重试、Retry-After、默认头、典型资源 happy path:

cargo test --all-features

重新生成 SDK

整个 SDK(src/models/data.rs + 28 个资源模块)由 codegen/cnb-codegen 工具 从 codegen/spec/swagger.json 生成。 当上游 spec 更新时:

curl -sS -o codegen/spec/swagger.json https://api.cnb.cool/swagger.json
cargo run --manifest-path codegen/Cargo.toml -- \
    --spec codegen/spec/swagger.json --out src
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

详见 codegen/README.md

MSRV

最低支持 Rust 1.75。CI 在 stable + 1.75 双轨道上运行。

升级到 0.2.0

0.2.0 不向后兼容 0.1.x。完整迁移指南见 CHANGELOG.md

许可证

MIT

相关链接