← 中文资源

简体中文版本

GraphQL Client 类型化查询

用 graphql-client 从查询文档派生响应类型:编译期生成 Rust 结构体,变量与响应全程强类型,GraphQL 错误显式分流。

阅读英文原文

示例说明

GraphQL 的响应形状由查询文档决定——graphql-client 在编译期解析查询与 schema,生成与选择集精确对应的 Rust 结构体:选了 id、name、lifetimeValue,响应类型就只有这三个字段,拼错字段名直接编译失败。变量结构体同样生成,region 与 first 的类型与 schema 参数定义一致。

技术要点

  • 查询文档编译期生成响应类型
  • 变量结构体与 schema 参数对齐
  • GraphQL 层错误与 HTTP 层错误分流

代码解读

TopCustomers 结构体的 derive 属性声明查询与 schema 的路径;build_query 把变量序列化为请求体,reqwest 发送后反序列化为生成的 Response 泛型——data 与 errors 两个字段都是 Option,查询级错误逐条打印不静默。edges 展平为节点集合,空连接回退空集。

使用建议

生成代码由构建脚本驱动,查询文档变更时重新生成即暴露差异。认证头等公共配置建议封装客户端工厂。分页遵循连接规范时可写通用的游标推进器。

代码示例

use graphql_client::{GraphQLQuery, Response};

// queries/top_customers.graphql:
// query TopCustomers($region: Region!, $first: Int!) {
//   customers(region: $region, first: $first) {
//     edges { node { id name lifetimeValue } }
//   }
// }

#[derive(GraphQLQuery)]
#[graphql(
    query_path = "queries/top_customers.graphql",
    schema_path = "schemas/customer.graphql",
    response_derives = "Debug"
)]
pub struct TopCustomers;

type Customer = top_customers::TopCustomersCustomersEdgesNode;

async fn fetch_top_customers(
    region: &str,
    first: i64,
) -> Result<Vec<Customer>, Box<dyn std::error::Error>> {
    let variables = top_customers::Variables {
        region: region.into(),
        first,
    };

    let request_body = TopCustomers::build_query(variables);
    let client = reqwest::Client::new();

    let res = client
        .post("https://api.example.com/graphql")
        .json(&request_body)
        .send()
        .await?
        .error_for_status()?;

    let response_body: Response<top_customers::ResponseData> = res.json().await?;

    let data = response_body
        .data
        .ok_or("query returned no data")?;

    if let Some(errors) = response_body.errors {
        for err in &errors {
            eprintln!("graphql error: {:?}", err.message);
        }
    }

    let customers: Vec<Customer> = data
        .customers
        .map(|conn| {
            conn.edges
                .into_iter()
                .filter_map(|edge| edge.map(|e| e.node))
                .collect()
        })
        .unwrap_or_default();

    Ok(customers)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let customers = fetch_top_customers("eu", 5).await?;
    for customer in customers {
        println!("{:?} value={}", customer, customer.lifetime_value);
    }
    Ok(())
}