Skip to content

Commit

Permalink
Validate parameters and retry request on error (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
krisukox authored Jul 29, 2023
1 parent eaf1c41 commit 8e7c426
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 157 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ jobs:
- name: Check out repository code
uses: actions/checkout@v3

- name: Run unit tests
run: |
go run ./examples/example1/main.go
go run ./examples/example2/main.go
- name: Run example1
run: go run ./examples/example1/main.go

- name: Run example2
run: go run ./examples/example2/main.go
3 changes: 2 additions & 1 deletion flights/city.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/url"

"github.com/hashicorp/go-retryablehttp"
"golang.org/x/text/language"
)

Expand All @@ -24,7 +25,7 @@ func (s *Session) doRequestCity(city string, lang language.Tag) (*http.Response,
`f.req=` + getCityReqData(city) +
`&at=AAuQa1qJpLKW2Hl-i40OwJyzmo22%3A1687083247610&`)

req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(jsonBody))
req, err := retryablehttp.NewRequest(http.MethodPost, requestURL, bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
Expand Down
8 changes: 8 additions & 0 deletions flights/city_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,12 @@ func TestAbbrCity(t *testing.T) {
t.Fatalf("wrong abbreviated city name, expected: %s received: %s", abbrB, b)
}
}

if abbrCity, ok := session.Cities.Load("Athens"); !ok || abbrCity != abbrA {
t.Fatalf("Athens abbreviated city name not stored in cache")
}

if abbrCity, ok := session.Cities.Load("Warsaw"); !ok || abbrCity != abbrB {
t.Fatalf("Warsaw abbreviated city name not stored in cache")
}
}
177 changes: 80 additions & 97 deletions flights/flight.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"time"

"github.com/hashicorp/go-retryablehttp"
"golang.org/x/text/currency"
"golang.org/x/text/language"
)
Expand Down Expand Up @@ -159,7 +159,7 @@ func (s *Session) doRequestFlights(
`f.req=` + reqDate +
`&at=AAuQa1qjMakasqKYcQeoFJjN7RZ3%3A1687955915303&`) // Add current unix timestamp instead of 1687955915303

req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody))
req, err := retryablehttp.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
Expand All @@ -176,32 +176,8 @@ func (s *Session) doRequestFlights(
return s.client.Do(req)
}

func getUnknowns(flightObj1 []interface{}) []interface{} {
unknowns := []interface{}{}
unknowns = append(unknowns, getRawElement(flightObj1, 1))
unknowns = append(unknowns, getRawElement(flightObj1, 2))
unknowns = append(unknowns, getRawElement(flightObj1, 7))
unknowns = append(unknowns, getRawElement(flightObj1, 9))
unknowns = append(unknowns, getRawElement(flightObj1, 12))
unknowns = append(unknowns, getRawElement(flightObj1, 13))
unknowns = append(unknowns, getRawElement(flightObj1, 14))
unknowns = append(unknowns, getRawElement(flightObj1, 15))
unknowns = append(unknowns, getRawElement(flightObj1, 16))
unknowns = append(unknowns, getRawElement(flightObj1, 18))
unknowns = append(unknowns, getRawElement(flightObj1, 19))
unknowns = append(unknowns, getRawElement(flightObj1, 23))
unknowns = append(unknowns, getRawElement(flightObj1, 24))
unknowns = append(unknowns, getRawElement(flightObj1, 25))
unknowns = append(unknowns, getRawElement(flightObj1, 26))
unknowns = append(unknowns, getRawElement(flightObj1, 27))
unknowns = append(unknowns, getRawElement(flightObj1, 28))
unknowns = append(unknowns, getRawElement(flightObj1, 29))
unknowns = append(unknowns, getRawElement(flightObj1, 31))
return unknowns
}

func getTripDuration(flights []Flight) time.Duration {
return flights[len(flights)-1].DepTime.Sub(flights[0].DepTime)
func getFlightsDuration(flights []Flight) time.Duration {
return flights[len(flights)-1].ArrTime.Sub(flights[0].DepTime)
}

func flightSchema(
Expand Down Expand Up @@ -247,7 +223,39 @@ func flightSchema(
}
}

func getOffersFromSection(rawOffers []json.RawMessage) ([]FullOffer, error) {
func getFlights(rawFlights []json.RawMessage) ([]Flight, error) {
flights := []Flight{}
for _, rawFlight := range rawFlights {
flight := Flight{}
flight.Unknown = make([]interface{}, 20)
var depHours, depMinutes, arrHours, arrMinutes, duration,
depYear, depMonth, depDay, arrYear, arrMonth, arrDay float64
var flightNoPart1, flightNoPart2 string
if err := json.Unmarshal(rawFlight, flightSchema(
&flight,
&depYear, &depMonth, &depDay, &depHours, &depMinutes,
&arrYear, &arrMonth, &arrDay, &arrHours, &arrMinutes,
&duration,
&flightNoPart1, &flightNoPart2,
)); err != nil {
return nil, err
}

flight.DepTime = time.Date(int(depYear), time.Month(depMonth), int(depDay), int(depHours), int(depMinutes), 0, 0, time.UTC)
flight.ArrTime = time.Date(int(arrYear), time.Month(arrMonth), int(arrDay), int(arrHours), int(arrMinutes), 0, 0, time.UTC)
parsedDuration, _ := time.ParseDuration(fmt.Sprintf("%dm", int(duration)))
flight.Duration = parsedDuration
flight.FlightNumber = flightNoPart1 + " " + flightNoPart2
flights = append(flights, flight)
}
return flights, nil
}

func offerSchema(rawFlights *[]json.RawMessage, price *float64) *[]interface{} {
return &[]interface{}{&[]interface{}{&[]interface{}{nil, nil, rawFlights}, &[]interface{}{&[]interface{}{nil, price}}}}
}

func getSubsectionOffers(rawOffers []json.RawMessage) ([]FullOffer, error) {
offers := []FullOffer{}
for _, rawOffer := range rawOffers {
offer := FullOffer{}
Expand All @@ -257,65 +265,49 @@ func getOffersFromSection(rawOffers []json.RawMessage) ([]FullOffer, error) {
continue
}

if err := json.Unmarshal(rawOffer, &[]interface{}{&[]interface{}{&[]interface{}{nil, nil, &rawFlights}, &[]interface{}{&[]interface{}{nil, &offer.Price}}}}); err != nil {
if err := json.Unmarshal(rawOffer, offerSchema(&rawFlights, &offer.Price)); err != nil {
continue
}
flights := []Flight{}
for _, rawFlight := range rawFlights {
flight := Flight{}
flight.Unknown = make([]interface{}, 20)
var depHours, depMinutes, arrHours, arrMinutes, duration,
depYear, depMonth, depDay, arrYear, arrMonth, arrDay float64
var flightNoPart1, flightNoPart2 string
if err := json.Unmarshal(rawFlight, flightSchema(
&flight,
&depYear, &depMonth, &depDay, &depHours, &depMinutes,
&arrYear, &arrMonth, &arrDay, &arrHours, &arrMinutes,
&duration,
&flightNoPart1, &flightNoPart2,
)); err != nil {
log.Fatal(err) // FIXME
}
location, _ := time.LoadLocation("Poland") // FIXME
flight.DepTime = time.Date(int(depYear), time.Month(depMonth), int(depDay), int(depHours), int(depMinutes), 0, 0, location)
flight.ArrTime = time.Date(int(arrYear), time.Month(arrMonth), int(arrDay), int(arrHours), int(arrMinutes), 0, 0, location)
parsedDuration, _ := time.ParseDuration(fmt.Sprintf("%dm", int(duration)))
flight.Duration = parsedDuration
flight.FlightNumber = flightNoPart1 + " " + flightNoPart2
flights = append(flights, flight)

flights, err := getFlights(rawFlights)
if err != nil || len(flights) == 0 {
continue
}

offer.Flight = flights
offer.ReturnFlight = []Flight{}
offer.StartDate = flights[0].DepTime // FIXME
offer.Duration = getTripDuration(flights)

offer.StartDate = flights[0].DepTime
offer.FlightDuration = getFlightsDuration(flights)

offers = append(offers, offer)
}
return offers, nil
}

func getOffers(bytesToDecode []byte) ([]FullOffer, *PriceRange, error) {
func sectionOffersSchema(rawOffers1, rawOffers2 *[]json.RawMessage, priceRange *PriceRange) *[]interface{} {
return &[]interface{}{nil, nil, rawOffers1, rawOffers2, nil, &[]interface{}{nil, nil, nil, nil,
&[]interface{}{nil, &priceRange.Low}, &[]interface{}{nil, &priceRange.High}}}
}

func getSectionOffers(bytesToDecode []byte) ([]FullOffer, *PriceRange, error) {
rawOffers1 := []json.RawMessage{}
rawOffers2 := []json.RawMessage{}

priceRange := PriceRange{}

locations := []json.RawMessage{} // TODO: use it to generate IATAcode -> country map, and use it in time.LoadLocation

if err := json.Unmarshal(bytesToDecode, &[]interface{}{
nil, nil, &rawOffers1, &rawOffers2, nil, &[]interface{}{nil, nil, nil, nil, &[]interface{}{nil, &priceRange.Low}, &[]interface{}{nil, &priceRange.High}},
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &locations}); err != nil {
if err := json.Unmarshal(bytesToDecode, sectionOffersSchema(&rawOffers1, &rawOffers2, &priceRange)); err != nil {
return nil, nil, err
}

allOffers := []FullOffer{}
offers1, err := getOffersFromSection(rawOffers1)
offers1, err := getSubsectionOffers(rawOffers1)
if err != nil {
return nil, nil, err
}
allOffers = append(allOffers, offers1...)

offers2, err := getOffersFromSection(rawOffers2)
offers2, err := getSubsectionOffers(rawOffers2)
if err != nil {
return nil, nil, err
}
Expand All @@ -324,7 +316,19 @@ func getOffers(bytesToDecode []byte) ([]FullOffer, *PriceRange, error) {
return allOffers, &priceRange, nil
}

func (s *Session) getAllOffers(
func checkDate(date, returnDate time.Time) error {
now := time.Now().Truncate(time.Hour * 24)

if returnDate.Before(date) {
return fmt.Errorf("returnDate is before date")
}
if date.Before(now) {
return fmt.Errorf("date is before today's date")
}
return nil
}

func (s *Session) GetOffers(
date, returnDate time.Time,
srcCities, srcAirports, dstCities, dstAirports []string,
adults int,
Expand All @@ -334,6 +338,17 @@ func (s *Session) getAllOffers(
tripType TripType,
lang language.Tag,
) ([]FullOffer, PriceRange, error) {
date = date.Truncate(24 * time.Hour)
returnDate = returnDate.Truncate(24 * time.Hour)

if err := checkDate(date, returnDate); err != nil {
return nil, PriceRange{}, err
}

if err := checkLocations(srcCities, srcAirports, dstCities, dstAirports); err != nil {
return nil, PriceRange{}, err
}

finalOffers := []FullOffer{}
finalPriceRange := PriceRange{}
resp, err := s.doRequestFlights(
Expand All @@ -356,7 +371,7 @@ func (s *Session) getAllOffers(
if err != nil {
return finalOffers, finalPriceRange, nil
}
offers, priceRange, _ := getOffers(bytesToDecode)
offers, priceRange, _ := getSectionOffers(bytesToDecode)
if offers != nil {
finalOffers = append(finalOffers, offers...)
}
Expand All @@ -365,35 +380,3 @@ func (s *Session) getAllOffers(
}
}
}

func (s *Session) GetOffers(
date, returnDate time.Time,
srcCities, srcAirports, dstCities, dstAirports []string,
adults int,
curr currency.Unit,
stops Stops,
class Class,
tripType TripType,
lang language.Tag,
) ([]FullOffer, PriceRange, error) {
log.SetFlags(log.LstdFlags | log.Lshortfile)
// TODO: Add date limit
var allOffers []FullOffer
var priceRange PriceRange
var err error

retries := 4

for i := 0; i < retries; i++ {
allOffers, priceRange, err = s.getAllOffers(
date, returnDate,
srcCities, srcAirports, dstCities, dstAirports,
adults, curr, stops, class, tripType, lang)
if err == nil {
return allOffers, priceRange, nil
}
log.Printf("Retry GetOffers")
}

return allOffers, priceRange, fmt.Errorf("number of retries %d exceeded: %w", retries, err)
}
49 changes: 32 additions & 17 deletions flights/flight_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package flights

import (
"fmt"
"net/url"
"testing"
"time"
Expand Down Expand Up @@ -83,21 +82,19 @@ func TestGetOffers(t *testing.T) {
dummyTime := time.Now()
dummyValue := 0

t1, _ := time.Parse(time.RFC3339, "2024-01-22T17:00:00+01:00")
t2, _ := time.Parse(time.RFC3339, "2024-01-22T18:35:00+01:00")
t1, _ := time.Parse(time.RFC3339, "2024-01-22T17:00:00Z")
t2, _ := time.Parse(time.RFC3339, "2024-01-22T18:35:00Z")
d1, _ := time.ParseDuration("1h35m0s")

t3, _ := time.Parse(time.RFC3339, "2024-01-22T21:25:00+01:00")
t4, _ := time.Parse(time.RFC3339, "2024-01-23T00:50:00+01:00")
t3, _ := time.Parse(time.RFC3339, "2024-01-22T21:25:00Z")
t4, _ := time.Parse(time.RFC3339, "2024-01-23T00:50:00Z")
d2, _ := time.ParseDuration("2h25m0s")

d3, _ := time.ParseDuration("4h25m0s")

t5, _ := time.Parse(time.RFC3339, "2024-01-22T17:00:00+01:00")
d3, _ := time.ParseDuration("7h50m0s")

expectedOffer := FullOffer{
Offer: Offer{
StartDate: t5,
StartDate: t1,
Price: 1315,
},
Flight: []Flight{{
Expand Down Expand Up @@ -125,12 +122,12 @@ func TestGetOffers(t *testing.T) {
AirlineName: "Lufthansa",
Legroom: "29 inches",
}},
ReturnFlight: []Flight{},
SrcAirportCode: "",
DstAirportCode: "",
OriginCity: "",
DestinationCity: "",
Duration: d3,
ReturnFlight: []Flight{},
SrcAirportCode: "",
DstAirportCode: "",
SrcCity: "",
DstCity: "",
FlightDuration: d3,
}
expectedPriceRange := PriceRange{1300, 2300}

Expand Down Expand Up @@ -166,8 +163,6 @@ func TestGetOffers(t *testing.T) {
t.Fatal(err)
}

fmt.Println(offers[0].Flight[0].Unknown...)

// Do not compare the unknown field
removeUnknowns(offers)

Expand Down Expand Up @@ -245,3 +240,23 @@ func TestFlightReqData(t *testing.T) {
t.Fatalf("wrong unescaped query, expected: %s received: %s", expectedReqData2, reqData2)
}
}

func _testGetOffersDateLimit(t *testing.T, session *Session, date, returnDate time.Time, errorValue string) {
_, _, err := session.GetOffers(
date, returnDate,
[]string{}, []string{}, []string{}, []string{},
0, currency.PLN, AnyStops, Economy, RoundTrip, language.English)

if err == nil {
t.Fatalf("GetOffers call for the following dates date %s returnDate %s, should result in error", date, returnDate)
} else if err.Error() != errorValue {
t.Fatalf(`Wrong error "%s" for GetOffers call with the following dates date %s returnDate %s`, err.Error(), date, returnDate)
}
}

func TestGetOffersDateLimit(t *testing.T) {
session := New()

_testGetOffersDateLimit(t, session, time.Now().AddDate(0, 0, 3), time.Now().AddDate(0, 0, 1), "returnDate is before date")
_testGetOffersDateLimit(t, session, time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, 2), "date is before today's date")
}
Loading

0 comments on commit 8e7c426

Please sign in to comment.