Recommendations
Nesta query, recommendations
, vamos fazer uma consulta a mesma API anterior, mas em um endpoint diferente. Esse endpoint retorna todas as opções de voo para uma rota (data, origem e destino). Consultaremos a URL de recommendations
da Latam https://bff.latam.com/ws/proxy/booking-webapp-bff/v1/public/revenue/recommendations/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 uma lista de todas as opções de vôo, além de lançar erros. Assim como a query bestPrices
, nossa query de recommendations
vai receber os mesmos parâmetros:
BestPrices:
#![allow(unused)] fn main() { bestPrices( departure: String, origin: String, destination: String, ) }
Recommendations:
#![allow(unused)] fn main() { recommendations( departure: String, origin: String, destination: String, ) }
O tipo de retorno de recommendations
será semelhante ao tipo de bestPrices
, Result<Recommendations, GenericError>
. As validações dos tipos de entrada podem continuar iguais:
#![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) } fn recommendations( departure: String, origin: String, destination: String, ) -> Result<Recommendations, GenericError> { error::iata_format(&origin, &destination)?; error::departure_date_format(&departure)?; // ... } } }
Agora podemos começar a implementar o resolver recommendations_info
, começando por conehcer o endpoint.
Conhecendo o endpoint de recommendations
Consultando o endpoint de recommendations
para data "2020-07-21"
, para origem POA
e para destino GRU
https://bff.latam.com/ws/proxy/booking-webapp-bff/v1/public/revenue/recommendations/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 (muitos campos foram ocultos pois não vamos utilizar):
{
"data":[
{
"flights":[
{
"flightCode":"LA4596",
"arrival":{
"airportCode":"GRU",
"airportName":"Guarulhos Intl.",
"cityCode":"SAO",
"cityName":"São Paulo",
"countryCode":"BR",
"date":"2020-07-21",
"dateTime":"2020-07-21T10:50-03:00",
"overnights":0,
"time":{ "stamp":"10:50", "hours":"10", "minutes":"50" }
},
"departure":{
"airportCode":"POA",
"airportName":"Salgado Filho",
"cityCode":"POA",
"cityName":"Porto Alegre",
"countryCode":"BR",
"date":"2020-07-21",
"dateTime":"2020-07-21T09:15-03:00",
"overnights":null,
"time":{ "stamp":"09:15", "hours":"09", "minutes":"15" }
},
"stops":0,
"segments":[
{
"flightCode":"LA4596",
"flightNumber":"4596",
"waitTime":null,
"equipment":{ "name":"Airbus 321", "code":"321" },
"legs":[
],
"airline":{
"marketing":{ "code":"LA", "name":"LATAM Airlines" },
"operating":{
"code":"JJ",
"name":"LATAM Airlines Brasil"
},
"code":"LA"
},
"duration":"PT1H35M",
"departure":{
"airportCode":"POA",
"airportName":"Salgado Filho",
"cityCode":"POA",
"cityName":"Porto Alegre",
"countryCode":"BR",
"date":"2020-07-21",
"dateTime":"2020-07-21T09:15-03:00",
"overnights":null,
"time":{ "stamp":"09:15", "hours":"09", "minutes":"15" }
},
"arrival":{
"airportCode":"GRU",
"airportName":"Guarulhos Intl.",
"cityCode":"SAO",
"cityName":"São Paulo",
"countryCode":"BR",
"date":"2020-07-21",
"dateTime":"2020-07-21T10:50-03:00",
"overnights":0,
"time":{ "stamp":"10:50", "hours":"10", "minutes":"50"
}
},
"familiesMap":{
"Y-LIGHT":{
"segmentClass":"G",
"farebasis":"GLYX0N1"
},
"Y-PLUS":{
"segmentClass":"G",
"farebasis":"GLYX0N8"
},
"Y-TOP":{
"segmentClass":"G",
"farebasis":"GLYX0N9"
},
"W-PLUS":{
"segmentClass":"P",
"farebasis":"GDKX0N2"
},
"W-TOP":{
"segmentClass":"W",
"farebasis":"XJ7X0NA"
}
}
}
],
"flightDuration":"PT1H35M",
"cabins":[
{
"code":"Y",
"displayPrice":101.03,
"availabilityCount":18,
"displayPrices":{
"slice":101.03,
"wholeTrip":101.03,
"sliceDiscountCode":"",
"wholeTripDiscountCode":""
},
"fares":[
{
"code":"SL",
"category":"LIGHT",
"fareId":"250/BdC0XLNrp3H333zqIqmJJpNOO/05D8NwB5zcjHVNnkyl4GjqR/YOQrcDNWLPERAtEBLYqieN002",
"price":{
"adult":{
"amountWithoutTax":68.9,
"taxAndFees":32.13,
"total":101.03
}
},
"availabilityCount":18,
},
{
"code":"SE",
"category":"PLUS",
"fareId":"250/BdC0XLNrp3H333zqIqmJJpNOO/05D8NwB5zcjHVNnkyl4GjqR/YOQrcDNWLPERAtEBLYqieN009",
"price":{
"adult":{
"amountWithoutTax":123.9,
"taxAndFees":32.13,
"total":156.03
}
},
"availabilityCount":18
},
{
"code":"SF",
"category":"TOP",
"fareId":"250/BdC0XLNrp3H333zqIqmJJpNOO/05D8NwB5zcjHVNnkyl4GjqR/YOQrcDNWLPERAtEBLYqieN00C",
"price":{
"adult":{
"amountWithoutTax":193.9,
"taxAndFees":32.13,
"total":226.03
}
},
"availabilityCount":18
}
]
},
{
"code":"W",
"displayPrice":247.03,
"availabilityCount":9,
"displayPrices":{
"slice":247.03,
"wholeTrip":247.03,
"sliceDiscountCode":"",
"wholeTripDiscountCode":""
},
"fares":[
{
"code":"RA",
"category":"PLUS",
"fareId":"250/BdC0XLNrp3H333zqIqmJJpNOO/05D8NwB5zcjHVNnkyl4GjqR/YOQrcDNWLPERAtEBLYqieN00F",
"price":{
"adult":{
"amountWithoutTax":214.9,
"taxAndFees":32.13,
"total":247.03
}
},
"availabilityCount":9,
},
{
"code":"RY",
"category":"TOP",
"fareId":"250/BdC0XLNrp3H333zqIqmJJpNOO/05D8NwB5zcjHVNnkyl4GjqR/YOQrcDNWLPERAtEBLYqieN00K",
"price":{
"adult":{
"amountWithoutTax":673.9,
"taxAndFees":32.13,
"total":706.03
}
},
"availabilityCount":12,
}
]
}
]
},
...Mais recomendações aqui
],
"currency":"BRL",
"recommendedFlightCode":"LA4596"
}
],
"status":{
"code":200,
"message":""
}
}
Com esse Json podemos começar a modelar a resposta de recommendations, conforme fizemos com BestPrices
.
Modelando Recommendations
Para modelar recomendarions precisamos fazer uma pequena refatoração em BestPrices
, para evitarmos a que Recommendations
e BestPrices
fiquem misturados no mesmo módulo. Podemos criar um novo arquivo para cada um dos módulos, como temos feito, ou criar módulos internos dentro de schema/model/web.rs
:
#![allow(unused)] fn main() { pub mod best_prices { 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>, } // ... } }
Agora podemos criar o módulo recommendations
com:
#![allow(unused)] fn main() { pub mod recommendations { use juniper::GraphQLObject; use serde::{Deserialize, Serialize}; // ... } }
Nossa estrutura de Recommendations
contém 2 campos principais data
e status
. status
é uma struct com os campos code
do tipo i32
e message
do tipo String
. Já data
é um vetor de uma struct que contrém os campos flight
, currency
, recommendedFlightCode
, resultando em algo assim:
#![allow(unused)] fn main() { pub mod recommendations { use juniper::GraphQLObject; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct Recommendations { data: Vec<Data>, status: Status, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct Status { code: i32, message: String, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct Data { flights: Vec<Flight>, recommended_flight_code: String, currency: String, } } }
Agora precisamos implementar as struct Flight
derivada do seguinte Json:
{
"flightCode":"LA4596",
"arrival":{ ... },
"departure":{ ... },
"stops":0,
"segments":[ ... ],
"flightDuration":"PT1H35M",
"cabins":[ ... ]
}
Assim, os campos são as strings flightDuration
e flightCode
, o campo stops
do tipo i32
, as duas structs de arrival
e departure
, os vetores de segments
e de cabins
:
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct Flight { flight_code: String, arrival: Location, departure: Location, stops: i32, segments: Vec<Segment>, flight_duration: String, cabins: Vec<Cabin> } }
Note que arrival
e departure
estão com o mesmo tipo, Location
, pois possuem a mesma estrutura:
{
"airportCode":"POA",
"airportName":"Salgado Filho",
"cityCode":"POA",
"cityName":"Porto Alegre",
"countryCode":"BR",
"date":"2020-07-21",
"dateTime":"2020-07-21T09:15-03:00",
"time":{ "stamp":"09:15", "hours":"09", "minutes":"15" }
}
Que se torna a struct Location
, na qual quase todos os campos são Strings exceto por time
que será a struct Time
:
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct Location { airport_code: String, airport_name: String, city_code: String, city_name:String, country_code: String, date: String, date_time: String, time: Time, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct Time { stamp: String, hours: String, minutes: String, } }
Para implementarmos Segment
precisamos nos basear no seguinte json:
{
"flightCode":"LA4596",
"flightNumber":"4596",
"equipment":{ "name":"Airbus 321", "code":"321" },
"airline":{
"marketing":{ "code":"LA", "name":"LATAM Airlines" },
},
"duration":"PT1H35M",
"departure": Location,
"arrival": Location,
}
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct Segment { flight_code: String, flight_number: String, equipment: Info, airline: Airline, duration: String, departure: Location, arrival: Location, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct Info { name: String, code: String, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct Airline { marketing: Info, } }
Por último, podemos implementarmos Cabin
e vamos nos basear no json:
{
"code":"Y",
"displayPrice":101.03,
"availabilityCount":18,
"displayPrices":{
"slice":101.03,
"wholeTrip":101.03,
},
"fares":[...]
}
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct Cabin { code: String, display_price: f64, availability_count: i32, display_prices: DisplayPrice, fares: Vec<Fare> } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct DisplayPrice { slice: f64, whole_trip: f64, } }
Por último precisamos modelar Fare
cujo Json é:
{
"code":"SL",
"category":"LIGHT",
"fareId":"250/BdC0XLNrp3H333zqIqmJJpNOO/05D8NwB5zcjHVNnkyl4GjqR/YOQrcDNWLPERAtEBLYqieN002",
"availabilityCount":18,
"price":{
"adult":{
"amountWithoutTax":68.9,
"taxAndFees":32.13,
"total":101.03
}
}
},
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct Fare { code: String, category: String, fare_id: String, availability_count: i32, price: Price } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] pub struct Price { adult: PriceInfo } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, GraphQLObject)] #[serde(rename_all = "camelCase")] pub struct PriceInfo { amount_without_tax: f64, tax_and_fees: f64, total: f64, } }
Com a modelagem pronta, podemos finalizar a implementação da query recommendations
.
Implementando a Query recommendations
Como a estrutura de recommendations
será praticamente igual a de best_prices
, podemos nos antecipar e adicionar o resolver de recommendations
na implementação de QueryRoot
:
#![allow(unused)] fn main() { use crate::resolvers::internal::{best_prices_info, recommendations_info}; use crate::schema::{errors::{GenericError}, model::web::{best_prices::BestPrices, recommendations::Recommendations}}; // ... #[juniper::object] impl QueryRoot { // ... fn recommendations( departure: String, origin: String, destination: String, ) -> Result<Recommendations, GenericError> { error::iata_format(&origin, &destination)?; error::departure_date_format(&departure)?; let recommendations = recommendations_info(departure, origin, destination)?; Ok(recommendations) } } }
Vamos receber um erro indicando que recommendations_info
não foi implementada ainda, e, assim, podemos partir para sua implementação, que seráexatamente igual a best_prices_info
, apenas renomeando os campos de best_prices
para recommendations
:
#![allow(unused)] fn main() { use crate::boundaries::http_out::{best_prices, recommendations}; use crate::schema::{errors::GenericError, model::web::{best_prices::BestPrices, recommendations::Recommendations}}; /// ... pub fn recommendations_info( departure: String, origin: String, destination: String, ) -> Result<Recommendations, GenericError> { let recommendations_text = recommendations(departure, origin, destination)?.text()?; let recommendations: Recommendations = serde_json::from_str(&recommendations_text)?; Ok(recommendations) } }
Agora precisamos implementar a função http_out::recommendations
que também é parecida com a função http_out::best_prices
pois possui apenas a url
de request diferente:
#![allow(unused)] fn main() { // boundaries/http_out.rs use reqwest::{blocking::Response, Result}; // ... pub fn recommendations(departure: String, origin: String, destination: String) -> Result<Response> { let url = format!("https://bff.latam.com/ws/proxy/booking-webapp-bff/v1/public/revenue/recommendations/oneway?departure={}&origin={}&destination={}&cabin=Y&country=BR&language=PT&home=pt_br&adult=1&promoCode=", departure, origin, destination); reqwest::blocking::get(&url) } }
Agora podemos executar cargo run
e brincar com a interface gráfica de nosso serviço em localhost:4000/graphiql
. Um exemplo de request que podemos utilizar é:
{
bestPrices(departure: "2020-07-21",
origin: "POA",
destination: "GRU") {
bestPrices {
date
available
price {amount}
}
}
recommendations(departure: "2020-07-21",
origin: "POA",
destination: "GRU") {
data {
recommendedFlightCode
flights {
flightCode
flightDuration
arrival {
airportCode
airportName
dateTime
}
departure {
airportCode
airportName
dateTime
}
}
}
}
}
- Lembre de atualizar as datas, quando este livro foi escrito elas estavam distantes.
A resposta parcial desta query é:
{
"data": {
"bestPrices": {
"bestPrices": [...]},
"recommendations": {
"data": [
{
"recommendedFlightCode": "LA4596",
"flights": [
{
"flightCode": "LA4596",
"flightDuration": "PT1H35M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T10:50-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T09:15-03:00"
}
},
{
"flightCode": "LA4629",
"flightDuration": "PT1H35M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T22:20-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T20:45-03:00"
}
},
{
"flightCode": "LA3287",
"flightDuration": "PT1H40M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T16:55-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T15:15-03:00"
}
},
{
"flightCode": "LA3152LA3633",
"flightDuration": "PT4H55M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T13:40-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T08:45-03:00"
}
},
{
"flightCode": "LA3152LA3613",
"flightDuration": "PT8H30M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T17:15-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T08:45-03:00"
}
},
{
"flightCode": "LA3090LA3029LA3296",
"flightDuration": "PT12H45M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T22:10-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T09:25-03:00"
}
},
{
"flightCode": "LA3090LA3151LA3687",
"flightDuration": "PT12H50M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T22:15-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T09:25-03:00"
}
},
{
"flightCode": "LA3152LA3175LA3687",
"flightDuration": "PT13H30M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T22:15-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T08:45-03:00"
}
},
{
"flightCode": "LA3152LA3028LA3381",
"flightDuration": "PT13H35M",
"arrival": {
"airportCode": "GRU",
"airportName": "Guarulhos Intl.",
"dateTime": "2020-07-21T22:20-03:00"
},
"departure": {
"airportCode": "POA",
"airportName": "Salgado Filho",
"dateTime": "2020-07-21T08:45-03:00"
}
}
]
}
]
}
}
}
Um exemplo utilizando variables
pelo Postman
pode ser a foto a seguir. A mesma estratégia poderá ser utilizada via curl
.
Nosso próximo passo é implementar um sistema de caching com redis, para evitar múltiplos requests para a API da Latam.