Configurando o GraphQL
Agora que temos contexto de como funciona o Actix, será muito mais simples criar um novo serviço, assim o objetivo neste capítulo será focar na parte GraphQL deste novo serviço. Antes vamos entender um pouco o que é GraphQL e porque vamos utlizar essa tecnologia.
GraphQL
GraphQL é uma tecnologia desenvolvida pelo Facebook que consiste em uma linguagem de queries para APIs e um runtime para executar estas queries. Além disso, GraphQL provê ferramentas para entender e descrever os dados das APIs, da ao cliente o poder de decidir quais dados quer consumir e facilita e evolução de APIs. De forma resumida, são quatro etapas que envolvem o GraphQL:
- Descrever seus dados via tipos:
type Project {
name: String
tagline: String
contributors: [User]
}
- Receber um request com os dados a serem consumidos:
{
project(name: "GraphQL") {
tagline
}
}
- Realizar as consultas a todos os serviços/APIs necessários
- Responder exatamente o que o cliente pediu.
{
"data": {
"project": {
"tagline": "A query language for APIs"
}
}
}
Assim, o motivo de escolhermos GraphQL como tecnologia para este serviço é a necessidade de consultar diversas fontes para um mesmo request, como mais de uma API e caching.
Queries Básicas
Vamos começar com o básico, fazer o sistema responder 404 NOT_FOUND
para rotas diversas e depois iniciar com uma query que responderá um simples pong
quando chamarmos a query ping
. Para isso, nosso primeiro passo é criar o serviço com cargo new recommendations-gql --bin
, e adicionar as dependências básicas ao nosso Cargo.toml:
[dependencies]
actix-web = "2.0.0"
actix-rt = "1.0.0"
juniper = "0.14.2"
serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.44"
serde_derive = "1.0.104"
Agora precisamos adicionar o caso de status code 404
. Para esse caso vamos utilizar outro recurso que não utilizamos antes que é o default_service
. Ele nos permite responder um valor default para qualquer rota não encontrada, neste caso escrevemos 404
:
// main.rs
use actix_web::{web, App, HttpServer};
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
App::new()
.default_service(web::to(|| async { "404" }))
})
.bind("127.0.0.1:4000")?
.run()
.await
}
Pronto! ao acessar localhost:4000
recebemos um 404
. Próximo passo é adicionarmos a query de ping
.
Ping em GraphQL
Primeiro passo para o ping
seria pensarmos o schema correspondente na estrutura GraphQL. Assim, nosso objetivo é realizar uma query nomeada ping
que retorne uma string contento "pong"
. Para isso sabemos que precisaremos que uma função ping
que retornar um Result<String, Error>
que contém pong
, algo como:
Mas esta função precisa estar dentro de um contexto Query
do graphql e para isso criamos uma struct chamada de QueryRoot
, que corresponde a raiz das queries, e aplicamos a macro #[juniper::object]
que transforma essa implementação de queries, impl QueryRoot
, em um objeto graphql do tipo Query
. Note que o tipo de retorno é um FieldResult
, que corresponde a um tipo Result
que já abstraiu o tipo do Error
para um erro que o GraphQL possa entender.
Segundo passo seria declarar um tipo Schema
que poderá ser utilizado pelos handlers do Graphql para validar as queries, as mutations e os tipos. Esse Schema
é composto de duas partes, a QueryRoot
e a MutationRoot
, que são designadas a um nó contendo os schemas chamado RootNode
, pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;
. Como ainda não temos nenhuma mutation, nosso MutationRoot
é bastante simples:
Queries e Mutations
O objetivo de uma query é "perguntar" para o sistema algum conjunto de infotmações, enquanto o objetivo da mutation é "mutar" alguma informação que o sistema possui, mas suas declarações são bastante parecidas com as das queries.
Por último precisamos de uma função que retorne o schema que criamos, que chamaremos de create_schema
:
Com isso podemos criar o módulo schema
em schema.rs
. Próximo passo é disponibilizar este Schema
para a aplicação, podemos fazer isso utilizando a função data
de actix_web::App
:
#[macro_use]
extern crate juniper;
extern crate serde_json;
use actix_web::{web, App, HttpServer};
mod schemas;
use crate::schemas::{create_schema, Schema};
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let schema: std::sync::Arc<Schema> = std::sync::Arc::new(create_schema());
HttpServer::new(move || {
App::new()
.data(schema.clone())
.default_service(web::to(|| async { "404" }))
})
.bind("127.0.0.1:4000")?
.run()
.await
}
Estamos utilizando a definição let schema: std::sync::Arc<Schema> =
para fazer um vínculo da variável schema
ao contexto de main
, note, também, que seu tipo precis ser std::sync::Arc
, pois todas as threads do graphql estarão acessado esse Schema
. O próximo passo é definirmos as rotas dos handlers
para o GraphQL, fazemos isso no módulo handlers
e exportamos estas infos pela função routes
:
#[macro_use]
extern crate juniper;
extern crate serde_json;
use actix_web::{web, App, HttpServer};
mod handlers;
mod schemas;
use crate::handlers::routes;
use crate::schemas::{create_schema, Schema};
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let schema: std::sync::Arc<Schema> = std::sync::Arc::new(create_schema());
HttpServer::new(move || {
App::new()
.data(schema.clone())
.configure(routes)
.default_service(web::to(|| async { "404" }))
})
.bind("127.0.0.1:4000")?
.run()
.await
}
Assim, a função routes
do módulo handlers
possuirá a seguinte estrutura:
Nossa configuração possuirá duas rotas /graphql
, que é a rota na qual fazemos um post com nossa query, e /graphiql
, que será uma rota que nos exibirá uma página web interativa da nossa aplicação:
Note que a direita na rota graphiql
existe uma aba chamada de Documentation Explorer
, ela nos permite saber as queries e as mutations disponíveis, assim como seus tipos de entrada e tipos de retorno.
Agora, o handler graphiql
é bastante simples, sua única função é encondar em HTML
o handler graphql
:
Com isso, podemos finalmente entender o que o handler graphql
faz:
O handler graphql
recebe como argumento dois campos 1. schema
através do tipo actix web::Data<Arc<Schema>>
e o request data
do tipo web::Json<GraphQLRequest>
. A magia acontece na linha data.execute(&schema, &())
, na qual o GraphQL executa nosso request, data
, com base no schema
. Depois disso, encodamos o resultado para Json e respondemos como um Result<HttpResponse, Error>
oriundo de Ok(HttpResponse::Ok().content_type("application/json").body(res))
. Se você executar este código será possível fazer a query ping
em localhost:4000/graphql:
Testando o endpoint
Como na parte anterior do livro falamos de como criar testes de integração no diretório tests/
, agora vamos partir para outra estratégia, que é criar testes de integração dentro do src/
, pois isto nos permite tirar proveito da flag #[cfg(test)]
. Para fazermos isso, precisamos criar um módulo test
, anotoado com a flag #[cfg(test)]
em main.rs
:
Depois disso é preciso criar o arquivo src/test/mod.rs
, que declarará o nome dos submodulos de teste, neste caso um simples pub mod ping;
. Para testarmos o ping
precisamos criar o módulo que declaramos em src/test/ping.rs
:
A estrutura do teste é praticamente igual a estrutura que estavamos utilizando anteriormente, as únicas diferenças são let schema: std::sync::Arc<Schema> = std::sync::Arc::new(create_schema());
, que a rota agora é /graphql
e que o payload é um json contendo um campo query
seguido de sua query "{\"query\": \"query ping { ping }\"}"
.
Agora vamos a implementação da primeira query de consulta, que chamaremos de bestPrices
.