rename locations to places (#38)

* rename locations to places

* update failing things

Co-authored-by: Janos Dobronszki <dobronszki@gmail.com>
This commit is contained in:
Asim Aslam
2021-01-13 13:08:19 +00:00
committed by GitHub
parent a8e56e5e9e
commit ef2f313e33
15 changed files with 662 additions and 882 deletions

1
places/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
locations

18
places/Makefile Normal file
View File

@@ -0,0 +1,18 @@
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/places.proto
.PHONY: build
build:
go build -o places *.go
.PHONY: test
test:
go test -v ./... -cover

23
places/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Locations Service
This is the Locations service
Generated with
```
micro new locations
```
## Usage
Locations makes use of postgres. Set the config for the database
```
micro user config set locations.database "postgresql://postgres@localhost:5432/locations?sslmode=disable"
```
Run the service
```
micro run .
```

3
places/generate.go Normal file
View File

@@ -0,0 +1,3 @@
package main
//go:generate make proto

177
places/handler/places.go Normal file
View File

@@ -0,0 +1,177 @@
package handler
import (
"context"
"sync"
"time"
"github.com/google/uuid"
geo "github.com/hailocab/go-geoindex"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
"gorm.io/gorm"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/places/model"
pb "github.com/micro/services/places/proto"
)
var (
ErrMissingPlaces = errors.BadRequest("MISSING_LOCATIONS", "One or more places are required")
ErrMissingLatitude = errors.BadRequest("MISSING_LATITUDE", "Latitude is required")
ErrMissingLongitude = errors.BadRequest("MISSING_LONGITUDE", "Longitude is required")
ErrMissingID = errors.BadRequest("MISSING_ID", "Place ID is required")
ErrMissingIDs = errors.BadRequest("MISSING_IDS", "One or more Place IDs are required")
ErrMissingBefore = errors.BadRequest("MISSING_BEFORE", "Before timestamp is required")
ErrMissingAfter = errors.BadRequest("MISSING_AFTER", "After timestamp is required")
ErrMissingRadius = errors.BadRequest("MISSING_RADIUS", "Radius is required")
)
type Places struct {
sync.RWMutex
Geoindex *geo.PointsIndex
DB *gorm.DB
}
// Save a set of places
func (l *Places) Save(ctx context.Context, req *pb.SaveRequest, rsp *pb.SaveResponse) error {
// validate the request
if len(req.Places) == 0 {
return ErrMissingPlaces
}
for _, l := range req.Places {
if l.Latitude == nil {
return ErrMissingLatitude
}
if l.Longitude == nil {
return ErrMissingLongitude
}
if len(l.Id) == 0 {
return ErrMissingID
}
}
// construct the database objects
ls := make([]*model.Location, len(req.Places))
for i, lc := range req.Places {
loc := &model.Location{
ID: uuid.New().String(),
PlaceID: lc.Id,
Latitude: lc.Latitude.Value,
Longitude: lc.Longitude.Value,
}
if lc.Timestamp != nil {
loc.Timestamp = lc.Timestamp.AsTime()
} else {
loc.Timestamp = time.Now()
}
ls[i] = loc
}
// write to the database
if err := l.DB.Create(ls).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error writing to the database")
}
// write to the geoindex
l.Lock()
defer l.Unlock()
for _, lc := range ls {
l.Geoindex.Add(lc)
}
return nil
}
// Last places for a set of users
func (l *Places) Last(ctx context.Context, req *pb.LastRequest, rsp *pb.ListResponse) error {
// validate the request
if req.Ids == nil {
return ErrMissingIDs
}
// query the database
q := l.DB.Raw("SELECT DISTINCT ON (place_id) place_id, timestamp, latitude, longitude FROM places WHERE place_id IN (?) ORDER BY place_id, timestamp DESC", req.Ids)
var locs []*model.Location
if err := q.Find(&locs).Error; err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error reading from the database")
}
// serialize the result
rsp.Places = serializePlaces(locs)
return nil
}
// Near returns the places near a point
func (l *Places) Near(ctx context.Context, req *pb.NearRequest, rsp *pb.ListResponse) error {
// validate the request
if req.Latitude == nil {
return ErrMissingLatitude
}
if req.Longitude == nil {
return ErrMissingLongitude
}
if req.Radius == nil {
return ErrMissingRadius
}
// query the geoindex
l.RLock()
p := geo.NewGeoPoint("query", req.Latitude.Value, req.Longitude.Value)
result := l.Geoindex.PointsWithin(p, geo.Km(req.Radius.Value), func(p geo.Point) bool {
return true
})
l.RUnlock()
// serialize the result
locs := make([]*model.Location, len(result))
for i, r := range result {
locs[i] = r.(*model.Location)
}
rsp.Places = serializePlaces(locs)
return nil
}
// Read places for a group of users between two points in time
func (l *Places) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ListResponse) error {
// validate the request
if len(req.Ids) == 0 {
return ErrMissingIDs
}
if req.Before == nil {
return ErrMissingBefore
}
if req.After == nil {
return ErrMissingAfter
}
// construct the request
q := l.DB.Model(&model.Location{})
q = q.Order("timestamp ASC")
q = q.Where("place_id IN (?) AND timestamp > ? AND timestamp < ?", req.Ids, req.After.AsTime(), req.Before.AsTime())
var locs []*model.Location
if err := q.Find(&locs).Error; err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error reading from the database")
}
// serialize the result
rsp.Places = serializePlaces(locs)
return nil
}
func serializePlaces(locs []*model.Location) []*pb.Location {
rsp := make([]*pb.Location, len(locs))
for i, l := range locs {
rsp[i] = &pb.Location{
Id: l.PlaceID,
Latitude: &wrapperspb.DoubleValue{Value: l.Latitude},
Longitude: &wrapperspb.DoubleValue{Value: l.Longitude},
Timestamp: timestamppb.New(l.Timestamp),
}
}
return rsp
}

View File

@@ -0,0 +1,519 @@
package handler_test
import (
"context"
"sort"
"testing"
"time"
"github.com/google/uuid"
geo "github.com/hailocab/go-geoindex"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"github.com/micro/services/places/handler"
"github.com/micro/services/places/model"
pb "github.com/micro/services/places/proto"
)
func testHandler(t *testing.T) pb.PlacesHandler {
// connect to the database
db, err := gorm.Open(postgres.Open("postgresql://postgres@localhost:5432/places?sslmode=disable"), &gorm.Config{})
if err != nil {
t.Fatalf("Error connecting to database: %v", err)
}
// migrate the database
if err := db.AutoMigrate(&model.Location{}); err != nil {
t.Fatalf("Error migrating database: %v", err)
}
// clean any data from a previous run
if err := db.Exec("TRUNCATE TABLE places CASCADE").Error; err != nil {
t.Fatalf("Error cleaning database: %v", err)
}
return &handler.Places{DB: db, Geoindex: geo.NewPointsIndex(geo.Km(0.1))}
}
func TestSave(t *testing.T) {
tt := []struct {
Name string
Places []*pb.Location
Error error
}{
{
Name: "NoPlaces",
Error: handler.ErrMissingPlaces,
},
{
Name: "NoLatitude",
Places: []*pb.Location{
{
Longitude: &wrapperspb.DoubleValue{Value: -0.1246},
Id: uuid.New().String(),
},
},
Error: handler.ErrMissingLatitude,
},
{
Name: "NoLongitude",
Places: []*pb.Location{
{
Latitude: &wrapperspb.DoubleValue{Value: -0.1246},
Id: uuid.New().String(),
},
},
Error: handler.ErrMissingLongitude,
},
{
Name: "OneLocation",
Places: []*pb.Location{
{
Latitude: &wrapperspb.DoubleValue{Value: 51.5007},
Longitude: &wrapperspb.DoubleValue{Value: 0.1246},
Timestamp: timestamppb.New(time.Now()),
Id: uuid.New().String(),
},
},
},
{
Name: "ManyPlaces",
Places: []*pb.Location{
{
Latitude: &wrapperspb.DoubleValue{Value: 51.5007},
Longitude: &wrapperspb.DoubleValue{Value: 0.1246},
Timestamp: timestamppb.New(time.Now()),
Id: uuid.New().String(),
},
{
Latitude: &wrapperspb.DoubleValue{Value: 51.003},
Longitude: &wrapperspb.DoubleValue{Value: -0.1246},
Id: uuid.New().String(),
},
},
},
}
h := testHandler(t)
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
err := h.Save(context.Background(), &pb.SaveRequest{
Places: tc.Places,
}, &pb.SaveResponse{})
assert.Equal(t, tc.Error, err)
})
}
}
func TestLast(t *testing.T) {
h := testHandler(t)
t.Run("MissingIDs", func(t *testing.T) {
err := h.Last(context.Background(), &pb.LastRequest{}, &pb.ListResponse{})
assert.Equal(t, handler.ErrMissingIDs, err)
})
t.Run("NoMatches", func(t *testing.T) {
var rsp pb.ListResponse
err := h.Last(context.Background(), &pb.LastRequest{
Ids: []string{uuid.New().String()},
}, &rsp)
assert.NoError(t, err)
assert.Empty(t, rsp.Places)
})
// generate some example data to work with
loc1 := &pb.Location{
Latitude: &wrapperspb.DoubleValue{Value: 51.5007},
Longitude: &wrapperspb.DoubleValue{Value: 0.1246},
Timestamp: timestamppb.New(time.Now()),
Id: "a",
}
loc2 := &pb.Location{
Latitude: &wrapperspb.DoubleValue{Value: 51.6007},
Longitude: &wrapperspb.DoubleValue{Value: 0.1546},
Timestamp: timestamppb.New(time.Now()),
Id: "b",
}
loc3 := &pb.Location{
Latitude: &wrapperspb.DoubleValue{Value: 52.6007},
Longitude: &wrapperspb.DoubleValue{Value: 0.2546},
Timestamp: timestamppb.New(time.Now()),
Id: loc2.Id,
}
err := h.Save(context.TODO(), &pb.SaveRequest{
Places: []*pb.Location{loc1, loc2, loc3},
}, &pb.SaveResponse{})
assert.NoError(t, err)
t.Run("OneUser", func(t *testing.T) {
var rsp pb.ListResponse
err := h.Last(context.Background(), &pb.LastRequest{
Ids: []string{loc2.Id},
}, &rsp)
assert.NoError(t, err)
if len(rsp.Places) != 1 {
t.Fatalf("One location should be returned")
}
assert.Equal(t, loc3.Id, rsp.Places[0].Id)
assert.Equal(t, loc3.Latitude.Value, rsp.Places[0].Latitude.Value)
assert.Equal(t, loc3.Longitude.Value, rsp.Places[0].Longitude.Value)
assert.Equal(t, loc3.Timestamp.AsTime(), rsp.Places[0].Timestamp.AsTime())
})
t.Run("ManyUser", func(t *testing.T) {
var rsp pb.ListResponse
err := h.Last(context.Background(), &pb.LastRequest{
Ids: []string{loc1.Id, loc2.Id},
}, &rsp)
assert.NoError(t, err)
if len(rsp.Places) != 2 {
t.Fatalf("Two places should be returned")
}
// sort using user_id so we can hardcode the index
sort.Slice(rsp.Places, func(i, j int) bool {
return rsp.Places[i].Id > rsp.Places[j].Id
})
assert.Equal(t, loc1.Id, rsp.Places[1].Id)
assert.Equal(t, loc1.Latitude.Value, rsp.Places[1].Latitude.Value)
assert.Equal(t, loc1.Longitude.Value, rsp.Places[1].Longitude.Value)
assert.Equal(t, loc1.Timestamp.AsTime(), rsp.Places[1].Timestamp.AsTime())
assert.Equal(t, loc3.Id, rsp.Places[0].Id)
assert.Equal(t, loc3.Latitude.Value, rsp.Places[0].Latitude.Value)
assert.Equal(t, loc3.Longitude.Value, rsp.Places[0].Longitude.Value)
assert.Equal(t, loc3.Timestamp.AsTime(), rsp.Places[0].Timestamp.AsTime())
})
}
func TestNear(t *testing.T) {
lat := &wrapperspb.DoubleValue{Value: 51.510357}
lng := &wrapperspb.DoubleValue{Value: -0.116773}
rad := &wrapperspb.DoubleValue{Value: 2.0}
inBoundsLat := &wrapperspb.DoubleValue{Value: 51.5110}
inBoundsLng := &wrapperspb.DoubleValue{Value: -0.1142}
outOfBoundsLat := &wrapperspb.DoubleValue{Value: 51.5415}
outOfBoundsLng := &wrapperspb.DoubleValue{Value: -0.0028}
tt := []struct {
Name string
Places []*pb.Location
Results []*pb.Location
QueryLatitude *wrapperspb.DoubleValue
QueryLongitude *wrapperspb.DoubleValue
QueryRadius *wrapperspb.DoubleValue
Error error
}{
{
Name: "MissingLatitude",
QueryLongitude: lng,
QueryRadius: rad,
Error: handler.ErrMissingLatitude,
},
{
Name: "MissingLongitude",
QueryLatitude: lat,
QueryRadius: rad,
Error: handler.ErrMissingLongitude,
},
{
Name: "MissingRadius",
QueryLatitude: lat,
QueryLongitude: lng,
Error: handler.ErrMissingRadius,
},
{
Name: "NoPlaces",
QueryLatitude: lat,
QueryLongitude: lng,
QueryRadius: rad,
},
{
Name: "OneWithinRadius",
QueryLatitude: lat,
QueryLongitude: lng,
QueryRadius: rad,
Places: []*pb.Location{
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "in",
},
&pb.Location{
Latitude: outOfBoundsLat,
Longitude: outOfBoundsLng,
Id: "out",
},
},
Results: []*pb.Location{
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "in",
},
},
},
{
Name: "NoneWithinRadius",
QueryLatitude: lat,
QueryLongitude: lng,
QueryRadius: &wrapperspb.DoubleValue{Value: 0.01},
Places: []*pb.Location{
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "in",
},
&pb.Location{
Latitude: outOfBoundsLat,
Longitude: outOfBoundsLng,
Id: "out",
},
},
},
{
Name: "TwoPlacesForUser",
QueryLatitude: lat,
QueryLongitude: lng,
QueryRadius: rad,
Places: []*pb.Location{
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "in",
},
&pb.Location{
Latitude: outOfBoundsLat,
Longitude: outOfBoundsLng,
Id: "out",
},
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "out",
},
},
Results: []*pb.Location{
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "in",
},
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "out",
},
},
},
{
Name: "ManyWithinRadius",
QueryLatitude: lat,
QueryLongitude: lng,
QueryRadius: &wrapperspb.DoubleValue{Value: 20},
Places: []*pb.Location{
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "in",
},
&pb.Location{
Latitude: outOfBoundsLat,
Longitude: outOfBoundsLng,
Id: "out",
},
},
Results: []*pb.Location{
&pb.Location{
Latitude: inBoundsLat,
Longitude: inBoundsLng,
Id: "in",
},
&pb.Location{
Latitude: outOfBoundsLat,
Longitude: outOfBoundsLng,
Id: "out",
},
},
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
h := testHandler(t)
// create the places
if len(tc.Places) > 0 {
err := h.Save(context.TODO(), &pb.SaveRequest{Places: tc.Places}, &pb.SaveResponse{})
assert.NoError(t, err)
}
// find near places
var rsp pb.ListResponse
err := h.Near(context.TODO(), &pb.NearRequest{
Latitude: tc.QueryLatitude,
Longitude: tc.QueryLongitude,
Radius: tc.QueryRadius,
}, &rsp)
assert.Equal(t, tc.Error, err)
// check the count of the results matches
if len(tc.Results) != len(rsp.Places) {
t.Errorf("Incorrect number of results returned. Expected %v, got %v", len(tc.Results), len(rsp.Places))
}
// validate the results match
sort.Slice(rsp.Places, func(i, j int) bool {
return rsp.Places[i].Id > rsp.Places[j].Id
})
sort.Slice(tc.Results, func(i, j int) bool {
return tc.Results[i].Id > tc.Results[j].Id
})
for i, r := range tc.Results {
l := rsp.Places[i]
assert.Equal(t, r.Id, l.Id)
assert.Equal(t, r.Latitude.Value, l.Latitude.Value)
assert.Equal(t, r.Longitude.Value, l.Longitude.Value)
}
})
}
}
func TestRead(t *testing.T) {
h := testHandler(t)
baseTime := time.Now().Add(time.Hour * -24)
t.Run("MissingIDs", func(t *testing.T) {
err := h.Read(context.Background(), &pb.ReadRequest{
After: timestamppb.New(baseTime),
Before: timestamppb.New(baseTime),
}, &pb.ListResponse{})
assert.Equal(t, handler.ErrMissingIDs, err)
})
t.Run("MissingAfter", func(t *testing.T) {
err := h.Read(context.Background(), &pb.ReadRequest{
Ids: []string{uuid.New().String()},
Before: timestamppb.New(baseTime),
}, &pb.ListResponse{})
assert.Equal(t, handler.ErrMissingAfter, err)
})
t.Run("MissingBefore", func(t *testing.T) {
err := h.Read(context.Background(), &pb.ReadRequest{
Ids: []string{uuid.New().String()},
After: timestamppb.New(baseTime),
}, &pb.ListResponse{})
assert.Equal(t, handler.ErrMissingBefore, err)
})
// generate some example data to work with
loc1 := &pb.Location{
Latitude: &wrapperspb.DoubleValue{Value: 51.5007},
Longitude: &wrapperspb.DoubleValue{Value: 0.1246},
Timestamp: timestamppb.New(baseTime.Add(time.Minute * 10)),
Id: "a",
}
loc2 := &pb.Location{
Latitude: &wrapperspb.DoubleValue{Value: 51.6007},
Longitude: &wrapperspb.DoubleValue{Value: 0.1546},
Timestamp: timestamppb.New(baseTime.Add(time.Minute * 20)),
Id: "b",
}
loc3 := &pb.Location{
Latitude: &wrapperspb.DoubleValue{Value: 52.6007},
Longitude: &wrapperspb.DoubleValue{Value: 0.2546},
Timestamp: timestamppb.New(baseTime.Add(time.Minute * 40)),
Id: loc2.Id,
}
err := h.Save(context.TODO(), &pb.SaveRequest{
Places: []*pb.Location{loc1, loc2, loc3},
}, &pb.SaveResponse{})
assert.NoError(t, err)
t.Run("NoMatches", func(t *testing.T) {
var rsp pb.ListResponse
err := h.Read(context.Background(), &pb.ReadRequest{
Ids: []string{uuid.New().String()},
After: timestamppb.New(baseTime),
Before: timestamppb.New(baseTime.Add(time.Hour)),
}, &rsp)
assert.NoError(t, err)
assert.Empty(t, rsp.Places)
})
t.Run("OnePlaceID", func(t *testing.T) {
var rsp pb.ListResponse
err := h.Read(context.Background(), &pb.ReadRequest{
Ids: []string{loc2.Id},
After: timestamppb.New(baseTime),
Before: timestamppb.New(baseTime.Add(time.Hour)),
}, &rsp)
assert.NoError(t, err)
if len(rsp.Places) != 2 {
t.Fatalf("Two places should be returned")
}
assert.Equal(t, loc2.Id, rsp.Places[0].Id)
assert.Equal(t, loc2.Latitude.Value, rsp.Places[0].Latitude.Value)
assert.Equal(t, loc2.Longitude.Value, rsp.Places[0].Longitude.Value)
assert.Equal(t, loc2.Timestamp.AsTime(), rsp.Places[0].Timestamp.AsTime())
assert.Equal(t, loc3.Id, rsp.Places[1].Id)
assert.Equal(t, loc3.Latitude.Value, rsp.Places[1].Latitude.Value)
assert.Equal(t, loc3.Longitude.Value, rsp.Places[1].Longitude.Value)
assert.Equal(t, loc3.Timestamp.AsTime(), rsp.Places[1].Timestamp.AsTime())
})
t.Run("OnePlaceIDReducedTime", func(t *testing.T) {
var rsp pb.ListResponse
err := h.Read(context.Background(), &pb.ReadRequest{
Ids: []string{loc2.Id},
After: timestamppb.New(baseTime),
Before: timestamppb.New(baseTime.Add(time.Minute * 30)),
}, &rsp)
assert.NoError(t, err)
if len(rsp.Places) != 1 {
t.Fatalf("One location should be returned")
}
assert.Equal(t, loc2.Id, rsp.Places[0].Id)
assert.Equal(t, loc2.Latitude.Value, rsp.Places[0].Latitude.Value)
assert.Equal(t, loc2.Longitude.Value, rsp.Places[0].Longitude.Value)
assert.Equal(t, loc2.Timestamp.AsTime(), rsp.Places[0].Timestamp.AsTime())
})
t.Run("TwoPlaceIDs", func(t *testing.T) {
var rsp pb.ListResponse
err := h.Read(context.Background(), &pb.ReadRequest{
Ids: []string{loc1.Id, loc2.Id},
After: timestamppb.New(baseTime),
Before: timestamppb.New(baseTime.Add(time.Minute * 30)),
}, &rsp)
assert.NoError(t, err)
if len(rsp.Places) != 2 {
t.Fatalf("Two places should be returned")
}
assert.Equal(t, loc1.Id, rsp.Places[0].Id)
assert.Equal(t, loc1.Latitude.Value, rsp.Places[0].Latitude.Value)
assert.Equal(t, loc1.Longitude.Value, rsp.Places[0].Longitude.Value)
assert.Equal(t, loc1.Timestamp.AsTime(), rsp.Places[0].Timestamp.AsTime())
assert.Equal(t, loc2.Id, rsp.Places[1].Id)
assert.Equal(t, loc2.Latitude.Value, rsp.Places[1].Latitude.Value)
assert.Equal(t, loc2.Longitude.Value, rsp.Places[1].Longitude.Value)
assert.Equal(t, loc2.Timestamp.AsTime(), rsp.Places[1].Timestamp.AsTime())
})
}

51
places/main.go Normal file
View File

@@ -0,0 +1,51 @@
package main
import (
geo "github.com/hailocab/go-geoindex"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"github.com/micro/services/places/handler"
"github.com/micro/services/places/model"
pb "github.com/micro/services/places/proto"
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/config"
"github.com/micro/micro/v3/service/logger"
)
var dbAddress = "postgresql://postgres@localhost:5432/places?sslmode=disable"
func main() {
// Create service
srv := service.New(
service.Name("places"),
service.Version("latest"),
)
// Connect to the database
cfg, err := config.Get("places.database")
if err != nil {
logger.Fatalf("Error loading config: %v", err)
}
addr := cfg.String(dbAddress)
db, err := gorm.Open(postgres.Open(addr), &gorm.Config{})
if err != nil {
logger.Fatalf("Error connecting to database: %v", err)
}
// Migrate the database
if err := db.AutoMigrate(&model.Location{}); err != nil {
logger.Fatalf("Error migrating database: %v", err)
}
// Register handler
pb.RegisterPlacesHandler(srv.Server(), &handler.Places{
DB: db, Geoindex: geo.NewPointsIndex(geo.Km(0.1)),
})
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

1
places/micro.mu Normal file
View File

@@ -0,0 +1 @@
service places

26
places/model/location.go Normal file
View File

@@ -0,0 +1,26 @@
package model
import (
"time"
)
type Location struct {
ID string
PlaceID string `gorm:"index"`
Latitude float64
Longitude float64
Timestamp time.Time
}
// use the place id for the geoindex so only one result is returned per place
func (l *Location) Id() string {
return l.PlaceID
}
func (l *Location) Lat() float64 {
return l.Latitude
}
func (l *Location) Lon() float64 {
return l.Longitude
}

407
places/proto/places.pb.go Normal file
View File

@@ -0,0 +1,407 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: proto/places.proto
package places
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
timestamp "github.com/golang/protobuf/ptypes/timestamp"
wrappers "github.com/golang/protobuf/ptypes/wrappers"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type Location struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Metadata map[string]string `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Timestamp *timestamp.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Latitude *wrappers.DoubleValue `protobuf:"bytes,5,opt,name=latitude,proto3" json:"latitude,omitempty"`
Longitude *wrappers.DoubleValue `protobuf:"bytes,6,opt,name=longitude,proto3" json:"longitude,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Location) Reset() { *m = Location{} }
func (m *Location) String() string { return proto.CompactTextString(m) }
func (*Location) ProtoMessage() {}
func (*Location) Descriptor() ([]byte, []int) {
return fileDescriptor_3b635ff9d2e2d652, []int{0}
}
func (m *Location) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Location.Unmarshal(m, b)
}
func (m *Location) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Location.Marshal(b, m, deterministic)
}
func (m *Location) XXX_Merge(src proto.Message) {
xxx_messageInfo_Location.Merge(m, src)
}
func (m *Location) XXX_Size() int {
return xxx_messageInfo_Location.Size(m)
}
func (m *Location) XXX_DiscardUnknown() {
xxx_messageInfo_Location.DiscardUnknown(m)
}
var xxx_messageInfo_Location proto.InternalMessageInfo
func (m *Location) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Location) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Location) GetMetadata() map[string]string {
if m != nil {
return m.Metadata
}
return nil
}
func (m *Location) GetTimestamp() *timestamp.Timestamp {
if m != nil {
return m.Timestamp
}
return nil
}
func (m *Location) GetLatitude() *wrappers.DoubleValue {
if m != nil {
return m.Latitude
}
return nil
}
func (m *Location) GetLongitude() *wrappers.DoubleValue {
if m != nil {
return m.Longitude
}
return nil
}
type SaveRequest struct {
Places []*Location `protobuf:"bytes,1,rep,name=places,proto3" json:"places,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SaveRequest) Reset() { *m = SaveRequest{} }
func (m *SaveRequest) String() string { return proto.CompactTextString(m) }
func (*SaveRequest) ProtoMessage() {}
func (*SaveRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_3b635ff9d2e2d652, []int{1}
}
func (m *SaveRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SaveRequest.Unmarshal(m, b)
}
func (m *SaveRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SaveRequest.Marshal(b, m, deterministic)
}
func (m *SaveRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_SaveRequest.Merge(m, src)
}
func (m *SaveRequest) XXX_Size() int {
return xxx_messageInfo_SaveRequest.Size(m)
}
func (m *SaveRequest) XXX_DiscardUnknown() {
xxx_messageInfo_SaveRequest.DiscardUnknown(m)
}
var xxx_messageInfo_SaveRequest proto.InternalMessageInfo
func (m *SaveRequest) GetPlaces() []*Location {
if m != nil {
return m.Places
}
return nil
}
type SaveResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SaveResponse) Reset() { *m = SaveResponse{} }
func (m *SaveResponse) String() string { return proto.CompactTextString(m) }
func (*SaveResponse) ProtoMessage() {}
func (*SaveResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_3b635ff9d2e2d652, []int{2}
}
func (m *SaveResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SaveResponse.Unmarshal(m, b)
}
func (m *SaveResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SaveResponse.Marshal(b, m, deterministic)
}
func (m *SaveResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_SaveResponse.Merge(m, src)
}
func (m *SaveResponse) XXX_Size() int {
return xxx_messageInfo_SaveResponse.Size(m)
}
func (m *SaveResponse) XXX_DiscardUnknown() {
xxx_messageInfo_SaveResponse.DiscardUnknown(m)
}
var xxx_messageInfo_SaveResponse proto.InternalMessageInfo
type LastRequest struct {
Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *LastRequest) Reset() { *m = LastRequest{} }
func (m *LastRequest) String() string { return proto.CompactTextString(m) }
func (*LastRequest) ProtoMessage() {}
func (*LastRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_3b635ff9d2e2d652, []int{3}
}
func (m *LastRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_LastRequest.Unmarshal(m, b)
}
func (m *LastRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_LastRequest.Marshal(b, m, deterministic)
}
func (m *LastRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_LastRequest.Merge(m, src)
}
func (m *LastRequest) XXX_Size() int {
return xxx_messageInfo_LastRequest.Size(m)
}
func (m *LastRequest) XXX_DiscardUnknown() {
xxx_messageInfo_LastRequest.DiscardUnknown(m)
}
var xxx_messageInfo_LastRequest proto.InternalMessageInfo
func (m *LastRequest) GetIds() []string {
if m != nil {
return m.Ids
}
return nil
}
type ListResponse struct {
Places []*Location `protobuf:"bytes,1,rep,name=places,proto3" json:"places,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ListResponse) Reset() { *m = ListResponse{} }
func (m *ListResponse) String() string { return proto.CompactTextString(m) }
func (*ListResponse) ProtoMessage() {}
func (*ListResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_3b635ff9d2e2d652, []int{4}
}
func (m *ListResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ListResponse.Unmarshal(m, b)
}
func (m *ListResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ListResponse.Marshal(b, m, deterministic)
}
func (m *ListResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ListResponse.Merge(m, src)
}
func (m *ListResponse) XXX_Size() int {
return xxx_messageInfo_ListResponse.Size(m)
}
func (m *ListResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ListResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ListResponse proto.InternalMessageInfo
func (m *ListResponse) GetPlaces() []*Location {
if m != nil {
return m.Places
}
return nil
}
type NearRequest struct {
Latitude *wrappers.DoubleValue `protobuf:"bytes,1,opt,name=latitude,proto3" json:"latitude,omitempty"`
Longitude *wrappers.DoubleValue `protobuf:"bytes,2,opt,name=longitude,proto3" json:"longitude,omitempty"`
// radius to search within, units km
Radius *wrappers.DoubleValue `protobuf:"bytes,3,opt,name=radius,proto3" json:"radius,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *NearRequest) Reset() { *m = NearRequest{} }
func (m *NearRequest) String() string { return proto.CompactTextString(m) }
func (*NearRequest) ProtoMessage() {}
func (*NearRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_3b635ff9d2e2d652, []int{5}
}
func (m *NearRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_NearRequest.Unmarshal(m, b)
}
func (m *NearRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_NearRequest.Marshal(b, m, deterministic)
}
func (m *NearRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_NearRequest.Merge(m, src)
}
func (m *NearRequest) XXX_Size() int {
return xxx_messageInfo_NearRequest.Size(m)
}
func (m *NearRequest) XXX_DiscardUnknown() {
xxx_messageInfo_NearRequest.DiscardUnknown(m)
}
var xxx_messageInfo_NearRequest proto.InternalMessageInfo
func (m *NearRequest) GetLatitude() *wrappers.DoubleValue {
if m != nil {
return m.Latitude
}
return nil
}
func (m *NearRequest) GetLongitude() *wrappers.DoubleValue {
if m != nil {
return m.Longitude
}
return nil
}
func (m *NearRequest) GetRadius() *wrappers.DoubleValue {
if m != nil {
return m.Radius
}
return nil
}
type ReadRequest struct {
Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"`
After *timestamp.Timestamp `protobuf:"bytes,2,opt,name=after,proto3" json:"after,omitempty"`
Before *timestamp.Timestamp `protobuf:"bytes,3,opt,name=before,proto3" json:"before,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ReadRequest) Reset() { *m = ReadRequest{} }
func (m *ReadRequest) String() string { return proto.CompactTextString(m) }
func (*ReadRequest) ProtoMessage() {}
func (*ReadRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_3b635ff9d2e2d652, []int{6}
}
func (m *ReadRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ReadRequest.Unmarshal(m, b)
}
func (m *ReadRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ReadRequest.Marshal(b, m, deterministic)
}
func (m *ReadRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ReadRequest.Merge(m, src)
}
func (m *ReadRequest) XXX_Size() int {
return xxx_messageInfo_ReadRequest.Size(m)
}
func (m *ReadRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ReadRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ReadRequest proto.InternalMessageInfo
func (m *ReadRequest) GetIds() []string {
if m != nil {
return m.Ids
}
return nil
}
func (m *ReadRequest) GetAfter() *timestamp.Timestamp {
if m != nil {
return m.After
}
return nil
}
func (m *ReadRequest) GetBefore() *timestamp.Timestamp {
if m != nil {
return m.Before
}
return nil
}
func init() {
proto.RegisterType((*Location)(nil), "places.Location")
proto.RegisterMapType((map[string]string)(nil), "places.Location.MetadataEntry")
proto.RegisterType((*SaveRequest)(nil), "places.SaveRequest")
proto.RegisterType((*SaveResponse)(nil), "places.SaveResponse")
proto.RegisterType((*LastRequest)(nil), "places.LastRequest")
proto.RegisterType((*ListResponse)(nil), "places.ListResponse")
proto.RegisterType((*NearRequest)(nil), "places.NearRequest")
proto.RegisterType((*ReadRequest)(nil), "places.ReadRequest")
}
func init() { proto.RegisterFile("proto/places.proto", fileDescriptor_3b635ff9d2e2d652) }
var fileDescriptor_3b635ff9d2e2d652 = []byte{
// 460 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x92, 0xcd, 0x6e, 0xd3, 0x40,
0x14, 0x85, 0x19, 0x3b, 0xb1, 0x92, 0xeb, 0x52, 0x45, 0x97, 0x2e, 0x2c, 0x0b, 0xb5, 0x91, 0x57,
0x59, 0x39, 0x28, 0x45, 0x22, 0x0a, 0x5b, 0xd8, 0x05, 0x84, 0x0c, 0x62, 0x3f, 0xa9, 0x6f, 0x22,
0x0b, 0xc7, 0x63, 0xec, 0x71, 0x51, 0x1f, 0x80, 0x87, 0xe2, 0x49, 0xd8, 0xf0, 0x30, 0x68, 0x7e,
0x1c, 0xbb, 0x45, 0x8d, 0x1a, 0x76, 0x33, 0xe3, 0xf3, 0x5d, 0x9f, 0x39, 0x67, 0x00, 0xcb, 0x4a,
0x48, 0x31, 0x2f, 0x73, 0x7e, 0x43, 0x75, 0xac, 0x37, 0xe8, 0x99, 0x5d, 0x78, 0xb5, 0x13, 0x62,
0x97, 0xd3, 0x5c, 0x9f, 0x6e, 0x9a, 0xed, 0x5c, 0x66, 0x7b, 0xaa, 0x25, 0xdf, 0x97, 0x46, 0x18,
0x5e, 0x3e, 0x14, 0xfc, 0xa8, 0x78, 0x59, 0x52, 0x65, 0x07, 0x45, 0xbf, 0x1d, 0x18, 0xad, 0xc5,
0x0d, 0x97, 0x99, 0x28, 0xf0, 0x1c, 0x9c, 0x2c, 0x0d, 0xd8, 0x94, 0xcd, 0xc6, 0x89, 0x93, 0xa5,
0x88, 0x30, 0x28, 0xf8, 0x9e, 0x02, 0x47, 0x9f, 0xe8, 0x35, 0xae, 0x60, 0xb4, 0x27, 0xc9, 0x53,
0x2e, 0x79, 0xe0, 0x4e, 0xdd, 0x99, 0xbf, 0xb8, 0x8c, 0xad, 0xb5, 0x76, 0x4e, 0xfc, 0xc1, 0x0a,
0xde, 0x17, 0xb2, 0xba, 0x4b, 0x0e, 0x7a, 0x5c, 0xc2, 0xf8, 0xe0, 0x2f, 0x18, 0x4c, 0xd9, 0xcc,
0x5f, 0x84, 0xb1, 0x31, 0x18, 0xb7, 0x06, 0xe3, 0x2f, 0xad, 0x22, 0xe9, 0xc4, 0xb8, 0x84, 0x51,
0xce, 0x65, 0x26, 0x9b, 0x94, 0x82, 0xa1, 0x06, 0x5f, 0xfe, 0x03, 0xbe, 0x13, 0xcd, 0x26, 0xa7,
0xaf, 0x3c, 0x6f, 0x28, 0x39, 0xa8, 0x71, 0x05, 0xe3, 0x5c, 0x14, 0x3b, 0x83, 0x7a, 0x4f, 0x40,
0x3b, 0x79, 0xf8, 0x16, 0x9e, 0xdf, 0xbb, 0x0a, 0x4e, 0xc0, 0xfd, 0x46, 0x77, 0x36, 0x21, 0xb5,
0xc4, 0x0b, 0x18, 0xde, 0x2a, 0xcc, 0x66, 0x64, 0x36, 0x2b, 0x67, 0xc9, 0xa2, 0x37, 0xe0, 0x7f,
0xe6, 0xb7, 0x94, 0xd0, 0xf7, 0x86, 0x6a, 0x89, 0x33, 0xb0, 0x9d, 0x05, 0x4c, 0xa7, 0x36, 0x79,
0x98, 0x5a, 0x62, 0xbf, 0x47, 0xe7, 0x70, 0x66, 0xc0, 0xba, 0x14, 0x45, 0x4d, 0xd1, 0x15, 0xf8,
0x6b, 0x5e, 0xcb, 0x76, 0xd0, 0x04, 0xdc, 0x2c, 0x35, 0x53, 0xc6, 0x89, 0x5a, 0x46, 0x4b, 0x38,
0x5b, 0x67, 0x4a, 0x60, 0x80, 0x13, 0x7e, 0xf5, 0x8b, 0x81, 0xff, 0x91, 0x78, 0xd5, 0xce, 0xee,
0xc7, 0xcc, 0xfe, 0x3f, 0x66, 0xe7, 0xa4, 0x98, 0xf1, 0x35, 0x78, 0x15, 0x4f, 0xb3, 0xa6, 0x0e,
0xdc, 0x27, 0x80, 0x56, 0x1b, 0xfd, 0x64, 0xe0, 0x27, 0xc4, 0xd3, 0x47, 0x73, 0xc1, 0x57, 0x30,
0xe4, 0x5b, 0x49, 0x95, 0xf5, 0x73, 0xec, 0xa9, 0x19, 0x21, 0x2e, 0xc0, 0xdb, 0xd0, 0x56, 0x54,
0x64, 0x9d, 0x1c, 0x43, 0xac, 0x72, 0xf1, 0x87, 0x81, 0xf7, 0x49, 0xc7, 0x89, 0xd7, 0x30, 0x50,
0xcd, 0xe1, 0x8b, 0x36, 0xf0, 0xde, 0x03, 0x08, 0x2f, 0xee, 0x1f, 0xda, 0x72, 0x9f, 0x29, 0x48,
0xd5, 0xdb, 0x41, 0xbd, 0xb2, 0x3b, 0xa8, 0x5f, 0xb0, 0x81, 0x54, 0x6f, 0x1d, 0xd4, 0x6b, 0xf1,
0x18, 0xa4, 0x02, 0xeb, 0xa0, 0x5e, 0x7c, 0x8f, 0x41, 0x1b, 0x4f, 0x5f, 0xfd, 0xfa, 0x6f, 0x00,
0x00, 0x00, 0xff, 0xff, 0x7a, 0x9f, 0xaf, 0x48, 0x86, 0x04, 0x00, 0x00,
}

View File

@@ -0,0 +1,154 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/places.proto
package places
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
_ "github.com/golang/protobuf/ptypes/timestamp"
_ "github.com/golang/protobuf/ptypes/wrappers"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Places service
func NewPlacesEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Places service
type PlacesService interface {
// Save a set of places
Save(ctx context.Context, in *SaveRequest, opts ...client.CallOption) (*SaveResponse, error)
// Last places for a set of users
Last(ctx context.Context, in *LastRequest, opts ...client.CallOption) (*ListResponse, error)
// Near returns the places near a point at a given time
Near(ctx context.Context, in *NearRequest, opts ...client.CallOption) (*ListResponse, error)
// Read places for a group of users between two points in time
Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ListResponse, error)
}
type placesService struct {
c client.Client
name string
}
func NewPlacesService(name string, c client.Client) PlacesService {
return &placesService{
c: c,
name: name,
}
}
func (c *placesService) Save(ctx context.Context, in *SaveRequest, opts ...client.CallOption) (*SaveResponse, error) {
req := c.c.NewRequest(c.name, "Places.Save", in)
out := new(SaveResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *placesService) Last(ctx context.Context, in *LastRequest, opts ...client.CallOption) (*ListResponse, error) {
req := c.c.NewRequest(c.name, "Places.Last", in)
out := new(ListResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *placesService) Near(ctx context.Context, in *NearRequest, opts ...client.CallOption) (*ListResponse, error) {
req := c.c.NewRequest(c.name, "Places.Near", in)
out := new(ListResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *placesService) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ListResponse, error) {
req := c.c.NewRequest(c.name, "Places.Read", in)
out := new(ListResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Places service
type PlacesHandler interface {
// Save a set of places
Save(context.Context, *SaveRequest, *SaveResponse) error
// Last places for a set of users
Last(context.Context, *LastRequest, *ListResponse) error
// Near returns the places near a point at a given time
Near(context.Context, *NearRequest, *ListResponse) error
// Read places for a group of users between two points in time
Read(context.Context, *ReadRequest, *ListResponse) error
}
func RegisterPlacesHandler(s server.Server, hdlr PlacesHandler, opts ...server.HandlerOption) error {
type places interface {
Save(ctx context.Context, in *SaveRequest, out *SaveResponse) error
Last(ctx context.Context, in *LastRequest, out *ListResponse) error
Near(ctx context.Context, in *NearRequest, out *ListResponse) error
Read(ctx context.Context, in *ReadRequest, out *ListResponse) error
}
type Places struct {
places
}
h := &placesHandler{hdlr}
return s.Handle(s.NewHandler(&Places{h}, opts...))
}
type placesHandler struct {
PlacesHandler
}
func (h *placesHandler) Save(ctx context.Context, in *SaveRequest, out *SaveResponse) error {
return h.PlacesHandler.Save(ctx, in, out)
}
func (h *placesHandler) Last(ctx context.Context, in *LastRequest, out *ListResponse) error {
return h.PlacesHandler.Last(ctx, in, out)
}
func (h *placesHandler) Near(ctx context.Context, in *NearRequest, out *ListResponse) error {
return h.PlacesHandler.Near(ctx, in, out)
}
func (h *placesHandler) Read(ctx context.Context, in *ReadRequest, out *ListResponse) error {
return h.PlacesHandler.Read(ctx, in, out)
}

53
places/proto/places.proto Normal file
View File

@@ -0,0 +1,53 @@
syntax = "proto3";
package places;
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
service Places {
// Save a set of places
rpc Save(SaveRequest) returns (SaveResponse) {}
// Last places for a set of users
rpc Last(LastRequest) returns (ListResponse) {}
// Near returns the places near a point at a given time
rpc Near(NearRequest) returns (ListResponse) {}
// Read places for a group of users between two points in time
rpc Read(ReadRequest) returns (ListResponse) {}
}
message Location {
string id = 1;
string name = 2;
map<string,string> metadata = 3;
google.protobuf.Timestamp timestamp = 4;
google.protobuf.DoubleValue latitude = 5;
google.protobuf.DoubleValue longitude = 6;
}
message SaveRequest {
repeated Location places = 1;
}
message SaveResponse {}
message LastRequest {
repeated string ids = 1;
}
message ListResponse {
repeated Location places = 1;
}
message NearRequest {
google.protobuf.DoubleValue latitude = 1;
google.protobuf.DoubleValue longitude = 2;
// radius to search within, units km
google.protobuf.DoubleValue radius = 3;
}
message ReadRequest {
repeated string ids = 1;
google.protobuf.Timestamp after = 2;
google.protobuf.Timestamp before = 3;
}