# cnb

[![Crates.io](https://img.shields.io/crates/v/cnb.svg)](https://crates.io/crates/cnb)
[![Documentation](https://docs.rs/cnb/badge.svg)](https://docs.rs/cnb)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Rust 1.75+](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](https://www.rust-lang.org)

> 非官方 Rust SDK，覆盖 [CNB (Cloud Native Build)](https://cnb.cool) 平台
> 公开的 [OpenAPI](https://api.cnb.cool) 全部 **241** 个端点（28 个资源
> 模块、271 个数据模型）。**0.2.0 是破坏性升级**——请阅读
> [CHANGELOG](./CHANGELOG.md) 中的迁移指南。

## 特性

- 🔐 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 拆分，按需开启

## 安装

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

最小依赖（仅启用需要的资源）：

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

## 快速开始

```rust
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）|

```rust
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，配合类型化响应：

```rust
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：

```rust
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?;
```

## 错误处理

```rust
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`。需要自定义：

```rust
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）

```toml
cnb = { version = "0.2", features = ["stream"] }
```

```rust
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）

```toml
cnb = { version = "0.2", features = ["tracing"] }
```

```rust
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::paginate` 与 `Stream` adaptors        |
| `all-resources`   |  ✓   | 启用所有资源模块                                       |
| `<resource>`      |  ✓   | 单独启用某个资源（`repositories`、`issues`、`git` …）  |

`rustls-tls` 与 `native-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](https://docs.rs/cnb)。

## 示例

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

```bash
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`](https://docs.rs/wiremock)，覆盖鉴权、错误体解析、
重试、`Retry-After`、默认头、典型资源 happy path：

```bash
cargo test --all-features
```

## 重新生成 SDK

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

```bash
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`](./codegen/README.md)。

## MSRV

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

## 升级到 0.2.0

0.2.0 不向后兼容 0.1.x。完整迁移指南见 [CHANGELOG.md](./CHANGELOG.md)。

## 许可证

[MIT](./LICENSE)

## 相关链接

- [CNB 官网](https://cnb.cool) ｜ [API 文档](https://api.cnb.cool) ｜ [使用文档](https://docs.cnb.cool)
- [docs.rs/cnb](https://docs.rs/cnb)
- [Issues / PRs](https://cnb.cool/aodoo/tools/rust-cnb)
