From 17af7c5ac526cd2d6f09b2618d69cc5a4b0a12f8 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 21 Jun 2022 15:25:06 +0200 Subject: [PATCH] Impl new machinery for array --- juniper/src/types/array.rs | 277 ++++++++++++++++++++++++++++++------- juniper/src/types/mod.rs | 2 +- 2 files changed, 231 insertions(+), 48 deletions(-) diff --git a/juniper/src/types/array.rs b/juniper/src/types/array.rs index f730c39d..5e7e48ef 100644 --- a/juniper/src/types/array.rs +++ b/juniper/src/types/array.rs @@ -1,118 +1,301 @@ //! GraphQL implementation for [array]. //! -//! [array]: primitive@std::array +//! [array]: prim@array + +use std::{ + mem::{self, MaybeUninit}, + ptr, +}; use crate::{ + behavior, executor::{ExecutionResult, Executor, Registry}, graphql, reflect, resolve, schema::meta::MetaType, - BoxFuture, Selection, + BoxFuture, FieldError, IntoFieldError, Selection, }; use super::iter; -/* -impl resolve::Type for [T; N] +impl resolve::Type for [T; N] where - T: resolve::Type, - Info: ?Sized, + T: resolve::Type, + TI: ?Sized, + BH: ?Sized, { - fn meta<'r>(registry: &mut Registry<'r, S>, info: &Info) -> MetaType<'r, S> + fn meta<'r, 'ti: 'r>(registry: &mut Registry<'r, SV>, type_info: &'ti TI) -> MetaType<'r, SV> where - S: 'r, + SV: 'r, { - registry - .build_list_type_new::(info, Some(N)) - .into_meta() + registry.wrap_list::, _>(type_info, None) } } -impl resolve::Value for [T; N] +impl resolve::Value for [T; N] where - T: resolve::Value, - Info: ?Sized, - Ctx: ?Sized, + T: resolve::Value, + TI: ?Sized, + CX: ?Sized, + BH: ?Sized, { fn resolve_value( &self, - selection_set: Option<&[Selection<'_, S>]>, - info: &Info, - executor: &Executor, - ) -> ExecutionResult { - iter::resolve_list(self.iter(), selection_set, info, executor) + selection_set: Option<&[Selection<'_, SV>]>, + type_info: &TI, + executor: &Executor, + ) -> ExecutionResult { + iter::resolve_list(self.iter(), selection_set, type_info, executor) } } -impl resolve::ValueAsync for [T; N] +impl resolve::ValueAsync for [T; N] where - T: resolve::ValueAsync + Sync, - Info: Sync + ?Sized, - Ctx: Sync + ?Sized, - S: Send + Sync, + T: resolve::ValueAsync + Sync, + TI: Sync + ?Sized, + CX: Sync + ?Sized, + SV: Send + Sync, + BH: ?Sized + 'static, // TODO: Lift `'static` bound if possible. { fn resolve_value_async<'r>( &'r self, - selection_set: Option<&'r [Selection<'_, S>]>, - info: &'r Info, - executor: &'r Executor, - ) -> BoxFuture<'r, ExecutionResult> { + selection_set: Option<&'r [Selection<'_, SV>]>, + type_info: &'r TI, + executor: &'r Executor, + ) -> BoxFuture<'r, ExecutionResult> { Box::pin(iter::resolve_list_async( self.iter(), selection_set, - info, + type_info, executor, )) } } -impl resolve::ToInputValue for [T; N] +impl resolve::ToInputValue for [T; N] where - T: resolve::ToInputValue, + T: resolve::ToInputValue, + BH: ?Sized, { - fn to_input_value(&self) -> graphql::InputValue { + fn to_input_value(&self) -> graphql::InputValue { graphql::InputValue::list(self.iter().map(T::to_input_value)) } } -/* -impl<'i, T, Info, S, const N: usize> graphql::InputType<'i, Info, S> for [T; N] +impl<'i, T, SV, BH, const N: usize> resolve::InputValue<'i, SV, BH> for [T; N] where - T: graphql::InputType<'i, Info, S>, - Info: ?Sized, + T: resolve::InputValue<'i, SV, BH>, + SV: 'i, + BH: ?Sized, +{ + type Error = TryFromInputValueError; + + fn try_from_input_value(v: &'i graphql::InputValue) -> Result { + struct PartiallyInitializedArray { + arr: [MaybeUninit; N], + init_len: usize, + no_drop: bool, + } + + impl Drop for PartiallyInitializedArray { + fn drop(&mut self) { + if self.no_drop { + return; + } + // Dropping a `MaybeUninit` does nothing, thus we need to drop + // the initialized elements manually, otherwise we may introduce + // a memory/resource leak if `T: Drop`. + for elem in &mut self.arr[0..self.init_len] { + // SAFETY: This is safe, because `self.init_len` represents + // the number of the initialized elements exactly. + unsafe { + ptr::drop_in_place(elem.as_mut_ptr()); + } + } + } + } + + match v { + graphql::InputValue::List(ls) => { + if ls.len() != N { + return Err(TryFromInputValueError::WrongCount { + actual: ls.len(), + expected: N, + }); + } + if N == 0 { + // TODO: Use `mem::transmute` instead of + // `mem::transmute_copy` below, once it's allowed + // for const generics: + // https://github.com/rust-lang/rust/issues/61956 + // SAFETY: `mem::transmute_copy` is safe here, because we + // check `N` to be `0`. It's no-op, actually. + return Ok(unsafe { mem::transmute_copy::<[T; 0], Self>(&[]) }); + } + + // SAFETY: The reason we're using a wrapper struct implementing + // `Drop` here is to be panic safe: + // `T: resolve::InputValue` implementation is not + // controlled by us, so calling + // `T::try_from_input_value(&i.item)` below may cause a + // panic when our array is initialized only partially. + // In such situation we need to drop already initialized + // values to avoid possible memory/resource leaks if + // `T: Drop`. + let mut out = PartiallyInitializedArray:: { + // SAFETY: The `.assume_init()` here is safe, because the + // type we are claiming to have initialized here is + // a bunch of `MaybeUninit`s, which do not require + // any initialization. + arr: unsafe { MaybeUninit::uninit().assume_init() }, + init_len: 0, + no_drop: false, + }; + + let mut items = ls.iter().map(|i| T::try_from_input_value(&i.item)); + for elem in &mut out.arr[..] { + if let Some(i) = items + .next() + .transpose() + .map_err(TryFromInputValueError::Item)? + { + *elem = MaybeUninit::new(i); + out.init_len += 1; + } + } + + // Do not drop collected `items`, because we're going to return + // them. + out.no_drop = true; + + // TODO: Use `mem::transmute` instead of `mem::transmute_copy` + // below, once it's allowed for const generics: + // https://github.com/rust-lang/rust/issues/61956 + // SAFETY: `mem::transmute_copy` is safe here, because we have + // exactly `N` initialized `items`. + // Also, despite `mem::transmute_copy` copies the value, + // we won't have a double-free when `T: Drop` here, + // because original array elements are `MaybeUninit`, so + // do nothing on `Drop`. + Ok(unsafe { mem::transmute_copy::<_, Self>(&out.arr) }) + } + // See "Input Coercion" on List types: + // https://spec.graphql.org/October2021#sec-Combining-List-and-Non-Null + graphql::InputValue::Null => Err(TryFromInputValueError::IsNull), + other => T::try_from_input_value(other) + .map_err(TryFromInputValueError::Item) + .and_then(|e: T| { + // TODO: Use `mem::transmute` instead of + // `mem::transmute_copy` below, once it's allowed + // for const generics: + // https://github.com/rust-lang/rust/issues/61956 + if N == 1 { + // SAFETY: `mem::transmute_copy` is safe here, because + // we check `N` to be `1`. Also, despite + // `mem::transmute_copy` copies the value, we + // won't have a double-free when `T: Drop` here, + // because original `e: T` value is wrapped into + // `mem::ManuallyDrop`, so does nothing on + // `Drop`. + Ok(unsafe { mem::transmute_copy::<_, Self>(&[mem::ManuallyDrop::new(e)]) }) + } else { + Err(TryFromInputValueError::WrongCount { + actual: 1, + expected: N, + }) + } + }), + } + } +} + +impl<'i, T, TI, SV, BH, const N: usize> graphql::InputType<'i, TI, SV, BH> for [T; N] +where + T: graphql::InputType<'i, TI, SV, BH>, + TI: ?Sized, + SV: 'i, + BH: ?Sized, { fn assert_input_type() { T::assert_input_type() } } -*/ -impl graphql::OutputType for [T; N] +impl graphql::OutputType for [T; N] where - T: graphql::OutputType, + T: graphql::OutputType, + TI: ?Sized, + CX: ?Sized, + BH: ?Sized, + Self: resolve::ValueAsync, { fn assert_output_type() { T::assert_output_type() } } -impl reflect::BaseType for [T; N] +impl reflect::BaseType for [T; N] where - T: reflect::BaseType, + T: reflect::BaseType, + BH: ?Sized, { const NAME: reflect::Type = T::NAME; } -impl reflect::BaseSubTypes for [T; N] +impl reflect::BaseSubTypes for [T; N] where - T: reflect::BaseSubTypes, + T: reflect::BaseSubTypes, + BH: ?Sized, { const NAMES: reflect::Types = T::NAMES; } -impl reflect::WrappedType for [T; N] +impl reflect::WrappedType for [T; N] where - T: reflect::WrappedType, + T: reflect::WrappedType, + BH: ?Sized, { const VALUE: reflect::WrappedValue = reflect::wrap::list(T::VALUE); } -*/ + +/// Possible errors of converting a [`graphql::InputValue`] into an exact-size +/// [array](prim@array). +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TryFromInputValueError { + /// [`graphql::InputValue`] cannot be [`Null`]. + /// + /// See ["Combining List and Non-Null" section of spec][0]. + /// + /// [`Null`]: [`InputValue::Null`] + /// [0]: https://spec.graphql.org/October2021#sec-Combining-List-and-Non-Null + IsNull, + + /// Wrong count of items. + WrongCount { + /// Actual count of items. + actual: usize, + + /// Expected count of items ([array](prim@array) size). + expected: usize, + }, + + /// Error of converting a [`graphql::InputValue::List`]'s item. + Item(E), +} + +impl IntoFieldError for TryFromInputValueError +where + E: IntoFieldError, +{ + fn into_field_error(self) -> FieldError { + const ERROR_PREFIX: &str = "Failed to convert into exact-size array"; + match self { + Self::IsNull => format!("{}: Value cannot be `null`", ERROR_PREFIX).into(), + Self::WrongCount { actual, expected } => format!( + "{}: wrong elements count: {} instead of {}", + ERROR_PREFIX, actual, expected, + ) + .into(), + Self::Item(s) => s.into_field_error(), + } + } +} diff --git a/juniper/src/types/mod.rs b/juniper/src/types/mod.rs index 44635ccd..1fe0dfe2 100644 --- a/juniper/src/types/mod.rs +++ b/juniper/src/types/mod.rs @@ -1,5 +1,5 @@ mod arc; -mod array; +pub mod array; mod r#box; pub mod iter; mod nullable;