use async_graphql::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
use crate::{
compact::{Bytes as CompactBytes, Utf8Bytes as CompactUtf8Bytes},
shared::{Bytes as SharedBytes, Utf8Bytes as SharedUtf8Bytes},
};
#[Scalar(name = "Bytes")]
impl ScalarType for SharedBytes {
fn parse(value: Value) -> InputValueResult<Self> {
match value {
Value::Binary(data) => Ok(Self::from(data)),
_ => Err(InputValueError::expected_type(value)),
}
}
fn is_valid(value: &Value) -> bool {
matches!(value, Value::Binary(_))
}
fn to_value(&self) -> Value {
Value::Binary(self.clone().into_bytes())
}
}
#[Scalar(name = "Bytes")]
impl ScalarType for CompactBytes {
fn parse(value: Value) -> InputValueResult<Self> {
match value {
Value::Binary(data) => Ok(Self::from(data)),
_ => Err(InputValueError::expected_type(value)),
}
}
fn is_valid(value: &Value) -> bool {
matches!(value, Value::Binary(_))
}
fn to_value(&self) -> Value {
Value::Binary(self.clone().into_bytes())
}
}
#[Scalar(name = "String")]
impl ScalarType for SharedUtf8Bytes {
fn parse(value: Value) -> InputValueResult<Self> {
match value {
Value::String(s) => Ok(Self::from(s)),
_ => Err(InputValueError::expected_type(value)),
}
}
fn is_valid(value: &Value) -> bool {
matches!(value, Value::String(_))
}
fn to_value(&self) -> Value {
Value::String(self.as_str().to_owned())
}
}
#[Scalar(name = "String")]
impl ScalarType for CompactUtf8Bytes {
fn parse(value: Value) -> InputValueResult<Self> {
match value {
Value::String(s) => Ok(Self::from(s)),
_ => Err(InputValueError::expected_type(value)),
}
}
fn is_valid(value: &Value) -> bool {
matches!(value, Value::String(_))
}
fn to_value(&self) -> Value {
Value::String(self.as_str().to_owned())
}
}
#[cfg(test)]
mod tests {
use async_graphql::{EmptyMutation, EmptySubscription, InputType, Object, Schema};
use super::*;
use crate::INLINE_CAP;
fn binary(len: usize) -> Value {
Value::Binary(::bytes::Bytes::from(std::vec![b'x'; len]))
}
#[test]
fn shared_bytes_parse_moves_the_allocation() {
for len in [1, INLINE_CAP, INLINE_CAP + 1, 4096] {
let source = ::bytes::Bytes::from(std::vec![7u8; len]);
let address = source.as_ptr();
let parsed = <SharedBytes as ScalarType>::parse(Value::Binary(source)).unwrap();
assert!(parsed.is_heap(), "len {len} left the shared strategy");
assert_eq!(parsed.as_slice().as_ptr(), address, "len {len} was copied");
}
}
#[test]
fn shared_bytes_to_value_shares_the_allocation() {
let value = SharedBytes::from(std::vec![7u8; INLINE_CAP + 1]);
let address = value.as_slice().as_ptr();
match <SharedBytes as ScalarType>::to_value(&value) {
Value::Binary(data) => assert_eq!(data.as_ptr(), address),
other => panic!("expected Value::Binary, got {other:?}"),
}
}
#[test]
fn compact_bytes_parse_inlines_up_to_the_limit() {
let inlined = <CompactBytes as ScalarType>::parse(binary(INLINE_CAP)).unwrap();
assert!(inlined.is_inline());
let source = ::bytes::Bytes::from(std::vec![7u8; INLINE_CAP + 1]);
let address = source.as_ptr();
let kept = <CompactBytes as ScalarType>::parse(Value::Binary(source)).unwrap();
assert!(kept.is_heap());
assert_eq!(kept.as_slice().as_ptr(), address);
}
#[test]
fn every_scalar_round_trips() {
let short = b"smol".as_slice();
let long = &std::vec![9u8; 4096][..];
for payload in [short, long] {
let shared = SharedBytes::copy_from_slice(payload);
let value = <SharedBytes as ScalarType>::to_value(&shared);
assert_eq!(
<SharedBytes as ScalarType>::parse(value)
.unwrap()
.as_slice(),
payload
);
let compact = CompactBytes::copy_from_slice(payload);
let value = <CompactBytes as ScalarType>::to_value(&compact);
assert_eq!(
<CompactBytes as ScalarType>::parse(value)
.unwrap()
.as_slice(),
payload
);
}
for payload in ["smol", &"é".repeat(4096)] {
let shared = SharedUtf8Bytes::from(payload);
let value = <SharedUtf8Bytes as ScalarType>::to_value(&shared);
assert_eq!(
<SharedUtf8Bytes as ScalarType>::parse(value)
.unwrap()
.as_str(),
payload
);
let compact = CompactUtf8Bytes::from(payload);
let value = <CompactUtf8Bytes as ScalarType>::to_value(&compact);
assert_eq!(
<CompactUtf8Bytes as ScalarType>::parse(value)
.unwrap()
.as_str(),
payload
);
}
}
#[test]
fn input_parse_resolves_on_the_rust_type() {
let bytes = binary(4096);
assert_eq!(
<SharedBytes as InputType>::parse(Some(bytes.clone()))
.unwrap()
.len(),
4096
);
assert_eq!(
<CompactBytes as InputType>::parse(Some(bytes))
.unwrap()
.len(),
4096
);
let text = Value::String("é".repeat(4096));
assert_eq!(
<SharedUtf8Bytes as InputType>::parse(Some(text.clone()))
.unwrap()
.as_str(),
"é".repeat(4096)
);
assert_eq!(
<CompactUtf8Bytes as InputType>::parse(Some(text))
.unwrap()
.as_str(),
"é".repeat(4096)
);
}
#[test]
fn text_scalars_refuse_binary_and_byte_scalars_refuse_text() {
let bytes = binary(4);
let text = Value::String("data".into());
assert!(!<SharedUtf8Bytes as ScalarType>::is_valid(&bytes));
assert!(<SharedUtf8Bytes as ScalarType>::parse(bytes.clone()).is_err());
assert!(!<CompactUtf8Bytes as ScalarType>::is_valid(&bytes));
assert!(<CompactUtf8Bytes as ScalarType>::parse(bytes).is_err());
assert!(!<SharedBytes as ScalarType>::is_valid(&text));
assert!(<SharedBytes as ScalarType>::parse(text.clone()).is_err());
assert!(!<CompactBytes as ScalarType>::is_valid(&text));
assert!(<CompactBytes as ScalarType>::parse(text).is_err());
}
struct Query;
#[Object]
impl Query {
async fn shared_bytes(&self, echo: SharedBytes) -> SharedBytes {
echo
}
async fn compact_bytes(&self, echo: CompactBytes) -> CompactBytes {
echo
}
async fn shared_utf8_bytes(&self, echo: SharedUtf8Bytes) -> SharedUtf8Bytes {
echo
}
async fn compact_utf8_bytes(&self, echo: CompactUtf8Bytes) -> CompactUtf8Bytes {
echo
}
async fn upstream_bytes(&self) -> ::bytes::Bytes {
::bytes::Bytes::new()
}
}
#[test]
fn sdl_declares_bytes_once_and_never_declares_string() {
let sdl = Schema::new(Query, EmptyMutation, EmptySubscription).sdl();
assert_eq!(sdl.matches("scalar Bytes\n").count(), 1, "{sdl}");
assert_eq!(sdl.matches("scalar String").count(), 0, "{sdl}");
}
struct SharedOnly;
#[Object(name = "Query")]
impl SharedOnly {
async fn value(&self) -> SharedBytes {
SharedBytes::new()
}
}
struct CompactOnly;
#[Object(name = "Query")]
impl CompactOnly {
async fn value(&self) -> CompactBytes {
CompactBytes::new()
}
}
#[test]
fn the_bytes_claimants_are_indistinguishable_in_a_schema() {
let shared = Schema::new(SharedOnly, EmptyMutation, EmptySubscription).sdl();
let compact = Schema::new(CompactOnly, EmptyMutation, EmptySubscription).sdl();
assert_eq!(shared, compact);
}
}