Best Prices
Nesta query, bestPrices
, vamos fazer uma consulta a uma API externa que retorna os melhores preços para uma rota (data, origem e destino). Consultaremos a URL de bestPrices
da Latam https://bff.latam.com/ws/proxy/booking-webapp-bff/v1/public/revenue/bestprices/oneway?departure=<YYYY-mm-dd>&origin=<IATA>&destination=<IATA>&cabin=Y&country=BR&language=PT&home=pt_br&adult=1&promoCode=
, na qual departure
é a data de partida no formato ano-mes-dia
, origin
é o código IATA da cidade ou do aeroporto de origem, destination
é o código IATA da cidade ou do aeroporto de destino. Assim, nossa query deve receber 3 argumentos departure
, origin
e destination
e retornar um conjunto de melhores preços, além de lançar erros. Caso estes argumentos não estejam dentro do padrão esperado. Com isso, nosso primeiro passo será implementar a função bestPrices
que recebe os 3 argumentos e por enquanto retornará uma String
.
Implementando a função básica de bestPrices
Nosso objetivo agora é fazer nosso GraphQL responder da seguinte forma:
A query que usamos é:
query {
bestPrices(
departure: "sdf",
origin: "sdf",
destinantion: "sdfg")
ping
}
E o valor de retorno é:
{
"data": {
"bestPrices": "test",
"ping": "pong"
}
}
O resultado de uma query GraphQL como a que mostamos retorna o campo data
que é um mapa contendo os resultados das queries bestPrices
e ping
. Para resolvermos isso, podemos escrever a seguinte função:
#![allow(unused)] fn main() { // schema/mod.rs // ... #[juniper::object] impl QueryRoot { //... fn bestPrices(departure: String, origin: String, destinantion: String) -> FieldResult<String> { Ok(String::from("test")) } } // ... }
Agora podemos começar a pensar um pouco melhor na organização do nosso código. Na seção de apresentação do livro desenhamos o seguinte diagrama:
api
main
|-> boundaries
|-> web
|-> db
|-> message
|-> controllers/resolvers
|-> adapters
|-> core
|-> business
|-> compute
|-> models/schemas
Com este esquema em mente, vamos ordenar como nossos arquivos ficarão organizados para um projeto GraphQL e exemplificar para onde cada conjunto já existente será movido:
api
main
|-> boundaries
|-> web
|-> db
|-> resolvers
|-> graphql
|-> queries
|-> mutations
|-> internal
|-> adapters
|-> core
|-> business
|-> compute
|-> schemas
|-> graphql
|-> model
|-> db
|-> web
|-> errors
Com esta definição em mente vamos alocar o projeto que contém as rotas e os handlers GraphQL em boundaries/web/handlers.rs
, pois este aquivo é responsável pela interface web do projeto. Qualquer módulo de comunicaçnao com banco ficaria em boudnaries/db/
, assim como de Kafka ficaria em boudnaries/kafka
ou boudnaries/messages
. Nosso arquivo schema/mod.rs
possui as configurações de resolvers, assim não faz sentido que esteja em schema/
, e moveremos ele para resolvers/graphql/
, poderiamos separar em queries
e mutations
, mas como nosso projeto somente conterá 2 queries, não precisamos nos preocupar em extrair para pastas diferentes. Além disso, chamei o que tipicamente é considerado um controller
de resolver/internal
, por simplicidade, caso prefira chamar de controller esta adequado também. Na pasta schemas
vamos adicionar todos os schemas de referência ao GraphQL em schemas/graphql
, assim como os de comunicação com o banco em schemas/model/db
e de interface web em schemas/model/web
. Já os erros de que usaremos para comunicar problemas estarão em schemas/errors
. Caso você fique com dúvidas de como ficou a organizacão do código, ela está disponível no commit https://github.com/web-dev-rust/airline-tickets/commit/c33a78cffbd74be49727c744623dcd1e10902cd4.
Validando argumentos
Com a função que implementamos para bestPrices
precisamos agora implementar os erros correspondentes, para isso criaremos o módulo schemas/errors.rs
e lá implementaremos os erros Graphql. O primeiro erro que vamos implementar é o erro do formato de origin
e destination
, pois IATAs devem ser 3 letras. Chamaremos esse conjunto de erros de InputError
e o erro correspondente ao IATA de IataFormatError
:
#![allow(unused)] fn main() { use juniper::{FieldError, IntoFieldError}; pub enum InputError { IataFormatError, } impl IntoFieldError for InputError { fn into_field_error(self) -> FieldError { match self { InputError::IataFormatError => FieldError::new( "The IATA format for origin and destinantion consists of 3 letter", graphql_value!({ "type": "IATA FORMAT ERROR" }), ), } } } }
Agora para usarmos esse erro precisamos modificar a função bestPrices
em resolvers/graphql.rs
para usar o tipo InputError
:
#![allow(unused)] fn main() { use crate::schema::errors::InputError; // ... #[juniper::object] impl QueryRoot { fn ping() -> FieldResult<String> { Ok(String::from("pong")) } fn bestPrices( departure: String, origin: String, destinantion: String, ) -> Result<String, InputError> { if origin.len() != 3 || !origin.chars().all(char::is_alphabetic) { return Err(InputError::IataFormatError); } Ok(String::from("test")) } } //... }
Se formos em localhost:4000/graphql
e enviarmos {bestPrices(departure: "IAT", origin: "IATA", destinantion: "sdfg")}
(origin
com 4 letras) receberemos o campo error
com o campo InputError::IataFormatError
:
{
"data": null,
"errors": [
{
"message": "The IATA format for origin and destinantion consists of 3 letter",
"locations": [
{
"line": 1,
"column": 2
}
],
"path": [
"bestPrices"
],
"extensions": {
"type": "IATA FORMAT ERROR"
}
}
]
}
O campo destination
também é um IATA e precisamos aplicar a lógica iata.len() != 3 || !iata.chars().all(char::is_alphabetic)
a ambos os campos, assim vamos criar um módulo de lógica que controlar quando esse erro deve ser lançado. O módulo será core/error.rs
:
use crate::schema::errors::InputError;
pub fn iata_format(origin: &str, destination: &str) -> Result<(), InputError> {
if origin.len() != 3
|| !origin.chars().all(char::is_alphabetic)
|| destination.len() != 3
|| !destination.chars().all(char::is_alphabetic)
{
Err(InputError::IataFormatError)
} else {
Ok(())
}
}
Os testes para esta função são:
#![allow(unused)] fn main() { #[cfg(test)] mod iata { use super::iata_format; use crate::schema::errors::InputError; #[test] fn len_should_be_3() { assert_eq!( iata_format("IATA", "IAT").err().unwrap(), InputError::IataFormatError ); assert_eq!( iata_format("IAT", "IATA").err().unwrap(), InputError::IataFormatError ); } #[test] fn only_letters() { assert_eq!( iata_format("IAT", "I4T").err().unwrap(), InputError::IataFormatError ); assert_eq!( iata_format("I&T", "IAT").err().unwrap(), InputError::IataFormatError ); } } }
Nesta função validamos que o formato IATA é respeitado tanto para origin
quanto para destination
, somente 3 letras. Caso alguma das verificaçnoes falhe, lançamos o erro InputError::IataFormatError
. Depois disso, aplicamos a função iata_format
em nosso resolver através de um match, que retorna o erro ou executa alguma função interna:
#![allow(unused)] fn main() { use crate::core::error; // ... #[juniper::object] impl QueryRoot { fn bestPrices( departure: String, origin: String, destination: String, ) -> Result<String, InputError> { match error::iata_format(&origin, &destination) { Err(e) => Err(e), Ok(_) => Ok(String::from("test")), } } } }
Próximo passo é determinar se departure
é uma data e seu valor é superior ao dia de hoje.
Validando datas
Para trabalharmos com datas precisamos incluir a crate chrono = "0.4.11"
no campo [dependencies]
do Cargo.toml. A primeira coisa que vamos verificar é se o formato da data está correto. Podemos fazer isso com a seguinte função:
#![allow(unused)] fn main() { use chrono::naive::NaiveDate; // ... pub fn departure_date_format(date: &str) -> Result<(), InputError> { let departure = NaiveDate::parse_from_str(date, "%Y-%m-%d"); match departure { Err(_) => Err(InputError::DateFormatError), Ok(d) => Ok(()), } } }
Com parse_from_str
verificamos se o formato da string departure
esta correto de acordo com o formatador que passamos "%Y-%m-%d"
. parse_from_str
nos retorna um Result
que podemos utilizar para compor o erro. Precisamos incluir um novo caso de erro, DateFormatError
em InputError
e adicionar sua cláusula no macth
. Assim, validamos isso com os testes a seguir:
#![allow(unused)] fn main() { #[cfg(test)] mod date { use super::departure_date_format; use crate::schema::errors::InputError; #[test] fn date_is_correct() { assert!(departure_date_format("3020-01-20").is_ok()); } #[test] fn date_should_be_yyyy_mm_dd() { assert_eq!( departure_date_format("2020/01/20").err().unwrap(), InputError::DateFormatError ); } } }
Próximo passo é verificar se a data de departure data é maior que a data de hoje, para isso podemos utilizar a função signed_duration_since
que nos retorna uma Duration
desde a data passada como argumento (today
). Podemos comparar essa data extraindo o número de dias com num_days
e verificar se é maior que 0
. Novamente precisamos adicionar um nove erro InvalidDateError
em InputError
.
#![allow(unused)] fn main() { use chrono::{naive::NaiveDate, offset::Utc}; // ... pub fn departure_date_format(date: &str) -> Result<(), InputError> { let departure = NaiveDate::parse_from_str(date, "%Y-%m-%d"); match departure { Err(_) => Err(InputError::DateFormatError), Ok(d) => { let today = Utc::today(); if d.signed_duration_since(today.naive_utc()).num_days() > 0 { Ok(()) } else { Err(InputError::InvalidDateError) } } } } }
E o teste para esse novo caso pode ser:
#![allow(unused)] fn main() { #[test] fn date_should_be_greater_than_today() { assert_eq!( departure_date_format("2019-01-20").err().unwrap(), InputError::InvalidDateError ); } }
O módulo schema/error.rs
fica da seguinte forma:
#![allow(unused)] fn main() { use juniper::{FieldError, IntoFieldError}; #[derive(Debug, Clone, PartialEq)] pub enum InputError { IataFormatError, DateFormatError, InvalidDateError, } impl IntoFieldError for InputError { fn into_field_error(self) -> FieldError { match self { InputError::IataFormatError => FieldError::new( "The IATA format for origin and destinantion consists of 3 letter", graphql_value!({ "type": "IATA FORMAT ERROR" }), ), InputError::DateFormatError => FieldError::new( "departure date should be formated yyyy-mm-dd", graphql_value!({ "type": "DATE FORMAT ERROR" }), ), InputError::InvalidDateError => FieldError::new( "Date should be greater than today", graphql_value!({ "type": "INVALID DATE ERROR" }), ), } } } }
Agora podemos adicionar este novo grupo de erros ao nosso resolver com:
fn bestPrices(
departure: String,
origin: String,
destination: String,
) -> Result<String, InputError> {
match (
error::iata_format(&origin, &destination),
error::departure_date_format(&departure),
) {
(Err(e), Err(e2)) => Err(e),
(Err(e), _) => Err(e),
(_, Err(e)) => Err(e),
_ => Ok(String::from("test")),
}
}
Próximo passo é responder as informações de bestPrices
em vez de Ok(String::from("test"))
.
Respondendo informacões de bestPrices
Para este caso devemos utilizar um cliente HTTP, que usualmente são assíncronos em Rust, porém a crate que estamos utilizando para GraphQL ainda não tem um suporte muito sólido para async/await
, e por isso preferi utilizar a crate de cliente HTTP reqwest
com o módulo reqwest::blocking
, mesmo que actix possua seu próprio módulo de cliente actix_web::client
.
Exemplo de client com
actix_web::client
use actix_web::client::Client; #[actix_rt::main] async fn main() { let mut client = Client::default(); // Cria `request builder` e envia com `send` let response = client.get("http://www.rust-lang.org") .header("User-Agent", "Actix-web") .send().await; // <-Envia o request println!("Response: {:?}", response); }
Conhecendo o endpoint
Consultando o endpoint de best_prices
para data "2020-07-21"
, para origem POA
e para destino GRU
https://bff.latam.com/ws/proxy/booking-webapp-bff/v1/public/revenue/bestprices/oneway?departure={data}&origin={iata}&destination={iata}&cabin=Y&country=BR&language=PT&home=pt_br&adult=1&promoCode= recebemos o seguinte campos relevantes no Json:
{
"itinerary":{
"date":"2020-07-21",
"originDestinations":[
{
"duration":95,
"departure":{
"airport":"POA",
"city":"POA",
"country":"BR",
"timestamp":"2020-07-21T11:10-03:00"
},
"arrival":{
"airport":"GRU",
"city":"SAO",
"country":"BR",
"timestamp":"2020-07-21T12:45-03:00"
}
}
]
},
"bestPrices":[
{
"date":"2020-07-18",
"available":true,
"price":{
"amount":117.03, "currency":"BRL"
}
},
{
"date":"2020-07-19",
"available":true,
"price":{
"amount":117.03, "currency":"BRL"
}
},
{
"date":"2020-07-20",
"available":true,
"price":{
"amount":117.03, "currency":"BRL"
}
},
{
"date":"2020-07-21",
"available":true,
"price":{
"amount":117.03, "currency":"BRL"
}
},
{
"date":"2020-07-22",
"available":true,
"price":{
"amount":117.03, "currency":"BRL"
}
},
{
"date":"2020-07-23",
"available":true,
"price":{
"amount":117.03, "currency":"BRL"
}
},
{
"date":"2020-07-24",
"available":true,
"price":{
"amount":117.03, "currency":"BRL"
}
}
]
}
Com isso, precisamos modelar a resposta de cada campo para uma estrutura de dados correspondete localizadas em schema/model/web.rs
, chamaremos esta estrutura de BestPrices
:
#![allow(unused)] fn main() { use juniper::GraphQLObject; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct BestPrices { itinerary: Itinerary, best_prices: Vec<BestPrice>, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct Itinerary { date: String, origin_destinations: Vec<OriginDestination>, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct OriginDestination { duration: i32, departure: AirportInfo, arrival: AirportInfo, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct AirportInfo { airport: String, city: String, country: String, timestamp: String, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct BestPrice { date: String, available: bool, price: Option<Price>, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct Price { amount: f64, currency: String, } } }
Para podermos converter o json em uma estrutura de dados Rust vamos precisar utilizar a crate serde
e adicionar #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
em todas as struct anteriores. Além disso, utilizaremos #[serde(rename_all = "camelCase")]
para transformar campos snake_case
em camelCase
, como origin_destinations
, e a macro GraphQLObject
para indicar que estas structs correspondem a um objeto GraphQL. Agora podemos fazer um request para este endpoint, para isso vamos criar o módulo boundaries/http_out
e utilizar o reqwest
para fazer um GET
no endpoint:
#![allow(unused)] fn main() { use reqwest::{blocking::Response, Result}; pub fn best_prices(departure: String, origin: String, destination: String) -> Result<Response> { let url = format!("https://bff.latam.com/ws/proxy/booking-webapp-bff/v1/public/revenue/bestprices/oneway?departure={}&origin={}&destination={}&cabin=Y&country=BR&language=PT&home=pt_br&adult=1&promoCode=", departure, origin, destination); reqwest::blocking::get(&url) } }
A função best_prices
formata a url
do request adicionando os parâmetros departure, origin, destination
para utilizar a função bloqueante get
de reqwest
, reqwest::blocking::get(&url)
. O tipo de retorno é um Result<Response>
da própria crate reqwest
.
Resolvendo BestPrices
Com a função boundaries::http_out::best_prices
fazendo o request, precisamos transformar o resultado desse request em uma estrutura de dados do tipo BestPrices
que serializa e implementa GraphQLObject
. Para coordenarmos isso, criamos um módulo resolvers/internal
que vai implementar a função best_prices_info
:
#![allow(unused)] fn main() { use crate::boundaries::http_out::best_prices; use crate::schema::{errors::InputError, model::web::BestPrices}; pub fn best_prices_info( departure: String, origin: String, destination: String, ) -> Result<BestPrices, InputError> { let best_prices_text = best_prices(departure, origin, destination) .unwrap() .text() .unwrap(); let best_prices: BestPrices = serde_json::from_str(&best_prices_text).unwrap(); Ok(best_prices) } }
Note que o resultado da função boundaries::http_out::best_prices
é um reqwest::Result<reqwest::blocking::response>
, e que para utilizarmos seus dados precisamos tratar como um Result
usual, por isso aplicamos unwrap
. Além disso, queremos a informação presente no body
da resposta, que obtemos como texto utilizando a função text
, que retorna um Result
, definimos o resultado deste processo como best_prices_text
. Com best_prices_text
podemos transformar esse texto em uma estrutura BestPrices
utilizando a função serde_json::from_str
, como em let best_prices: BestPrices = serde_json::from_str(&best_prices_text).unwrap();
e retornar essa infomacão em um Ok
. O código ainda possui alguns defeitos como a grande quantidade de unwraps
e um InputError
totalmente deslocado, logo veremos como melhorar o código neste sentido. best_prices_info
ainda não está conectado a nenhuma parte do código GraphQL, assim, precisamos chamar esta função no resolver GraphQL best_prices
e mudar seu tipo de resposta para utilizar schema::model::web::BestPrices
, Result<BestPrices, InputError
.
#![allow(unused)] fn main() { use crate::core::error; use crate::resolvers::internal::best_prices_info; use crate::schema::{errors::InputError, model::web::BestPrices}; use juniper::FieldResult; use juniper::RootNode; pub struct QueryRoot; #[juniper::object] impl QueryRoot { fn ping() -> FieldResult<String> { Ok(String::from("pong")) } fn bestPrices( departure: String, origin: String, destination: String, ) -> Result<BestPrices, InputError> { match ( error::iata_format(&origin, &destination), error::departure_date_format(&departure), ) { (Err(e), Err(e2)) => Err(e), (Err(e), _) => Err(e), (_, Err(e)) => Err(e), _ => best_prices_info(departure, origin, destination), } } } // ... }
Melhorando as mensagens de erro.
O conceito de Input Error
é particularmente estranho para erros de conversão de Json com serde
ou de request com reqwest
, assim, uma possível solução é fazer um "super grupo" de erros, que vou chamar de GenericError
, e esse vai possuír um enum
chamado InternalError
:
#[derive(Debug, Clone, PartialEq)]
pub enum GenericError {
InputError(InputError),
InternalError(InternalError),
}
#[derive(Debug, Clone, PartialEq)]
pub enum InputError {
IataFormatError,
DateFormatError,
InvalidDateError,
}
#[derive(Debug, Clone, PartialEq)]
pub enum InternalError {
RequestFailedError,
ResponseParseError,
}
A próxima mudança que podemos fazer é alterar todos os Result<BestPrices, InputError>
para Result<BestPrices, GenericError>
, o que causa uma grande quantidade de alarmes em nosso código, mas em vez de arrumarmos cada um dos alarmes e termos mais dor de cabeça, vamos implementar a trait From
para dois erros do tipo GenericError::InternalError
, reqwest::Error
e serde_json::Error
, pois estes são os erros que queremos lançar na função esolvers::internal::best_prices_info;
. O primeiro erro, reqwest::Error
, tem como objetivo retirar o unwrap
e o expect
da chamada best_prices(departure, origin, destination).unwrap().text().expect(...);
, obtendo como resultado best_prices(departure, origin, destination)?.text()?;
, que nos ajuda a aproveitar o tipo de retorno GenericError
. O mesmo faremos para transformar a chamada de serde_json::from_str(&best_prices_text).unwrap();
em serde_json::from_str(&best_prices_text)?;
aplicando a trait From
no tipo de erro serde_json::Error
:
#![allow(unused)] fn main() { impl From<reqwest::Error> for GenericError { fn from(e: reqwest::Error) -> Self { GenericError::InternalError(InternalError::RequestFailedError) } } impl From<serde_json::Error> for GenericError { fn from(e: serde_json::Error) -> Self { GenericError::InternalError(InternalError::ResponseParseError) } } }
O efeito disso é que o arquivo resolver/internal.rs
se torna muito mais simples:
#![allow(unused)] fn main() { use crate::boundaries::http_out::best_prices; use crate::schema::{errors::GenericError, model::web::BestPrices}; pub fn best_prices_info( departure: String, origin: String, destination: String, ) -> Result<BestPrices, GenericError> { let best_prices_text = best_prices(departure, origin, destination)?.text()?; let best_prices: BestPrices = serde_json::from_str(&best_prices_text)?; Ok(best_prices) } }
Com isso, podemos notar que a vida da query bestPrices
ficou muito mais simples, pois o match
se torna desnecessário, já que as funções error::iata_format
e error::departure_date_format
retornam um tipo Result<(),InputError>
, que é facilmente convertido para um Result<(),GenericError>
, nos permitindo utilizar a sintaxe try
para elas na query bestPrices
, error::iata_format(&origin, &destination)?;
e error::departure_date_format(&departure)?;
. O arquivo core/error.rs
passa a ter a seguinte aparência:
#![allow(unused)] fn main() { use crate::schema::errors::{GenericError, InputError}; use chrono::{naive::NaiveDate, offset::Utc}; pub fn iata_format(origin: &str, destination: &str) -> Result<(), GenericError> { if origin.len() != 3 || !origin.chars().all(char::is_alphabetic) || destination.len() != 3 || !destination.chars().all(char::is_alphabetic) { Err(GenericError::InputError(InputError::IataFormatError)) } else { Ok(()) } } pub fn departure_date_format(date: &str) -> Result<(), GenericError> { let departure = NaiveDate::parse_from_str(date, "%Y-%m-%d"); match departure { Err(_) => Err(GenericError::InputError(InputError::DateFormatError)), Ok(d) => { let today = Utc::today(); if d.signed_duration_since(today.naive_utc()).num_days() > 0 { Ok(()) } else { Err(GenericError::InputError(InputError::InvalidDateError)) } } } } #[cfg(test)] mod date { use super::departure_date_format; use crate::schema::errors::{InputError, GenericError}; #[test] fn date_is_correct() { assert!(departure_date_format("3020-01-20").is_ok()); } #[test] fn date_should_be_yyyy_mm_dd() { assert_eq!( departure_date_format("2020/01/20").err().unwrap(), GenericError::InputError(InputError::DateFormatError) ); } #[test] fn date_should_be_greater_than_today() { assert_eq!( departure_date_format("2019-01-20").err().unwrap(), GenericError::InputError(InputError::InvalidDateError) ); } } #[cfg(test)] mod iata { use super::iata_format; use crate::schema::errors::{InputError, GenericError}; #[test] fn len_should_be_3() { assert_eq!( iata_format("IATA", "IAT").err().unwrap(), GenericError::InputError(InputError::IataFormatError) ); assert_eq!( iata_format("IAT", "IATA").err().unwrap(), GenericError::InputError(InputError::IataFormatError) ); } #[test] fn only_letters() { assert_eq!( iata_format("IAT", "I4T").err().unwrap(), GenericError::InputError(InputError::IataFormatError) ); assert_eq!( iata_format("I&T", "IAT").err().unwrap(), GenericError::InputError(InputError::IataFormatError) ); } } }
Todas essas mudanças nos permitem ainda simplificar a query bestPrices
para utilizar os operadores try
:
#![allow(unused)] fn main() { #[juniper::object] impl QueryRoot { fn ping() -> FieldResult<String> { Ok(String::from("pong")) } fn bestPrices( departure: String, origin: String, destination: String, ) -> Result<BestPrices, GenericError> { error::iata_format(&origin, &destination)?; error::departure_date_format(&departure)?; let best_price = best_prices_info(departure, origin, destination)?; Ok(best_price) } } }
Note que ainda há um problema envolvendo a trait IntoFieldError
que não está implementada para o enum GenericError
. Fazemos isso refatorando a implementação da trait para o enum InputError
, reaproveitando todos seus campos:
#![allow(unused)] fn main() { impl IntoFieldError for GenericError { fn into_field_error(self) -> FieldError { match self { GenericError::InputError(InputError::IataFormatError) => FieldError::new( "The IATA format for origin and destinantion consists of 3 letter", graphql_value!({ "type": "IATA FORMAT ERROR" }), ), GenericError::InputError(InputError::DateFormatError) => FieldError::new( "departure date should be formated yyyy-mm-dd", graphql_value!({ "type": "DATE FORMAT ERROR" }), ), GenericError::InputError(InputError::InvalidDateError) => FieldError::new( "Date should be greater than today", graphql_value!({ "type": "INVALID DATE ERROR" }), ), GenericError::InternalError(InternalError::RequestFailedError) => FieldError::new( "Could not complete properly request to the backend", graphql_value!({ "type": "REQUEST FAILED ERROR" }), ), GenericError::InternalError(InternalError::ResponseParseError) => FieldError::new( "Could not parse response from backend", graphql_value!({ "type": "RESPONSE PARSE ERROR" }), ), } } } }
Agora que evoluímos os erros da nossa API, podemos fazer a query de recommendations.