forked from micro/services
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
47f52e4
commit da97810
Showing
15 changed files
with
1,437 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
etas |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
FROM alpine | ||
ADD etas /etas | ||
ENTRYPOINT [ "/etas" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
GOPATH:=$(shell go env GOPATH) | ||
|
||
.PHONY: init | ||
init: | ||
go get -u github.com/golang/protobuf/proto | ||
go get -u github.com/golang/protobuf/protoc-gen-go | ||
go get github.com/micro/micro/v3/cmd/protoc-gen-micro | ||
|
||
.PHONY: proto | ||
proto: | ||
protoc --proto_path=. --micro_out=. --go_out=:. proto/etas.proto | ||
|
||
.PHONY: build | ||
build: | ||
go build -o etas *.go | ||
|
||
.PHONY: test | ||
test: | ||
go test -v ./... -cover | ||
|
||
.PHONY: docker | ||
docker: | ||
docker build . -t etas:latest |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# ETAs Service | ||
|
||
This is the ETAs service. It provides ETAs for single-pickup, multi-dropoff routes. It takes into account time and traffic. | ||
|
||
Current limitations: | ||
• Only supports "Driving" (not walking, cycling) | ||
• Does not optimize route | ||
|
||
## Usage | ||
|
||
There is one required config value: `google.maps.apikey`. Once you have set this config value, run the service using `micro run`. | ||
|
||
```bash | ||
micro@Bens-MBP-3 etas % micro call etas ETAs.Calculate $(cat example-req.json) | ||
{ | ||
"points": { | ||
"brentwood-station": { | ||
"estimated_arrival_time": "2020-12-15T11:01:29.429947Z", | ||
"estimated_departure_time": "2020-12-15T11:01:29.429947Z" | ||
}, | ||
"nandos": { | ||
"estimated_arrival_time": "2020-12-15T10:54:38.429947Z", | ||
"estimated_departure_time": "2020-12-15T10:54:38.429947Z" | ||
}, | ||
"shenfield-station": { | ||
"estimated_arrival_time": "2020-12-15T10:48:34.429947Z", | ||
"estimated_departure_time": "2020-12-15T10:48:34.429947Z" | ||
} | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"pickup": { | ||
"id": "shenfield-station", | ||
"latitude": 51.6308, | ||
"longitude": 0.3295 | ||
}, | ||
"waypoints": [ | ||
{ | ||
"id": "nandos", | ||
"latitude": 51.6199, | ||
"longitude": 0.2999 | ||
}, | ||
{ | ||
"id": "brentwood-station", | ||
"latitude": 51.6136, | ||
"longitude": 0.2996 | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
package main | ||
//go:generate make proto |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
module etas | ||
|
||
go 1.15 | ||
|
||
require ( | ||
github.com/golang/protobuf v1.4.3 | ||
github.com/micro/micro/v3 v3.0.2 | ||
github.com/stretchr/testify v1.6.1 | ||
google.golang.org/protobuf v1.25.0 | ||
googlemaps.github.io/maps v1.3.1 | ||
) |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package main | ||
|
||
import ( | ||
"etas/handler" | ||
pb "etas/proto" | ||
|
||
"googlemaps.github.io/maps" | ||
|
||
"github.com/micro/micro/v3/service" | ||
"github.com/micro/micro/v3/service/config" | ||
"github.com/micro/micro/v3/service/logger" | ||
) | ||
|
||
func main() { | ||
// Create service | ||
srv := service.New( | ||
service.Name("etas"), | ||
service.Version("latest"), | ||
) | ||
|
||
// Connect to GoogleMaps | ||
cf, err := config.Get("google.maps.apikey") | ||
if err != nil { | ||
logger.Fatalf("Error loading config: %v", err) | ||
} | ||
key := cf.String("") | ||
if len(key) == 0 { | ||
logger.Fatalf("Missing require config: google.maps.apikey") | ||
} | ||
m, err := maps.NewClient(maps.WithAPIKey(key)) | ||
if err != nil { | ||
logger.Fatal(err) | ||
} | ||
|
||
// Register handler | ||
pb.RegisterETAsHandler(srv.Server(), &handler.ETAs{Maps: m}) | ||
|
||
// Run service | ||
if err := srv.Run(); err != nil { | ||
logger.Fatal(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
service etas |
Oops, something went wrong.