async-graphql icon indicating copy to clipboard operation
async-graphql copied to clipboard

Add support for semantic nullability with `@semanticNonNull`

Open XiNiHa opened this issue 1 year ago • 1 comments

Description of the feature

Recently, many GraphQL frameworks and libraries are adopting semantic nullability support with @semanticNonNull, namely Relay and Apollo Kotlin at the client, Caliban and Grats at the server. There are also tools like graphql-sock and graphql-toe to help existing libraries work with semantic nullability. While it's already possible to mimic the functionality in async-graphql by manually injecting the directives, since it's a huge pain putting them everywhere and making everything optional, adding macro support for the feature in async-graphql would be very helpful.

Code example (if possible)

TODO: how would we make the feature opt-in? A Cargo feature? An attribute on macros?

struct MyObject;

#[Object]
impl MyObject {
    async fn value(&self) -> i32 {
        42
    }

    async fn optional_value(&self) -> Option<i32> {
        Some(42)
    }

    async fn fallible_value(&self, input: String) -> Result<i32> {
        Ok(input
            .parse()
            .map_err(|err: ParseIntError| err.extend_with(|_, e| e.set("code", 400)))?)
    }

    async fn fallible_optional_value(&self, input: Option<String>) -> Result<Option<i32>> {
        match input {
            Some(input) => Ok(input
                .parse()
                .map_err(|err: ParseIntError| err.extend_with(|_, e| e.set("code", 400)))?),
            None => Ok(None)
        }
    }
}

This should result in the following schema:

type MyObject {
    # infallible values as strict non-null (no change)
    value: Int!
    # optional values as nullable (no change)
    optionalValue: Int
    # fallible values as semantic non-null, marked with a directive (new!)
    fallibleValue: Int @semanticNonNull
    # fallible optional value as nullable (no change)
    fallibleOptionalValue: Int
}

In addition, Result<Vec<T>>, Vec<Result<T>>, and Result<Vec<Result<T>>> should be handled using the levels argument in the directive.

Result<Vec<T>> => [T!] @semanticNonNull(levels: [0])
Vec<Result<T>> => [T]! @semanticNonNull(levels: [1])
Result<Vec<Result<T>>> => [T] @semanticNonNull(levels: [0, 1])

XiNiHa avatar Sep 30 '24 09:09 XiNiHa