async-graphql-test 1.0.0

A test framework for Rust GraphQL servers.
Documentation
use async_graphql::async_trait::async_trait;
use async_graphql::{Context, CustomDirective, Directive, ResolveFut, ServerResult, Value};

use super::{MatcherError, ValueTypeName};

/// Use `@shouldBeCloseTo` to compare floating point numbers for approximate equality.
///
/// The optional `numDigits` argument limits the number of digits to check **after** the decimal point.
/// For the default value `2`, the test criterion is `Math.abs(expected - received) < 0.005`
/// (that is, `10 ** -2 / 2`).
///
/// Intuitive equality comparisons often fail, because arithmetic on decimal (base 10) values
/// often have rounding errors in limited precision binary (base 2) representation.
/// For example, this test fails:
///
/// ```graphql
/// {
///   add(0.1, 0.2) @shouldEqual(to: 0.3)  # Fails!
/// }
/// ```
///
/// It fails because in IEEE 754 standard, `0.2 + 0.1` is actually `0.30000000000000004`.
///
/// For example, this test passes with a precision of 5 digits:
///
/// ```graphql
/// {
///   add(0.1, 0.2) @shouldBeClose(to: 0.3, numDigits: 5)
/// }
/// ```
///
/// Because floating point errors are the problem that `toBeCloseTo` solves, it does not support big integer values.
#[Directive(location = "Field", name = "shouldBeClose")]
pub fn should_be_close_to(
    to: f64,
    #[graphql(default = 2)] num_digits: i32,
) -> impl CustomDirective {
    ShouldBeCloseTo { to, num_digits }
}

struct ShouldBeCloseTo {
    to: f64,
    num_digits: i32,
}

#[async_trait]
impl CustomDirective for ShouldBeCloseTo {
    async fn resolve_field(
        &self,
        ctx: &Context<'_>,
        resolve: ResolveFut<'_>,
    ) -> ServerResult<Option<Value>> {
        match resolve.await {
            Ok(None) => Ok(None),
            Ok(Some(Value::Number(num))) => {
                let expected_diff = 10f64.powi(-self.num_digits) / 2.;
                let received_diff = (self.to - num.as_f64().unwrap()).abs();
                if received_diff < expected_diff {
                    Ok(Some(Value::Number(num)))
                } else {
                    Err(MatcherError::new(
                        ctx.item.pos,
                        format!(
                            "Expected: > {}\nExpected precision: {}\nReceived:   {num}",
                            self.to, self.num_digits
                        ),
                    ))
                }
            }
            Ok(Some(value)) => {
                let type_name = ValueTypeName(&value);
                Err(MatcherError::new(
                    ctx.item.pos,
                    format!(
                        "@shouldBeClose error: value must be numeric.\nReceived has type:  {type_name}\nReceived has value: {value}"
                    ),
                ))
            }
            Err(err) => Err(MatcherError::unexpected_error(err)),
        }
    }
}