use async_graphql::{InputValueError, InputValueResult, Scalar, ScalarType, Value}; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; /// Hex-encoded git object id (sha1/sha256). #[derive(Clone, Copy, Debug)] pub struct ObjectIdScalar(pub gix::hash::ObjectId); #[Scalar( name = "ObjectId", specified_by_url = "https://git-scm.com/docs/gitglossary#def_object-id" )] impl ScalarType for ObjectIdScalar { fn parse(value: Value) -> InputValueResult { if let Value::String(s) = value { let oid = gix::hash::ObjectId::from_hex(s.as_bytes()) .map_err(|e| InputValueError::custom(format!("invalid object id: {e}")))?; return Ok(ObjectIdScalar(oid)); } Err(InputValueError::custom("ObjectId must be a hex string")) } fn to_value(&self) -> Value { Value::String(self.0.to_string()) } } impl From for ObjectIdScalar { fn from(id: gix::hash::ObjectId) -> Self { ObjectIdScalar(id) } } impl From for gix::hash::ObjectId { fn from(s: ObjectIdScalar) -> Self { s.0 } } /// Date + offset in seconds (sign matters). #[derive(Clone, Copy, Debug)] pub struct DateTimeScalar { pub timestamp: i64, pub offset_seconds: i32, } #[Scalar( name = "DateTime", specified_by_url = "https://git-scm.com/docs/git-commit#_date_formats" )] impl ScalarType for DateTimeScalar { fn parse(value: Value) -> InputValueResult { if let Value::Object(mut obj) = value { let ts = obj .swap_remove("timestamp") .ok_or_else(|| InputValueError::custom("DateTime: `timestamp` is required"))?; let off = obj .swap_remove("offsetSeconds") .ok_or_else(|| InputValueError::custom("DateTime: `offsetSeconds` is required"))?; let ts = match ts { Value::Number(n) => n .as_i64() .ok_or_else(|| InputValueError::custom("DateTime: `timestamp` must be int"))?, _ => { return Err(InputValueError::custom( "DateTime: `timestamp` must be a number", )); } }; let off = match off { Value::Number(n) => n.as_i64().ok_or_else(|| { InputValueError::custom("DateTime: `offsetSeconds` must be int") })? as i32, _ => { return Err(InputValueError::custom( "DateTime: `offsetSeconds` must be a number", )); } }; return Ok(DateTimeScalar { timestamp: ts, offset_seconds: off, }); } Err(InputValueError::custom( "DateTime must be an object with `timestamp` and `offsetSeconds`", )) } fn to_value(&self) -> Value { let mut obj = async_graphql::indexmap::IndexMap::new(); obj.insert( async_graphql::Name::new("timestamp"), Value::Number(self.timestamp.into()), ); obj.insert( async_graphql::Name::new("offsetSeconds"), Value::Number((self.offset_seconds as i64).into()), ); Value::Object(obj) } } impl From<&gitbaby::commit::types::Signature> for DateTimeScalar { fn from(sig: &gitbaby::commit::types::Signature) -> Self { let ts = sig.time.unix_timestamp(); DateTimeScalar { timestamp: ts, offset_seconds: sig.time.offset().whole_seconds(), } } } impl From for DateTimeScalar { fn from(dt: time::OffsetDateTime) -> Self { DateTimeScalar { timestamp: dt.unix_timestamp(), offset_seconds: dt.offset().whole_seconds(), } } } /// Bytes ↔ base64 string for blob payloads. #[derive(Clone, Debug)] pub struct Base64Scalar(pub Vec); #[Scalar( name = "Base64", specified_by_url = "https://www.rfc-editor.org/rfc/rfc4648" )] impl ScalarType for Base64Scalar { fn parse(value: Value) -> InputValueResult { if let Value::String(s) = value { let bytes = BASE64_ENGINE .decode(s.as_bytes()) .map_err(|e| InputValueError::custom(format!("invalid base64: {e}")))?; return Ok(Base64Scalar(bytes)); } Err(InputValueError::custom("Base64 must be a string")) } fn to_value(&self) -> Value { Value::String(BASE64_ENGINE.encode(&self.0)) } }