如何让含EdgeDB专属类型的结构体兼容async-graphql OutputType
解决EdgeDB类型兼容async-graphql OutputType的单结构体方案
方案一:直接为EdgeDB原生类型实现async-graphql的ScalarType trait
无需额外定义结构体,直接给edgedb_protocol::model下的类型实现ScalarType,既能保留EdgeDB Queryable所需的解码能力,又满足async-graphql的OutputType要求。
实现Uuid的ScalarType
use async_graphql::{Scalar, Value, ScalarType, Error}; use edgedb_protocol::model::Uuid; #[Scalar] impl ScalarType for Uuid { // 从GraphQL输入解析为Uuid fn parse(value: Value) -> Result<Self, Error> { match value { Value::String(s) => Uuid::parse_str(&s) .map_err(|e| Error::new(format!("无效的UUID格式: {}", e))), _ => Err(Error::new("UUID字段需传入字符串类型")), } } // 将Uuid序列化为GraphQL输出值 fn to_value(&self) -> Value { Value::String(self.to_string()) } }
实现Datetime的ScalarType
use async_graphql::{Scalar, Value, ScalarType, Error}; use edgedb_protocol::model::Datetime; use chrono::{DateTime, Utc, ParseError}; #[Scalar] impl ScalarType for Datetime { fn parse(value: Value) -> Result<Self, Error> { match value { Value::String(s) => { // 先解析为chrono的UTC时间,再转换为EdgeDB的Datetime let dt: DateTime<Utc> = DateTime::parse_from_rfc3339(&s) .map_err(|e: ParseError| Error::new(format!("无效的时间格式: {}", e)))? .with_timezone(&Utc); Ok(Datetime::from(dt)) } _ => Err(Error::new("时间字段需传入RFC3339格式的字符串")), } } fn to_value(&self) -> Value { // 将EdgeDB Datetime序列化为RFC3339字符串 Value::String(self.to_rfc3339()) } }
使用单结构体同时派生Queryable和SimpleObject
现在结构体可以直接同时使用两个派生宏,无需额外修改:
use edgedb_derive::Queryable; use async_graphql::SimpleObject; use edgedb_protocol::model::{Uuid, Datetime}; #[derive(Debug, Queryable, SimpleObject)] pub struct OtpPhoneRequest { pub id: Uuid, pub created_at: Option<Datetime>, // 其他业务字段... }
方案二:使用透明包装类型(避免直接修改外部类型)
如果不想直接为第三方库的类型实现trait(担心版本冲突或命名空间问题),可以用newtype包装EdgeDB类型,通过#[edgedb(transparent)]让Queryable自动透传内部类型的解码逻辑,同时为包装类型实现ScalarType。
定义包装类型
use async_graphql::{Scalar, Value, ScalarType, Error}; use edgedb_protocol::model::Uuid; use edgedb_derive::Queryable; #[derive(Clone, Copy, Debug, Queryable)] #[edgedb(transparent)] // 告诉EdgeDB派生宏直接使用内部类型的解码逻辑 pub struct GqlUuid(pub Uuid); #[Scalar] impl ScalarType for GqlUuid { fn parse(value: Value) -> Result<Self, Error> { let uuid = Uuid::parse_str(&value.to_string()) .map_err(|e| Error::new(format!("无效的UUID格式: {}", e)))?; Ok(Self(uuid)) } fn to_value(&self) -> Value { Value::String(self.0.to_string()) } } // 同理包装Datetime #[derive(Clone, Debug, Queryable)] #[edgedb(transparent)] pub struct GqlDatetime(pub Datetime); #[Scalar] impl ScalarType for GqlDatetime { // 解析和序列化逻辑同方案一,内部调用Datetime的方法 fn parse(value: Value) -> Result<Self, Error> { match value { Value::String(s) => { let dt: DateTime<Utc> = DateTime::parse_from_rfc3339(&s) .map_err(|e| Error::new(format!("无效的时间格式: {}", e)))? .with_timezone(&Utc); Ok(Self(Datetime::from(dt))) } _ => Err(Error::new("时间字段需传入RFC3339格式的字符串")), } } fn to_value(&self) -> Value { Value::String(self.0.to_rfc3339()) } }
使用包装后的结构体
#[derive(Debug, Queryable, SimpleObject)] pub struct OtpPhoneRequest { pub id: GqlUuid, pub created_at: Option<GqlDatetime>, // 其他业务字段... }
关键说明
- 方案一最简洁,直接复用EdgeDB原生类型,无需额外包装,适合大多数场景。
- 方案二通过包装类型隔离第三方依赖,适合对依赖侵入性有要求的项目。
- 两种方案都无需维护两个结构体,避免代码重复;同时
Option类型会自动继承内部类型的OutputType实现,无需额外处理。
内容的提问来源于stack exchange,提问作者FoxPro
相关产品推荐
相关产品推荐

