mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-18 05:35:10 +00:00
ETAs Service (#31)
This commit is contained in:
102
etas/handler/etas.go
Normal file
102
etas/handler/etas.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
pb "etas/proto"
|
||||
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"googlemaps.github.io/maps"
|
||||
)
|
||||
|
||||
type ETAs struct {
|
||||
Maps *maps.Client
|
||||
}
|
||||
|
||||
// Calculate the ETAs for a route
|
||||
func (e *ETAs) Calculate(ctx context.Context, req *pb.Route, rsp *pb.Response) error {
|
||||
// validate the request
|
||||
if req.Pickup == nil {
|
||||
return errors.BadRequest("etas.Calculate", "Missing pickup")
|
||||
}
|
||||
if len(req.Waypoints) == 0 {
|
||||
return errors.BadRequest("etas.Calculate", "One more more waypoints required")
|
||||
}
|
||||
if err := validatePoint(req.Pickup, "Pickup"); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, p := range req.Waypoints {
|
||||
if err := validatePoint(p, fmt.Sprintf("Waypoint %v", i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// construct the request
|
||||
destinations := make([]string, len(req.Waypoints))
|
||||
for i, p := range req.Waypoints {
|
||||
destinations[i] = pointToCoords(p)
|
||||
}
|
||||
departureTime := "now"
|
||||
if req.StartTime != nil {
|
||||
departureTime = req.StartTime.String()
|
||||
}
|
||||
resp, err := e.Maps.DistanceMatrix(ctx, &maps.DistanceMatrixRequest{
|
||||
Origins: []string{pointToCoords(req.Pickup)},
|
||||
Destinations: destinations,
|
||||
DepartureTime: departureTime,
|
||||
Units: "UnitsMetric",
|
||||
Mode: maps.TravelModeDriving,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check the correct number of elements (route segments) were returned
|
||||
// from the Google API
|
||||
if len(resp.Rows[0].Elements) != len(destinations) {
|
||||
return errors.InternalServerError("etas.Calculate", "Invalid downstream response. Expected %v segments but got %v", len(destinations), len(resp.Rows[0].Elements))
|
||||
}
|
||||
|
||||
// calculate the response
|
||||
currentTime := time.Now()
|
||||
if req.StartTime != nil {
|
||||
currentTime = req.StartTime.AsTime()
|
||||
}
|
||||
rsp.Points = make(map[string]*pb.ETA, len(req.Waypoints)+1)
|
||||
for i, p := range append([]*pb.Point{req.Pickup}, req.Waypoints...) {
|
||||
at := currentTime
|
||||
if i > 0 {
|
||||
at = at.Add(resp.Rows[0].Elements[i-1].Duration)
|
||||
}
|
||||
et := at.Add(time.Minute * time.Duration(p.WaitTime))
|
||||
|
||||
rsp.Points[p.Id] = &pb.ETA{
|
||||
EstimatedArrivalTime: timestamppb.New(at),
|
||||
EstimatedDepartureTime: timestamppb.New(et),
|
||||
}
|
||||
|
||||
currentTime = et
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePoint(p *pb.Point, desc string) error {
|
||||
if len(p.Id) == 0 {
|
||||
return errors.BadRequest("etas.Calculate", "%v missing ID", desc)
|
||||
}
|
||||
if p.Latitude == 0 {
|
||||
return errors.BadRequest("etas.Calculate", "%v missing Latitude", desc)
|
||||
}
|
||||
if p.Longitude == 0 {
|
||||
return errors.BadRequest("etas.Calculate", "%v missing Longitude", desc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pointToCoords(p *pb.Point) string {
|
||||
return fmt.Sprintf("%v,%v", p.Latitude, p.Longitude)
|
||||
}
|
||||
129
etas/handler/etas_test.go
Normal file
129
etas/handler/etas_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"etas/handler"
|
||||
pb "etas/proto"
|
||||
|
||||
"googlemaps.github.io/maps"
|
||||
)
|
||||
|
||||
func TestCalculate(t *testing.T) {
|
||||
// mock the API response from Google Maps
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||
fmt.Fprintln(w, `{
|
||||
"rows": [
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"duration": {
|
||||
"text": "10 mins",
|
||||
"value": 600
|
||||
},
|
||||
"status": "OK"
|
||||
},
|
||||
{
|
||||
"duration": {
|
||||
"text": "6 mins",
|
||||
"value": 360
|
||||
},
|
||||
"status": "OK"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"status": "OK"
|
||||
}`)
|
||||
}))
|
||||
defer s.Close()
|
||||
m, err := maps.NewClient(maps.WithAPIKey("notrequired"), maps.WithBaseURL(s.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// construct the handler and test the response
|
||||
e := handler.ETAs{m}
|
||||
t.Run("MissingPickup", func(t *testing.T) {
|
||||
err := e.Calculate(context.TODO(), &pb.Route{
|
||||
Waypoints: []*pb.Point{
|
||||
&pb.Point{
|
||||
Id: "shenfield-station",
|
||||
Latitude: 51.6308,
|
||||
Longitude: 0.3295,
|
||||
},
|
||||
},
|
||||
}, &pb.Response{})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("MissingWaypoints", func(t *testing.T) {
|
||||
err := e.Calculate(context.TODO(), &pb.Route{
|
||||
Pickup: &pb.Point{
|
||||
Id: "shenfield-station",
|
||||
Latitude: 51.6308,
|
||||
Longitude: 0.3295,
|
||||
},
|
||||
}, &pb.Response{})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
st := time.Unix(1609459200, 0)
|
||||
|
||||
var rsp pb.Response
|
||||
err := e.Calculate(context.TODO(), &pb.Route{
|
||||
StartTime: timestamppb.New(st),
|
||||
Pickup: &pb.Point{
|
||||
Id: "shenfield-station",
|
||||
Latitude: 51.6308,
|
||||
Longitude: 0.3295,
|
||||
WaitTime: 5,
|
||||
},
|
||||
Waypoints: []*pb.Point{
|
||||
{
|
||||
Id: "nandos",
|
||||
Latitude: 51.6199,
|
||||
Longitude: 0.2999,
|
||||
WaitTime: 10,
|
||||
},
|
||||
{
|
||||
Id: "brentwood-station",
|
||||
Latitude: 51.6136,
|
||||
Longitude: 0.2996,
|
||||
},
|
||||
},
|
||||
}, &rsp)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNilf(t, rsp.Points, "Points should be returned")
|
||||
|
||||
p := rsp.Points["shenfield-station"]
|
||||
ea := st
|
||||
ed := ea.Add(time.Minute * 5)
|
||||
assert.True(t, p.EstimatedArrivalTime.AsTime().Equal(ea))
|
||||
assert.True(t, p.EstimatedDepartureTime.AsTime().Equal(ed))
|
||||
|
||||
p = rsp.Points["nandos"]
|
||||
ea = ed.Add(time.Minute * 10) // drive time
|
||||
ed = ea.Add(time.Minute * 10) // wait time
|
||||
assert.True(t, p.EstimatedArrivalTime.AsTime().Equal(ea))
|
||||
assert.True(t, p.EstimatedDepartureTime.AsTime().Equal(ed))
|
||||
|
||||
p = rsp.Points["brentwood-station"]
|
||||
ea = ed.Add(time.Minute * 6) // drive time
|
||||
ed = ea
|
||||
assert.True(t, p.EstimatedArrivalTime.AsTime().Equal(ea))
|
||||
assert.True(t, p.EstimatedDepartureTime.AsTime().Equal(ed))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user