Skip to content

Commit

Permalink
add docs on how to provide data through a graphql service in rust
Browse files Browse the repository at this point in the history
  • Loading branch information
iamvigneshwars committed Jul 4, 2024
1 parent f9bdc85 commit 0d53cc8
Showing 1 changed file with 334 additions and 0 deletions.
334 changes: 334 additions & 0 deletions docs/how-tos/rust_service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
# Rust Service

## Preface

This guide will explain how to develop a Rust service to serve data to the Graph.
We will cover:

- Setting up the HTTP/GraphQL Server in [Serving GraphQL Requests](#serving-graphql-requests)
- Creating GraphQL Objects and providing field resolvers in [Defining Objects](#defining-objects)
- Extending the graph using [Federation](#federated)
- Exporting the GraphQL schema for use in federation in [Schema Generation](#schema-generation)

## Dependencies

This guide will utilize the following dependencies:

- `axum` as the HTTP server
- `async-graphql` as graphql server library
- `async-graphql-axum` for GraphQL request handling
- `tokio` as runtime for writing asynchronous rust program

```toml
[dependencies]
async-graphql = "7.0.6"
async-graphql-axum = "7.0.6"
axum = "0.7.5"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
```

## Serving GraphQL Requests

To handle GraphQL requests, we start by creating a server with a placeholder GraphQL schema. We use the `async-graphql-axum` crate, which provides integration between `async-graphql` and `axum`, to create a `graphql_handler`.

!!! example "GraphQL Request Handler"

```rust
use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::Extension,
routing::post,
Router,
};

struct Query;

#[tokio::main]
async fn main() {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();

let app = Router::new()
.route("/graphql", post(graphql_handler))
.layer(Extension(schema));

let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}

async fn graphql_handler(
schema: Extension<Schema<Query, EmptyMutation, EmptySubscription>>,
req: GraphQLRequest,
) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
```

!!! info

`GraphQLRequest` handles incoming GraphQL requests. It wraps the raw HTTP request
data and converts it into a form that can be processed by async-graphql.

`GraphQLResponse` wraps the results of executing a GraphQL query. It ensures
that the response is correctly formatted as a valid HTTP response that Axum can
send back to the client.

## Defining Objects

!!! info

Refer to [Async-graphql Book SimpleObject](https://async-graphql.github.io/async-graphql/en/define_simple_object.html)
for a full explanation of how to implement various objects and resolvers.

An `Object` can be defined by using the `Object` macro from `async-graphql` crate. A GraphQL object must have
a resolver defined for each field in its `impl`.

We can use the `SimpleObject` macro from `async-graphql` to map all the fields of a struct to a GraphQL object.

```rust
use async_graphql::SimpleObject;
#[derive(SimpleObject)]
struct Person {
id: u32,
first_name: String,
last_name: String,
preferred_name: Optional<String>,
}
```
We can write a resolver for the `Person`. A resolver function resolvers the values for all the fields a GraphQL object.

!!! example "Defind Object and a Resolver"

```rust
use async_graphql::Object;
struct Query;

#[Object]
impl Query {
async fn get_person(&self) -> Person {
Person {
id: 1,
first_name: "foo".to_string(),
last_name: "bar".to_string(),
preferred_name: None,
}
}
}
```
Putting everything together, we should be able to run the follwing code,

!!! example "Simple GraphQL service to provide a Person information"

```rust
use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::Extension,
routing::post,
Router,
};

struct Query;

#[Object]
impl Query {
async fn get_person(&self) -> Person {
Person {
id: 1,
first_name: "foo".to_string(),
last_name: "bar".to_string(),
preferred_name: None,
}
}
}

#[derive(SimpleObject)]
struct Person {
id: u32,
first_name: String,
last_name: String,
preferred_name: Option<String>,
}

#[tokio::main]
async fn main() {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();

let app = Router::new()
.route("/graphql", post(graphql_handler))
.layer(Extension(schema));

let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}

async fn graphql_handler(
schema: Extension<Schema<Query, EmptyMutation, EmptySubscription>>,
req: GraphQLRequest,
) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
```
When we send request to the endpoint `curl -X POST -H "Content-Type: application/json" -d '{"query":"{ getPerson {id, firstName, lastName, preferredName} }"}' http://127.0.0.1:8000/graphql` we should get the following response
```json
{
"data":{
"getPerson":{
"id":1,
"firstName":"foo",
"lastName":"bar",
"preferredName":null
}
}
}
```

## Related

Most of the fields of a GraphQL object directly return the value of the field,
but sometimes the the fields are calculated or resolved by a different resolver.
In such cases, we can use the `ComplexObject` macro to write a user defined resolver.
This can be useful when we want to resolve related Objects.

!!! example "Related Objects"

```rust
#[derive(SimpleObject)]
#[graphql(complex)] // We need this macro for the ComplexObject macro to take effect
struct Person {
id: u32,
first_name: String,
last_name: String,
preferred_name: Option<String>,
}

#[derive(SimpleObject)]
struct Pet{
id: u32,
owner_id: u32,
}

#[ComplexObject]
impl Person{
async fn pet(&self) -> Pet {
Pet {
id: 10,
owner_id: 1
}
}
}
```

Now the `Person` object should have a `pet` field that resolvers a `Pet` object. When we send request to the endpoint,
`curl -X POST -H "Content-Type: application/json" -d '{"query":"{ getPerson {id, pet {id}} }"}' http://127.0.0.1:8000/graphql`
we should get the following response
```json
{
"data":{
"getPerson":{
"id":1,
"pet":{"id":10}
}
}
}
```

### Federated
!!! info

Refer to [Async-graphql Book Apollo Federation](https://async-graphql.github.io/async-graphql/en/apollo_federation.html) for a full explanation on federation using rust.

To enable federation support we can the `schema.enable_federation()` method.

!!! example "Enable federation support"

```rust
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.enable_federation()
.finish();
```

We can extends the fields of an Object from another service/subgraph using Entities and @key.

!!! info

Refer to [Introduction to Entities](https://www.apollographql.com/docs/federation/entities/) for a full explanation on entities.

!!! example "Extending fields in Pet Object from another service/subgraph"

```rust

#[derive(SimpleObject)]
struct Pet{
id: u32,
}

struct Query;

#[Object]
impl Query {
#[graphql(entity)]
async fn reference_resolver(&self, id: u32) -> Pet {
Pet { id }
}
}

```

!!! info

The `reference_resolver` function is not visible for the user consuming the API, it's a way of informing the graphql
router/gateway that this subgraph can resolve the `id` field for the `Pet` object using the `id` as the key.

Now we can extend fields in `Pet` object using the `ComplexObject` macro.

!!! example "Add fields to Pet object"

```rust
#[derive(SimpleObject)]
#[graphql(complex)] // We need this macro for the ComplexObject macro to take effect
struct Pet{
id: u32,
}

#[derive(SimpleObject)]
struct Toy{
id: u32,
name: String,
}

struct Query;

#[Object]
impl Query {
#[graphql(entity)]
async fn reference_resolver(&self, id: u32) -> Pet {
Pet { id }
}

async fn toys(&self) -> Toy {
Toy {id, name}
}
}

```
Schema Generation

We can generate schema using `schema.sdl_with_options()` method. To enable federation declaratives we should
pass `SDLExportOptions::new().federation()` as an argument to the method.

!!! example "Generate Schema with federation declaratives"

```rust
use async_graphql::SDLExportOptions;
#[tokio::main]
async fn main() {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.enable_federation()
.finish();

let schema_string = schema.sdl_with_options(SDLExportOptions::new().federation());
println!("{}", schema_string)
}

```

0 comments on commit 0d53cc8

Please sign in to comment.