Codebase list golang-github-go-kit-kit / 39b1e0c
Merge remote-tracking branch 'upstream/master' Olivier Bregeras 8 years ago
37 changed file(s) with 2869 addition(s) and 285 deletion(s). Raw diff Collapse all Expand all
55 - 1.4.2
66 - 1.5.3
77 - 1.6
8 - tip
8 #- tip
4040
4141 func (r postProfileResponse) error() error { return r.Err }
4242
43 // Regarding errors returned from service methods, we have two options. We can
44 // return the error via the endpoint itself. That makes certain things a
45 // little bit easier, like providing non-200 HTTP responses to the client. But
46 // Go kit assumes that endpoint errors are (or may be treated as) transport-
47 // level errors. For example, an endpoint error will count against a circuit
48 // breaker error count. Therefore, it's almost certainly better to return
49 // service method (business logic) errors in the response object. This means
50 // we have to do a bit more work in the HTTP response encoder to detect e.g. a
51 // not-found error and provide a proper HTTP status code.
43 // Regarding errors returned from service (business logic) methods, we have two
44 // options. We could return the error via the endpoint itself. That makes
45 // certain things a little bit easier, like providing non-200 HTTP responses to
46 // the client. But Go kit assumes that endpoint errors are (or may be treated
47 // as) transport-domain errors. For example, an endpoint error will count
48 // against a circuit breaker error count. Therefore, it's almost certainly
49 // better to return service (business logic) errors in the response object. This
50 // means we have to do a bit more work in the HTTP response encoder to detect
51 // e.g. a not-found error and provide a proper HTTP status code. That work is
52 // done with the errorer interface, in transport.go.
5253
5354 func makePostProfileEndpoint(s ProfileService) endpoint.Endpoint {
5455 return func(ctx context.Context, request interface{}) (response interface{}, err error) {
+0
-77
examples/profilesvc/logging_middleware.go less more
0 package main
1
2 import (
3 "time"
4
5 "golang.org/x/net/context"
6
7 "github.com/go-kit/kit/log"
8 )
9
10 type loggingMiddleware struct {
11 next ProfileService
12 logger log.Logger
13 }
14
15 func (mw loggingMiddleware) PostProfile(ctx context.Context, p Profile) (err error) {
16 defer func(begin time.Time) {
17 mw.logger.Log("method", "PostProfile", "id", p.ID, "took", time.Since(begin), "err", err)
18 }(time.Now())
19 return mw.next.PostProfile(ctx, p)
20 }
21
22 func (mw loggingMiddleware) GetProfile(ctx context.Context, id string) (p Profile, err error) {
23 defer func(begin time.Time) {
24 mw.logger.Log("method", "GetProfile", "id", id, "took", time.Since(begin), "err", err)
25 }(time.Now())
26 return mw.next.GetProfile(ctx, id)
27 }
28
29 func (mw loggingMiddleware) PutProfile(ctx context.Context, id string, p Profile) (err error) {
30 defer func(begin time.Time) {
31 mw.logger.Log("method", "PutProfile", "id", id, "took", time.Since(begin), "err", err)
32 }(time.Now())
33 return mw.next.PutProfile(ctx, id, p)
34 }
35
36 func (mw loggingMiddleware) PatchProfile(ctx context.Context, id string, p Profile) (err error) {
37 defer func(begin time.Time) {
38 mw.logger.Log("method", "PatchProfile", "id", id, "took", time.Since(begin), "err", err)
39 }(time.Now())
40 return mw.next.PatchProfile(ctx, id, p)
41 }
42
43 func (mw loggingMiddleware) DeleteProfile(ctx context.Context, id string) (err error) {
44 defer func(begin time.Time) {
45 mw.logger.Log("method", "DeleteProfile", "id", id, "took", time.Since(begin), "err", err)
46 }(time.Now())
47 return mw.next.DeleteProfile(ctx, id)
48 }
49
50 func (mw loggingMiddleware) GetAddresses(ctx context.Context, profileID string) (addresses []Address, err error) {
51 defer func(begin time.Time) {
52 mw.logger.Log("method", "GetAddresses", "profileID", profileID, "took", time.Since(begin), "err", err)
53 }(time.Now())
54 return mw.next.GetAddresses(ctx, profileID)
55 }
56
57 func (mw loggingMiddleware) GetAddress(ctx context.Context, profileID string, addressID string) (a Address, err error) {
58 defer func(begin time.Time) {
59 mw.logger.Log("method", "GetAddress", "profileID", profileID, "addressID", addressID, "took", time.Since(begin), "err", err)
60 }(time.Now())
61 return mw.next.GetAddress(ctx, profileID, addressID)
62 }
63
64 func (mw loggingMiddleware) PostAddress(ctx context.Context, profileID string, a Address) (err error) {
65 defer func(begin time.Time) {
66 mw.logger.Log("method", "PostAddress", "profileID", profileID, "took", time.Since(begin), "err", err)
67 }(time.Now())
68 return mw.next.PostAddress(ctx, profileID, a)
69 }
70
71 func (mw loggingMiddleware) DeleteAddress(ctx context.Context, profileID string, addressID string) (err error) {
72 defer func(begin time.Time) {
73 mw.logger.Log("method", "DeleteAddress", "profileID", profileID, "addressID", addressID, "took", time.Since(begin), "err", err)
74 }(time.Now())
75 return mw.next.DeleteAddress(ctx, profileID, addressID)
76 }
0 package main
1
2 import (
3 "time"
4
5 "golang.org/x/net/context"
6
7 "github.com/go-kit/kit/log"
8 )
9
10 type loggingMiddleware struct {
11 next ProfileService
12 logger log.Logger
13 }
14
15 func (mw loggingMiddleware) PostProfile(ctx context.Context, p Profile) (err error) {
16 defer func(begin time.Time) {
17 mw.logger.Log("method", "PostProfile", "id", p.ID, "took", time.Since(begin), "err", err)
18 }(time.Now())
19 return mw.next.PostProfile(ctx, p)
20 }
21
22 func (mw loggingMiddleware) GetProfile(ctx context.Context, id string) (p Profile, err error) {
23 defer func(begin time.Time) {
24 mw.logger.Log("method", "GetProfile", "id", id, "took", time.Since(begin), "err", err)
25 }(time.Now())
26 return mw.next.GetProfile(ctx, id)
27 }
28
29 func (mw loggingMiddleware) PutProfile(ctx context.Context, id string, p Profile) (err error) {
30 defer func(begin time.Time) {
31 mw.logger.Log("method", "PutProfile", "id", id, "took", time.Since(begin), "err", err)
32 }(time.Now())
33 return mw.next.PutProfile(ctx, id, p)
34 }
35
36 func (mw loggingMiddleware) PatchProfile(ctx context.Context, id string, p Profile) (err error) {
37 defer func(begin time.Time) {
38 mw.logger.Log("method", "PatchProfile", "id", id, "took", time.Since(begin), "err", err)
39 }(time.Now())
40 return mw.next.PatchProfile(ctx, id, p)
41 }
42
43 func (mw loggingMiddleware) DeleteProfile(ctx context.Context, id string) (err error) {
44 defer func(begin time.Time) {
45 mw.logger.Log("method", "DeleteProfile", "id", id, "took", time.Since(begin), "err", err)
46 }(time.Now())
47 return mw.next.DeleteProfile(ctx, id)
48 }
49
50 func (mw loggingMiddleware) GetAddresses(ctx context.Context, profileID string) (addresses []Address, err error) {
51 defer func(begin time.Time) {
52 mw.logger.Log("method", "GetAddresses", "profileID", profileID, "took", time.Since(begin), "err", err)
53 }(time.Now())
54 return mw.next.GetAddresses(ctx, profileID)
55 }
56
57 func (mw loggingMiddleware) GetAddress(ctx context.Context, profileID string, addressID string) (a Address, err error) {
58 defer func(begin time.Time) {
59 mw.logger.Log("method", "GetAddress", "profileID", profileID, "addressID", addressID, "took", time.Since(begin), "err", err)
60 }(time.Now())
61 return mw.next.GetAddress(ctx, profileID, addressID)
62 }
63
64 func (mw loggingMiddleware) PostAddress(ctx context.Context, profileID string, a Address) (err error) {
65 defer func(begin time.Time) {
66 mw.logger.Log("method", "PostAddress", "profileID", profileID, "took", time.Since(begin), "err", err)
67 }(time.Now())
68 return mw.next.PostAddress(ctx, profileID, a)
69 }
70
71 func (mw loggingMiddleware) DeleteAddress(ctx context.Context, profileID string, addressID string) (err error) {
72 defer func(begin time.Time) {
73 mw.logger.Log("method", "DeleteAddress", "profileID", profileID, "addressID", addressID, "took", time.Since(begin), "err", err)
74 }(time.Now())
75 return mw.next.DeleteAddress(ctx, profileID, addressID)
76 }
+0
-186
examples/profilesvc/profile_service.go less more
0 package main
1
2 import (
3 "errors"
4 "sync"
5
6 "golang.org/x/net/context"
7 )
8
9 // ProfileService is a simple CRUD interface for user profiles.
10 type ProfileService interface {
11 PostProfile(ctx context.Context, p Profile) error
12 GetProfile(ctx context.Context, id string) (Profile, error)
13 PutProfile(ctx context.Context, id string, p Profile) error
14 PatchProfile(ctx context.Context, id string, p Profile) error
15 DeleteProfile(ctx context.Context, id string) error
16 GetAddresses(ctx context.Context, profileID string) ([]Address, error)
17 GetAddress(ctx context.Context, profileID string, addressID string) (Address, error)
18 PostAddress(ctx context.Context, profileID string, a Address) error
19 DeleteAddress(ctx context.Context, profileID string, addressID string) error
20 }
21
22 // Profile represents a single user profile.
23 // ID should be globally unique.
24 type Profile struct {
25 ID string `json:"id"`
26 Name string `json:"name,omitempty"`
27 Addresses []Address `json:"addresses,omitempty"`
28 }
29
30 // Address is a field of a user profile.
31 // ID should be unique within the profile (at a minimum).
32 type Address struct {
33 ID string `json:"id"`
34 Location string `json:"location,omitempty"`
35 }
36
37 var (
38 errInconsistentIDs = errors.New("inconsistent IDs")
39 errAlreadyExists = errors.New("already exists")
40 errNotFound = errors.New("not found")
41 )
42
43 type inmemService struct {
44 mtx sync.RWMutex
45 m map[string]Profile
46 }
47
48 func newInmemService() ProfileService {
49 return &inmemService{
50 m: map[string]Profile{},
51 }
52 }
53
54 func (s *inmemService) PostProfile(ctx context.Context, p Profile) error {
55 s.mtx.Lock()
56 defer s.mtx.Unlock()
57 if _, ok := s.m[p.ID]; ok {
58 return errAlreadyExists // POST = create, don't overwrite
59 }
60 s.m[p.ID] = p
61 return nil
62 }
63
64 func (s *inmemService) GetProfile(ctx context.Context, id string) (Profile, error) {
65 s.mtx.RLock()
66 defer s.mtx.RUnlock()
67 p, ok := s.m[id]
68 if !ok {
69 return Profile{}, errNotFound
70 }
71 return p, nil
72 }
73
74 func (s *inmemService) PutProfile(ctx context.Context, id string, p Profile) error {
75 if id != p.ID {
76 return errInconsistentIDs
77 }
78 s.mtx.Lock()
79 defer s.mtx.Unlock()
80 s.m[id] = p // PUT = create or update
81 return nil
82 }
83
84 func (s *inmemService) PatchProfile(ctx context.Context, id string, p Profile) error {
85 if p.ID != "" && id != p.ID {
86 return errInconsistentIDs
87 }
88
89 s.mtx.Lock()
90 defer s.mtx.Unlock()
91
92 existing, ok := s.m[id]
93 if !ok {
94 return errNotFound // PATCH = update existing, don't create
95 }
96
97 // We assume that it's not possible to PATCH the ID, and that it's not
98 // possible to PATCH any field to its zero value. That is, the zero value
99 // means not specified. The way around this is to use e.g. Name *string in
100 // the Profile definition. But since this is just a demonstrative example,
101 // I'm leaving that out.
102
103 if p.Name != "" {
104 existing.Name = p.Name
105 }
106 if len(p.Addresses) > 0 {
107 existing.Addresses = p.Addresses
108 }
109 s.m[id] = existing
110 return nil
111 }
112
113 func (s *inmemService) DeleteProfile(ctx context.Context, id string) error {
114 s.mtx.Lock()
115 defer s.mtx.Unlock()
116 if _, ok := s.m[id]; !ok {
117 return errNotFound
118 }
119 delete(s.m, id)
120 return nil
121 }
122
123 func (s *inmemService) GetAddresses(ctx context.Context, profileID string) ([]Address, error) {
124 s.mtx.RLock()
125 defer s.mtx.RUnlock()
126 p, ok := s.m[profileID]
127 if !ok {
128 return []Address{}, errNotFound
129 }
130 return p.Addresses, nil
131 }
132
133 func (s *inmemService) GetAddress(ctx context.Context, profileID string, addressID string) (Address, error) {
134 s.mtx.RLock()
135 defer s.mtx.RUnlock()
136 p, ok := s.m[profileID]
137 if !ok {
138 return Address{}, errNotFound
139 }
140 for _, address := range p.Addresses {
141 if address.ID == addressID {
142 return address, nil
143 }
144 }
145 return Address{}, errNotFound
146 }
147
148 func (s *inmemService) PostAddress(ctx context.Context, profileID string, a Address) error {
149 s.mtx.Lock()
150 defer s.mtx.Unlock()
151 p, ok := s.m[profileID]
152 if !ok {
153 return errNotFound
154 }
155 for _, address := range p.Addresses {
156 if address.ID == a.ID {
157 return errAlreadyExists
158 }
159 }
160 p.Addresses = append(p.Addresses, a)
161 s.m[profileID] = p
162 return nil
163 }
164
165 func (s *inmemService) DeleteAddress(ctx context.Context, profileID string, addressID string) error {
166 s.mtx.Lock()
167 defer s.mtx.Unlock()
168 p, ok := s.m[profileID]
169 if !ok {
170 return errNotFound
171 }
172 newAddresses := make([]Address, 0, len(p.Addresses))
173 for _, address := range p.Addresses {
174 if address.ID == addressID {
175 continue // delete
176 }
177 newAddresses = append(newAddresses, address)
178 }
179 if len(newAddresses) == len(p.Addresses) {
180 return errNotFound
181 }
182 p.Addresses = newAddresses
183 s.m[profileID] = p
184 return nil
185 }
0 package main
1
2 import (
3 "errors"
4 "sync"
5
6 "golang.org/x/net/context"
7 )
8
9 // ProfileService is a simple CRUD interface for user profiles.
10 type ProfileService interface {
11 PostProfile(ctx context.Context, p Profile) error
12 GetProfile(ctx context.Context, id string) (Profile, error)
13 PutProfile(ctx context.Context, id string, p Profile) error
14 PatchProfile(ctx context.Context, id string, p Profile) error
15 DeleteProfile(ctx context.Context, id string) error
16 GetAddresses(ctx context.Context, profileID string) ([]Address, error)
17 GetAddress(ctx context.Context, profileID string, addressID string) (Address, error)
18 PostAddress(ctx context.Context, profileID string, a Address) error
19 DeleteAddress(ctx context.Context, profileID string, addressID string) error
20 }
21
22 // Profile represents a single user profile.
23 // ID should be globally unique.
24 type Profile struct {
25 ID string `json:"id"`
26 Name string `json:"name,omitempty"`
27 Addresses []Address `json:"addresses,omitempty"`
28 }
29
30 // Address is a field of a user profile.
31 // ID should be unique within the profile (at a minimum).
32 type Address struct {
33 ID string `json:"id"`
34 Location string `json:"location,omitempty"`
35 }
36
37 var (
38 errInconsistentIDs = errors.New("inconsistent IDs")
39 errAlreadyExists = errors.New("already exists")
40 errNotFound = errors.New("not found")
41 )
42
43 type inmemService struct {
44 mtx sync.RWMutex
45 m map[string]Profile
46 }
47
48 func newInmemService() ProfileService {
49 return &inmemService{
50 m: map[string]Profile{},
51 }
52 }
53
54 func (s *inmemService) PostProfile(ctx context.Context, p Profile) error {
55 s.mtx.Lock()
56 defer s.mtx.Unlock()
57 if _, ok := s.m[p.ID]; ok {
58 return errAlreadyExists // POST = create, don't overwrite
59 }
60 s.m[p.ID] = p
61 return nil
62 }
63
64 func (s *inmemService) GetProfile(ctx context.Context, id string) (Profile, error) {
65 s.mtx.RLock()
66 defer s.mtx.RUnlock()
67 p, ok := s.m[id]
68 if !ok {
69 return Profile{}, errNotFound
70 }
71 return p, nil
72 }
73
74 func (s *inmemService) PutProfile(ctx context.Context, id string, p Profile) error {
75 if id != p.ID {
76 return errInconsistentIDs
77 }
78 s.mtx.Lock()
79 defer s.mtx.Unlock()
80 s.m[id] = p // PUT = create or update
81 return nil
82 }
83
84 func (s *inmemService) PatchProfile(ctx context.Context, id string, p Profile) error {
85 if p.ID != "" && id != p.ID {
86 return errInconsistentIDs
87 }
88
89 s.mtx.Lock()
90 defer s.mtx.Unlock()
91
92 existing, ok := s.m[id]
93 if !ok {
94 return errNotFound // PATCH = update existing, don't create
95 }
96
97 // We assume that it's not possible to PATCH the ID, and that it's not
98 // possible to PATCH any field to its zero value. That is, the zero value
99 // means not specified. The way around this is to use e.g. Name *string in
100 // the Profile definition. But since this is just a demonstrative example,
101 // I'm leaving that out.
102
103 if p.Name != "" {
104 existing.Name = p.Name
105 }
106 if len(p.Addresses) > 0 {
107 existing.Addresses = p.Addresses
108 }
109 s.m[id] = existing
110 return nil
111 }
112
113 func (s *inmemService) DeleteProfile(ctx context.Context, id string) error {
114 s.mtx.Lock()
115 defer s.mtx.Unlock()
116 if _, ok := s.m[id]; !ok {
117 return errNotFound
118 }
119 delete(s.m, id)
120 return nil
121 }
122
123 func (s *inmemService) GetAddresses(ctx context.Context, profileID string) ([]Address, error) {
124 s.mtx.RLock()
125 defer s.mtx.RUnlock()
126 p, ok := s.m[profileID]
127 if !ok {
128 return []Address{}, errNotFound
129 }
130 return p.Addresses, nil
131 }
132
133 func (s *inmemService) GetAddress(ctx context.Context, profileID string, addressID string) (Address, error) {
134 s.mtx.RLock()
135 defer s.mtx.RUnlock()
136 p, ok := s.m[profileID]
137 if !ok {
138 return Address{}, errNotFound
139 }
140 for _, address := range p.Addresses {
141 if address.ID == addressID {
142 return address, nil
143 }
144 }
145 return Address{}, errNotFound
146 }
147
148 func (s *inmemService) PostAddress(ctx context.Context, profileID string, a Address) error {
149 s.mtx.Lock()
150 defer s.mtx.Unlock()
151 p, ok := s.m[profileID]
152 if !ok {
153 return errNotFound
154 }
155 for _, address := range p.Addresses {
156 if address.ID == a.ID {
157 return errAlreadyExists
158 }
159 }
160 p.Addresses = append(p.Addresses, a)
161 s.m[profileID] = p
162 return nil
163 }
164
165 func (s *inmemService) DeleteAddress(ctx context.Context, profileID string, addressID string) error {
166 s.mtx.Lock()
167 defer s.mtx.Unlock()
168 p, ok := s.m[profileID]
169 if !ok {
170 return errNotFound
171 }
172 newAddresses := make([]Address, 0, len(p.Addresses))
173 for _, address := range p.Addresses {
174 if address.ID == addressID {
175 continue // delete
176 }
177 newAddresses = append(newAddresses, address)
178 }
179 if len(newAddresses) == len(p.Addresses) {
180 return errNotFound
181 }
182 p.Addresses = newAddresses
183 s.m[profileID] = p
184 return nil
185 }
218218 // errorer is implemented by all concrete response types. It allows us to
219219 // change the HTTP response code without needing to trigger an endpoint
220220 // (transport-level) error. For more information, read the big comment in
221 // endpoint.go.
221 // endpoints.go.
222222 type errorer interface {
223223 error() error
224224 }
238238 }
239239
240240 func encodeError(w stdhttp.ResponseWriter, err error) {
241 if err == nil {
242 panic("encodeError with nil error")
243 }
241244 w.WriteHeader(codeFrom(err))
242245 json.NewEncoder(w).Encode(map[string]interface{}{
243246 "error": err.Error(),
246249
247250 func codeFrom(err error) int {
248251 switch err {
249 case nil:
250 return stdhttp.StatusOK
251252 case errNotFound:
252253 return stdhttp.StatusNotFound
253254 case errAlreadyExists, errInconsistentIDs:
0 # shipping
1
2 This example demonstrates a more real-world application consisting of multiple services.
3
4 ## Description
5
6 The implementation is based on the container shipping domain from the [Domain Driven Design](http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) book by Eric Evans, which was [originally](http://dddsample.sourceforge.net/) implemented in Java but has since been ported to Go. This example is a somewhat stripped down version to demonstrate the use of Go kit. The [original Go application](https://github.com/marcusolsson/goddd) is maintained separately and accompanied by an [AngularJS application](https://github.com/marcusolsson/dddelivery-angularjs) as well as a mock [routing service](https://github.com/marcusolsson/pathfinder).
7
8 ### Organization
9
10 The application consists of three application services, `booking`, `handling` and `tracking`. Each of these is an individual Go kit service as seen in previous examples.
11
12 - __booking__ - used by the shipping company to book and route cargos.
13 - __handling__ - used by our staff around the world to register whenever the cargo has been received, loaded etc.
14 - __tracking__ - used by the customer to track the cargo along the route
15
16 There are also a few pure domain packages that contain some intricate business-logic. They provide domain objects and services that are used by each application service to provide interesting use-cases for the user.
17
18 `repository` contains in-memory implementations for the repositories found in the domain packages.
19
20 The `routing` package provides a _domain service_ that is used to query an external application for possible routes.
21
22 ## Contributing
23
24 As with all Go kit examples you are more than welcome to contribute. If you do however, please consider contributing back to the original project as well.
0 package booking
1
2 import (
3 "time"
4
5 "golang.org/x/net/context"
6
7 "github.com/go-kit/kit/endpoint"
8 "github.com/go-kit/kit/examples/shipping/cargo"
9 "github.com/go-kit/kit/examples/shipping/location"
10 )
11
12 type bookCargoRequest struct {
13 Origin location.UNLocode
14 Destination location.UNLocode
15 ArrivalDeadline time.Time
16 }
17
18 type bookCargoResponse struct {
19 ID cargo.TrackingID `json:"tracking_id,omitempty"`
20 Err error `json:"error,omitempty"`
21 }
22
23 func (r bookCargoResponse) error() error { return r.Err }
24
25 func makeBookCargoEndpoint(s Service) endpoint.Endpoint {
26 return func(ctx context.Context, request interface{}) (interface{}, error) {
27 req := request.(bookCargoRequest)
28 id, err := s.BookNewCargo(req.Origin, req.Destination, req.ArrivalDeadline)
29 return bookCargoResponse{ID: id, Err: err}, nil
30 }
31 }
32
33 type loadCargoRequest struct {
34 ID cargo.TrackingID
35 }
36
37 type loadCargoResponse struct {
38 Cargo *Cargo `json:"cargo,omitempty"`
39 Err error `json:"error,omitempty"`
40 }
41
42 func (r loadCargoResponse) error() error { return r.Err }
43
44 func makeLoadCargoEndpoint(bs Service) endpoint.Endpoint {
45 return func(ctx context.Context, request interface{}) (interface{}, error) {
46 req := request.(loadCargoRequest)
47 c, err := bs.LoadCargo(req.ID)
48 return loadCargoResponse{Cargo: &c, Err: err}, nil
49 }
50 }
51
52 type requestRoutesRequest struct {
53 ID cargo.TrackingID
54 }
55
56 type requestRoutesResponse struct {
57 Routes []cargo.Itinerary `json:"routes,omitempty"`
58 Err error `json:"error,omitempty"`
59 }
60
61 func (r requestRoutesResponse) error() error { return r.Err }
62
63 func makeRequestRoutesEndpoint(s Service) endpoint.Endpoint {
64 return func(ctx context.Context, request interface{}) (interface{}, error) {
65 req := request.(requestRoutesRequest)
66 itin := s.RequestPossibleRoutesForCargo(req.ID)
67 return requestRoutesResponse{Routes: itin, Err: nil}, nil
68 }
69 }
70
71 type assignToRouteRequest struct {
72 ID cargo.TrackingID
73 Itinerary cargo.Itinerary
74 }
75
76 type assignToRouteResponse struct {
77 Err error `json:"error,omitempty"`
78 }
79
80 func (r assignToRouteResponse) error() error { return r.Err }
81
82 func makeAssignToRouteEndpoint(s Service) endpoint.Endpoint {
83 return func(ctx context.Context, request interface{}) (interface{}, error) {
84 req := request.(assignToRouteRequest)
85 err := s.AssignCargoToRoute(req.ID, req.Itinerary)
86 return assignToRouteResponse{Err: err}, nil
87 }
88 }
89
90 type changeDestinationRequest struct {
91 ID cargo.TrackingID
92 Destination location.UNLocode
93 }
94
95 type changeDestinationResponse struct {
96 Err error `json:"error,omitempty"`
97 }
98
99 func (r changeDestinationResponse) error() error { return r.Err }
100
101 func makeChangeDestinationEndpoint(s Service) endpoint.Endpoint {
102 return func(ctx context.Context, request interface{}) (interface{}, error) {
103 req := request.(changeDestinationRequest)
104 err := s.ChangeDestination(req.ID, req.Destination)
105 return changeDestinationResponse{Err: err}, nil
106 }
107 }
108
109 type listCargosRequest struct{}
110
111 type listCargosResponse struct {
112 Cargos []Cargo `json:"cargos,omitempty"`
113 Err error `json:"error,omitempty"`
114 }
115
116 func (r listCargosResponse) error() error { return r.Err }
117
118 func makeListCargosEndpoint(s Service) endpoint.Endpoint {
119 return func(ctx context.Context, request interface{}) (interface{}, error) {
120 _ = request.(listCargosRequest)
121 return listCargosResponse{Cargos: s.Cargos(), Err: nil}, nil
122 }
123 }
124
125 type listLocationsRequest struct {
126 }
127
128 type listLocationsResponse struct {
129 Locations []Location `json:"locations,omitempty"`
130 Err error `json:"error,omitempty"`
131 }
132
133 func makeListLocationsEndpoint(s Service) endpoint.Endpoint {
134 return func(ctx context.Context, request interface{}) (interface{}, error) {
135 _ = request.(listLocationsRequest)
136 return listLocationsResponse{Locations: s.Locations(), Err: nil}, nil
137 }
138 }
0 package booking
1
2 import (
3 "time"
4
5 "github.com/go-kit/kit/examples/shipping/cargo"
6 "github.com/go-kit/kit/examples/shipping/location"
7 "github.com/go-kit/kit/log"
8 )
9
10 type loggingService struct {
11 logger log.Logger
12 Service
13 }
14
15 // NewLoggingService returns a new instance of a logging Service.
16 func NewLoggingService(logger log.Logger, s Service) Service {
17 return &loggingService{logger, s}
18 }
19
20 func (s *loggingService) BookNewCargo(origin location.UNLocode, destination location.UNLocode, arrivalDeadline time.Time) (id cargo.TrackingID, err error) {
21 defer func(begin time.Time) {
22 s.logger.Log(
23 "method", "book",
24 "origin", origin,
25 "destination", destination,
26 "arrival_deadline", arrivalDeadline,
27 "took", time.Since(begin),
28 "err", err,
29 )
30 }(time.Now())
31 return s.Service.BookNewCargo(origin, destination, arrivalDeadline)
32 }
33
34 func (s *loggingService) LoadCargo(id cargo.TrackingID) (c Cargo, err error) {
35 defer func(begin time.Time) {
36 s.logger.Log(
37 "method", "load",
38 "tracking_id", id,
39 "took", time.Since(begin),
40 "err", err,
41 )
42 }(time.Now())
43 return s.Service.LoadCargo(id)
44 }
45
46 func (s *loggingService) RequestPossibleRoutesForCargo(id cargo.TrackingID) []cargo.Itinerary {
47 defer func(begin time.Time) {
48 s.logger.Log(
49 "method", "request_routes",
50 "tracking_id", id,
51 "took", time.Since(begin),
52 )
53 }(time.Now())
54 return s.Service.RequestPossibleRoutesForCargo(id)
55 }
56
57 func (s *loggingService) AssignCargoToRoute(id cargo.TrackingID, itinerary cargo.Itinerary) (err error) {
58 defer func(begin time.Time) {
59 s.logger.Log(
60 "method", "assign_to_route",
61 "tracking_id", id,
62 "took", time.Since(begin),
63 "err", err,
64 )
65 }(time.Now())
66 return s.Service.AssignCargoToRoute(id, itinerary)
67 }
68
69 func (s *loggingService) ChangeDestination(id cargo.TrackingID, l location.UNLocode) (err error) {
70 defer func(begin time.Time) {
71 s.logger.Log(
72 "method", "change_destination",
73 "tracking_id", id,
74 "destination", l,
75 "took", time.Since(begin),
76 "err", err,
77 )
78 }(time.Now())
79 return s.Service.ChangeDestination(id, l)
80 }
81
82 func (s *loggingService) Cargos() []Cargo {
83 defer func(begin time.Time) {
84 s.logger.Log(
85 "method", "list_cargos",
86 "took", time.Since(begin),
87 )
88 }(time.Now())
89 return s.Service.Cargos()
90 }
91
92 func (s *loggingService) Locations() []Location {
93 defer func(begin time.Time) {
94 s.logger.Log(
95 "method", "list_locations",
96 "took", time.Since(begin),
97 )
98 }(time.Now())
99 return s.Service.Locations()
100 }
0 // Package booking provides the use-case of booking a cargo. Used by views
1 // facing an administrator.
2 package booking
3
4 import (
5 "errors"
6 "time"
7
8 "github.com/go-kit/kit/examples/shipping/cargo"
9 "github.com/go-kit/kit/examples/shipping/location"
10 "github.com/go-kit/kit/examples/shipping/routing"
11 )
12
13 // ErrInvalidArgument is returned when one or more arguments are invalid.
14 var ErrInvalidArgument = errors.New("invalid argument")
15
16 // Service is the interface that provides booking methods.
17 type Service interface {
18 // BookNewCargo registers a new cargo in the tracking system, not yet
19 // routed.
20 BookNewCargo(origin location.UNLocode, destination location.UNLocode, arrivalDeadline time.Time) (cargo.TrackingID, error)
21
22 // LoadCargo returns a read model of a cargo.
23 LoadCargo(trackingID cargo.TrackingID) (Cargo, error)
24
25 // RequestPossibleRoutesForCargo requests a list of itineraries describing
26 // possible routes for this cargo.
27 RequestPossibleRoutesForCargo(trackingID cargo.TrackingID) []cargo.Itinerary
28
29 // AssignCargoToRoute assigns a cargo to the route specified by the
30 // itinerary.
31 AssignCargoToRoute(trackingID cargo.TrackingID, itinerary cargo.Itinerary) error
32
33 // ChangeDestination changes the destination of a cargo.
34 ChangeDestination(trackingID cargo.TrackingID, unLocode location.UNLocode) error
35
36 // Cargos returns a list of all cargos that have been booked.
37 Cargos() []Cargo
38
39 // Locations returns a list of registered locations.
40 Locations() []Location
41 }
42
43 type service struct {
44 cargoRepository cargo.Repository
45 locationRepository location.Repository
46 routingService routing.Service
47 handlingEventRepository cargo.HandlingEventRepository
48 }
49
50 func (s *service) AssignCargoToRoute(id cargo.TrackingID, itinerary cargo.Itinerary) error {
51 if id == "" || len(itinerary.Legs) == 0 {
52 return ErrInvalidArgument
53 }
54
55 c, err := s.cargoRepository.Find(id)
56 if err != nil {
57 return err
58 }
59
60 c.AssignToRoute(itinerary)
61
62 if err := s.cargoRepository.Store(c); err != nil {
63 return err
64 }
65
66 return nil
67 }
68
69 func (s *service) BookNewCargo(origin, destination location.UNLocode, arrivalDeadline time.Time) (cargo.TrackingID, error) {
70 if origin == "" || destination == "" || arrivalDeadline.IsZero() {
71 return "", ErrInvalidArgument
72 }
73
74 id := cargo.NextTrackingID()
75 rs := cargo.RouteSpecification{
76 Origin: origin,
77 Destination: destination,
78 ArrivalDeadline: arrivalDeadline,
79 }
80
81 c := cargo.New(id, rs)
82
83 if err := s.cargoRepository.Store(c); err != nil {
84 return "", err
85 }
86
87 return c.TrackingID, nil
88 }
89
90 func (s *service) LoadCargo(trackingID cargo.TrackingID) (Cargo, error) {
91 if trackingID == "" {
92 return Cargo{}, ErrInvalidArgument
93 }
94
95 c, err := s.cargoRepository.Find(trackingID)
96 if err != nil {
97 return Cargo{}, err
98 }
99
100 return assemble(c, s.handlingEventRepository), nil
101 }
102
103 func (s *service) ChangeDestination(id cargo.TrackingID, destination location.UNLocode) error {
104 if id == "" || destination == "" {
105 return ErrInvalidArgument
106 }
107
108 c, err := s.cargoRepository.Find(id)
109 if err != nil {
110 return err
111 }
112
113 l, err := s.locationRepository.Find(destination)
114 if err != nil {
115 return err
116 }
117
118 c.SpecifyNewRoute(cargo.RouteSpecification{
119 Origin: c.Origin,
120 Destination: l.UNLocode,
121 ArrivalDeadline: c.RouteSpecification.ArrivalDeadline,
122 })
123
124 if err := s.cargoRepository.Store(c); err != nil {
125 return err
126 }
127
128 return nil
129 }
130
131 func (s *service) RequestPossibleRoutesForCargo(id cargo.TrackingID) []cargo.Itinerary {
132 if id == "" {
133 return nil
134 }
135
136 c, err := s.cargoRepository.Find(id)
137 if err != nil {
138 return []cargo.Itinerary{}
139 }
140
141 return s.routingService.FetchRoutesForSpecification(c.RouteSpecification)
142 }
143
144 func (s *service) Cargos() []Cargo {
145 var result []Cargo
146 for _, c := range s.cargoRepository.FindAll() {
147 result = append(result, assemble(c, s.handlingEventRepository))
148 }
149 return result
150 }
151
152 func (s *service) Locations() []Location {
153 var result []Location
154 for _, v := range s.locationRepository.FindAll() {
155 result = append(result, Location{
156 UNLocode: string(v.UNLocode),
157 Name: v.Name,
158 })
159 }
160 return result
161 }
162
163 // NewService creates a booking service with necessary dependencies.
164 func NewService(cr cargo.Repository, lr location.Repository, her cargo.HandlingEventRepository, rs routing.Service) Service {
165 return &service{
166 cargoRepository: cr,
167 locationRepository: lr,
168 handlingEventRepository: her,
169 routingService: rs,
170 }
171 }
172
173 // Location is a read model for booking views.
174 type Location struct {
175 UNLocode string `json:"locode"`
176 Name string `json:"name"`
177 }
178
179 // Cargo is a read model for booking views.
180 type Cargo struct {
181 ArrivalDeadline time.Time `json:"arrival_deadline"`
182 Destination string `json:"destination"`
183 Legs []cargo.Leg `json:"legs,omitempty"`
184 Misrouted bool `json:"misrouted"`
185 Origin string `json:"origin"`
186 Routed bool `json:"routed"`
187 TrackingID string `json:"tracking_id"`
188 }
189
190 func assemble(c *cargo.Cargo, her cargo.HandlingEventRepository) Cargo {
191 return Cargo{
192 TrackingID: string(c.TrackingID),
193 Origin: string(c.Origin),
194 Destination: string(c.RouteSpecification.Destination),
195 Misrouted: c.Delivery.RoutingStatus == cargo.Misrouted,
196 Routed: !c.Itinerary.IsEmpty(),
197 ArrivalDeadline: c.RouteSpecification.ArrivalDeadline,
198 Legs: c.Itinerary.Legs,
199 }
200 }
0 package booking
1
2 import (
3 "encoding/json"
4 "errors"
5 "net/http"
6 "time"
7
8 "github.com/gorilla/mux"
9 "golang.org/x/net/context"
10
11 "github.com/go-kit/kit/examples/shipping/cargo"
12 "github.com/go-kit/kit/examples/shipping/location"
13 kitlog "github.com/go-kit/kit/log"
14 kithttp "github.com/go-kit/kit/transport/http"
15 )
16
17 // MakeHandler returns a handler for the booking service.
18 func MakeHandler(ctx context.Context, bs Service, logger kitlog.Logger) http.Handler {
19 opts := []kithttp.ServerOption{
20 kithttp.ServerErrorLogger(logger),
21 kithttp.ServerErrorEncoder(encodeError),
22 }
23
24 bookCargoHandler := kithttp.NewServer(
25 ctx,
26 makeBookCargoEndpoint(bs),
27 decodeBookCargoRequest,
28 encodeResponse,
29 opts...,
30 )
31 loadCargoHandler := kithttp.NewServer(
32 ctx,
33 makeLoadCargoEndpoint(bs),
34 decodeLoadCargoRequest,
35 encodeResponse,
36 opts...,
37 )
38 requestRoutesHandler := kithttp.NewServer(
39 ctx,
40 makeRequestRoutesEndpoint(bs),
41 decodeRequestRoutesRequest,
42 encodeResponse,
43 opts...,
44 )
45 assignToRouteHandler := kithttp.NewServer(
46 ctx,
47 makeAssignToRouteEndpoint(bs),
48 decodeAssignToRouteRequest,
49 encodeResponse,
50 opts...,
51 )
52 changeDestinationHandler := kithttp.NewServer(
53 ctx,
54 makeChangeDestinationEndpoint(bs),
55 decodeChangeDestinationRequest,
56 encodeResponse,
57 opts...,
58 )
59 listCargosHandler := kithttp.NewServer(
60 ctx,
61 makeListCargosEndpoint(bs),
62 decodeListCargosRequest,
63 encodeResponse,
64 opts...,
65 )
66 listLocationsHandler := kithttp.NewServer(
67 ctx,
68 makeListLocationsEndpoint(bs),
69 decodeListLocationsRequest,
70 encodeResponse,
71 opts...,
72 )
73
74 r := mux.NewRouter()
75
76 r.Handle("/booking/v1/cargos", bookCargoHandler).Methods("POST")
77 r.Handle("/booking/v1/cargos", listCargosHandler).Methods("GET")
78 r.Handle("/booking/v1/cargos/{id}", loadCargoHandler).Methods("GET")
79 r.Handle("/booking/v1/cargos/{id}/request_routes", requestRoutesHandler).Methods("GET")
80 r.Handle("/booking/v1/cargos/{id}/assign_to_route", assignToRouteHandler).Methods("POST")
81 r.Handle("/booking/v1/cargos/{id}/change_destination", changeDestinationHandler).Methods("POST")
82 r.Handle("/booking/v1/locations", listLocationsHandler).Methods("GET")
83 r.Handle("/booking/v1/docs", http.StripPrefix("/booking/v1/docs", http.FileServer(http.Dir("booking/docs"))))
84
85 return r
86 }
87
88 var errBadRoute = errors.New("bad route")
89
90 func decodeBookCargoRequest(r *http.Request) (interface{}, error) {
91 var body struct {
92 Origin string `json:"origin"`
93 Destination string `json:"destination"`
94 ArrivalDeadline time.Time `json:"arrival_deadline"`
95 }
96
97 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
98 return nil, err
99 }
100
101 return bookCargoRequest{
102 Origin: location.UNLocode(body.Origin),
103 Destination: location.UNLocode(body.Destination),
104 ArrivalDeadline: body.ArrivalDeadline,
105 }, nil
106 }
107
108 func decodeLoadCargoRequest(r *http.Request) (interface{}, error) {
109 vars := mux.Vars(r)
110 id, ok := vars["id"]
111 if !ok {
112 return nil, errBadRoute
113 }
114 return loadCargoRequest{ID: cargo.TrackingID(id)}, nil
115 }
116
117 func decodeRequestRoutesRequest(r *http.Request) (interface{}, error) {
118 vars := mux.Vars(r)
119 id, ok := vars["id"]
120 if !ok {
121 return nil, errBadRoute
122 }
123 return requestRoutesRequest{ID: cargo.TrackingID(id)}, nil
124 }
125
126 func decodeAssignToRouteRequest(r *http.Request) (interface{}, error) {
127 vars := mux.Vars(r)
128 id, ok := vars["id"]
129 if !ok {
130 return nil, errBadRoute
131 }
132
133 var itinerary cargo.Itinerary
134 if err := json.NewDecoder(r.Body).Decode(&itinerary); err != nil {
135 return nil, err
136 }
137
138 return assignToRouteRequest{
139 ID: cargo.TrackingID(id),
140 Itinerary: itinerary,
141 }, nil
142 }
143
144 func decodeChangeDestinationRequest(r *http.Request) (interface{}, error) {
145 vars := mux.Vars(r)
146 id, ok := vars["id"]
147 if !ok {
148 return nil, errBadRoute
149 }
150
151 var body struct {
152 Destination string `json:"destination"`
153 }
154
155 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
156 return nil, err
157 }
158
159 return changeDestinationRequest{
160 ID: cargo.TrackingID(id),
161 Destination: location.UNLocode(body.Destination),
162 }, nil
163 }
164
165 func decodeListCargosRequest(r *http.Request) (interface{}, error) {
166 return listCargosRequest{}, nil
167 }
168
169 func decodeListLocationsRequest(r *http.Request) (interface{}, error) {
170 return listLocationsRequest{}, nil
171 }
172
173 func encodeResponse(w http.ResponseWriter, response interface{}) error {
174 if e, ok := response.(errorer); ok && e.error() != nil {
175 encodeError(w, e.error())
176 return nil
177 }
178 w.Header().Set("Content-Type", "application/json; charset=utf-8")
179 return json.NewEncoder(w).Encode(response)
180 }
181
182 type errorer interface {
183 error() error
184 }
185
186 // encode errors from business-logic
187 func encodeError(w http.ResponseWriter, err error) {
188 switch err {
189 case cargo.ErrUnknown:
190 w.WriteHeader(http.StatusNotFound)
191 case ErrInvalidArgument:
192 w.WriteHeader(http.StatusBadRequest)
193 default:
194 w.WriteHeader(http.StatusInternalServerError)
195 }
196 w.Header().Set("Content-Type", "application/json; charset=utf-8")
197 json.NewEncoder(w).Encode(map[string]interface{}{
198 "error": err.Error(),
199 })
200 }
0 // Package cargo contains the heart of the domain model.
1 package cargo
2
3 import (
4 "errors"
5 "strings"
6 "time"
7
8 "github.com/pborman/uuid"
9
10 "github.com/go-kit/kit/examples/shipping/location"
11 )
12
13 // TrackingID uniquely identifies a particular cargo.
14 type TrackingID string
15
16 // Cargo is the central class in the domain model.
17 type Cargo struct {
18 TrackingID TrackingID
19 Origin location.UNLocode
20 RouteSpecification RouteSpecification
21 Itinerary Itinerary
22 Delivery Delivery
23 }
24
25 // SpecifyNewRoute specifies a new route for this cargo.
26 func (c *Cargo) SpecifyNewRoute(rs RouteSpecification) {
27 c.RouteSpecification = rs
28 c.Delivery = c.Delivery.UpdateOnRouting(c.RouteSpecification, c.Itinerary)
29 }
30
31 // AssignToRoute attaches a new itinerary to this cargo.
32 func (c *Cargo) AssignToRoute(itinerary Itinerary) {
33 c.Itinerary = itinerary
34 c.Delivery = c.Delivery.UpdateOnRouting(c.RouteSpecification, c.Itinerary)
35 }
36
37 // DeriveDeliveryProgress updates all aspects of the cargo aggregate status
38 // based on the current route specification, itinerary and handling of the cargo.
39 func (c *Cargo) DeriveDeliveryProgress(history HandlingHistory) {
40 c.Delivery = DeriveDeliveryFrom(c.RouteSpecification, c.Itinerary, history)
41 }
42
43 // New creates a new, unrouted cargo.
44 func New(id TrackingID, rs RouteSpecification) *Cargo {
45 itinerary := Itinerary{}
46 history := HandlingHistory{make([]HandlingEvent, 0)}
47
48 return &Cargo{
49 TrackingID: id,
50 Origin: rs.Origin,
51 RouteSpecification: rs,
52 Delivery: DeriveDeliveryFrom(rs, itinerary, history),
53 }
54 }
55
56 // Repository provides access a cargo store.
57 type Repository interface {
58 Store(cargo *Cargo) error
59 Find(trackingID TrackingID) (*Cargo, error)
60 FindAll() []*Cargo
61 }
62
63 // ErrUnknown is used when a cargo could not be found.
64 var ErrUnknown = errors.New("unknown cargo")
65
66 // NextTrackingID generates a new tracking ID.
67 // TODO: Move to infrastructure(?)
68 func NextTrackingID() TrackingID {
69 return TrackingID(strings.Split(strings.ToUpper(uuid.New()), "-")[0])
70 }
71
72 // RouteSpecification Contains information about a route: its origin,
73 // destination and arrival deadline.
74 type RouteSpecification struct {
75 Origin location.UNLocode
76 Destination location.UNLocode
77 ArrivalDeadline time.Time
78 }
79
80 // IsSatisfiedBy checks whether provided itinerary satisfies this
81 // specification.
82 func (s RouteSpecification) IsSatisfiedBy(itinerary Itinerary) bool {
83 return itinerary.Legs != nil &&
84 s.Origin == itinerary.InitialDepartureLocation() &&
85 s.Destination == itinerary.FinalArrivalLocation()
86 }
87
88 // RoutingStatus describes status of cargo routing.
89 type RoutingStatus int
90
91 // Valid routing statuses.
92 const (
93 NotRouted RoutingStatus = iota
94 Misrouted
95 Routed
96 )
97
98 func (s RoutingStatus) String() string {
99 switch s {
100 case NotRouted:
101 return "Not routed"
102 case Misrouted:
103 return "Misrouted"
104 case Routed:
105 return "Routed"
106 }
107 return ""
108 }
109
110 // TransportStatus describes status of cargo transportation.
111 type TransportStatus int
112
113 // Valid transport statuses.
114 const (
115 NotReceived TransportStatus = iota
116 InPort
117 OnboardCarrier
118 Claimed
119 Unknown
120 )
121
122 func (s TransportStatus) String() string {
123 switch s {
124 case NotReceived:
125 return "Not received"
126 case InPort:
127 return "In port"
128 case OnboardCarrier:
129 return "Onboard carrier"
130 case Claimed:
131 return "Claimed"
132 case Unknown:
133 return "Unknown"
134 }
135 return ""
136 }
0 package cargo
1
2 import (
3 "time"
4
5 "github.com/go-kit/kit/examples/shipping/location"
6 "github.com/go-kit/kit/examples/shipping/voyage"
7 )
8
9 // Delivery is the actual transportation of the cargo, as opposed to the
10 // customer requirement (RouteSpecification) and the plan (Itinerary).
11 type Delivery struct {
12 Itinerary Itinerary
13 RouteSpecification RouteSpecification
14 RoutingStatus RoutingStatus
15 TransportStatus TransportStatus
16 NextExpectedActivity HandlingActivity
17 LastEvent HandlingEvent
18 LastKnownLocation location.UNLocode
19 CurrentVoyage voyage.Number
20 ETA time.Time
21 IsMisdirected bool
22 IsUnloadedAtDestination bool
23 }
24
25 // UpdateOnRouting creates a new delivery snapshot to reflect changes in
26 // routing, i.e. when the route specification or the itinerary has changed but
27 // no additional handling of the cargo has been performed.
28 func (d Delivery) UpdateOnRouting(rs RouteSpecification, itinerary Itinerary) Delivery {
29 return newDelivery(d.LastEvent, itinerary, rs)
30 }
31
32 // IsOnTrack checks if the delivery is on track.
33 func (d Delivery) IsOnTrack() bool {
34 return d.RoutingStatus == Routed && !d.IsMisdirected
35 }
36
37 // DeriveDeliveryFrom creates a new delivery snapshot based on the complete
38 // handling history of a cargo, as well as its route specification and
39 // itinerary.
40 func DeriveDeliveryFrom(rs RouteSpecification, itinerary Itinerary, history HandlingHistory) Delivery {
41 lastEvent, _ := history.MostRecentlyCompletedEvent()
42 return newDelivery(lastEvent, itinerary, rs)
43 }
44
45 // newDelivery creates a up-to-date delivery based on an handling event,
46 // itinerary and a route specification.
47 func newDelivery(lastEvent HandlingEvent, itinerary Itinerary, rs RouteSpecification) Delivery {
48 var (
49 routingStatus = calculateRoutingStatus(itinerary, rs)
50 transportStatus = calculateTransportStatus(lastEvent)
51 lastKnownLocation = calculateLastKnownLocation(lastEvent)
52 isMisdirected = calculateMisdirectedStatus(lastEvent, itinerary)
53 isUnloadedAtDestination = calculateUnloadedAtDestination(lastEvent, rs)
54 currentVoyage = calculateCurrentVoyage(transportStatus, lastEvent)
55 )
56
57 d := Delivery{
58 LastEvent: lastEvent,
59 Itinerary: itinerary,
60 RouteSpecification: rs,
61 RoutingStatus: routingStatus,
62 TransportStatus: transportStatus,
63 LastKnownLocation: lastKnownLocation,
64 IsMisdirected: isMisdirected,
65 IsUnloadedAtDestination: isUnloadedAtDestination,
66 CurrentVoyage: currentVoyage,
67 }
68
69 d.NextExpectedActivity = calculateNextExpectedActivity(d)
70 d.ETA = calculateETA(d)
71
72 return d
73 }
74
75 // Below are internal functions used when creating a new delivery.
76
77 func calculateRoutingStatus(itinerary Itinerary, rs RouteSpecification) RoutingStatus {
78 if itinerary.Legs == nil {
79 return NotRouted
80 }
81
82 if rs.IsSatisfiedBy(itinerary) {
83 return Routed
84 }
85
86 return Misrouted
87 }
88
89 func calculateMisdirectedStatus(event HandlingEvent, itinerary Itinerary) bool {
90 if event.Activity.Type == NotHandled {
91 return false
92 }
93
94 return !itinerary.IsExpected(event)
95 }
96
97 func calculateUnloadedAtDestination(event HandlingEvent, rs RouteSpecification) bool {
98 if event.Activity.Type == NotHandled {
99 return false
100 }
101
102 return event.Activity.Type == Unload && rs.Destination == event.Activity.Location
103 }
104
105 func calculateTransportStatus(event HandlingEvent) TransportStatus {
106 switch event.Activity.Type {
107 case NotHandled:
108 return NotReceived
109 case Load:
110 return OnboardCarrier
111 case Unload:
112 return InPort
113 case Receive:
114 return InPort
115 case Customs:
116 return InPort
117 case Claim:
118 return Claimed
119 }
120 return Unknown
121 }
122
123 func calculateLastKnownLocation(event HandlingEvent) location.UNLocode {
124 return event.Activity.Location
125 }
126
127 func calculateNextExpectedActivity(d Delivery) HandlingActivity {
128 if !d.IsOnTrack() {
129 return HandlingActivity{}
130 }
131
132 switch d.LastEvent.Activity.Type {
133 case NotHandled:
134 return HandlingActivity{Type: Receive, Location: d.RouteSpecification.Origin}
135 case Receive:
136 l := d.Itinerary.Legs[0]
137 return HandlingActivity{Type: Load, Location: l.LoadLocation, VoyageNumber: l.VoyageNumber}
138 case Load:
139 for _, l := range d.Itinerary.Legs {
140 if l.LoadLocation == d.LastEvent.Activity.Location {
141 return HandlingActivity{Type: Unload, Location: l.UnloadLocation, VoyageNumber: l.VoyageNumber}
142 }
143 }
144 case Unload:
145 for i, l := range d.Itinerary.Legs {
146 if l.UnloadLocation == d.LastEvent.Activity.Location {
147 if i < len(d.Itinerary.Legs)-1 {
148 return HandlingActivity{Type: Load, Location: d.Itinerary.Legs[i+1].LoadLocation, VoyageNumber: d.Itinerary.Legs[i+1].VoyageNumber}
149 }
150
151 return HandlingActivity{Type: Claim, Location: l.UnloadLocation}
152 }
153 }
154 }
155
156 return HandlingActivity{}
157 }
158
159 func calculateCurrentVoyage(transportStatus TransportStatus, event HandlingEvent) voyage.Number {
160 if transportStatus == OnboardCarrier && event.Activity.Type != NotHandled {
161 return event.Activity.VoyageNumber
162 }
163
164 return voyage.Number("")
165 }
166
167 func calculateETA(d Delivery) time.Time {
168 if !d.IsOnTrack() {
169 return time.Time{}
170 }
171
172 return d.Itinerary.FinalArrivalTime()
173 }
0 package cargo
1
2 // TODO: It would make sense to have this in its own package. Unfortunately,
3 // then there would be a circular dependency between the cargo and handling
4 // packages since cargo.Delivery would use handling.HandlingEvent and
5 // handling.HandlingEvent would use cargo.TrackingID. Also,
6 // HandlingEventFactory depends on the cargo repository.
7 //
8 // It would make sense not having the cargo package depend on handling.
9
10 import (
11 "errors"
12 "time"
13
14 "github.com/go-kit/kit/examples/shipping/location"
15 "github.com/go-kit/kit/examples/shipping/voyage"
16 )
17
18 // HandlingActivity represents how and where a cargo can be handled, and can
19 // be used to express predictions about what is expected to happen to a cargo
20 // in the future.
21 type HandlingActivity struct {
22 Type HandlingEventType
23 Location location.UNLocode
24 VoyageNumber voyage.Number
25 }
26
27 // HandlingEvent is used to register the event when, for instance, a cargo is
28 // unloaded from a carrier at a some location at a given time.
29 type HandlingEvent struct {
30 TrackingID TrackingID
31 Activity HandlingActivity
32 }
33
34 // HandlingEventType describes type of a handling event.
35 type HandlingEventType int
36
37 // Valid handling event types.
38 const (
39 NotHandled HandlingEventType = iota
40 Load
41 Unload
42 Receive
43 Claim
44 Customs
45 )
46
47 func (t HandlingEventType) String() string {
48 switch t {
49 case NotHandled:
50 return "Not Handled"
51 case Load:
52 return "Load"
53 case Unload:
54 return "Unload"
55 case Receive:
56 return "Receive"
57 case Claim:
58 return "Claim"
59 case Customs:
60 return "Customs"
61 }
62
63 return ""
64 }
65
66 // HandlingHistory is the handling history of a cargo.
67 type HandlingHistory struct {
68 HandlingEvents []HandlingEvent
69 }
70
71 // MostRecentlyCompletedEvent returns most recently completed handling event.
72 func (h HandlingHistory) MostRecentlyCompletedEvent() (HandlingEvent, error) {
73 if len(h.HandlingEvents) == 0 {
74 return HandlingEvent{}, errors.New("delivery history is empty")
75 }
76
77 return h.HandlingEvents[len(h.HandlingEvents)-1], nil
78 }
79
80 // HandlingEventRepository provides access a handling event store.
81 type HandlingEventRepository interface {
82 Store(e HandlingEvent)
83 QueryHandlingHistory(TrackingID) HandlingHistory
84 }
85
86 // HandlingEventFactory creates handling events.
87 type HandlingEventFactory struct {
88 CargoRepository Repository
89 VoyageRepository voyage.Repository
90 LocationRepository location.Repository
91 }
92
93 // CreateHandlingEvent creates a validated handling event.
94 func (f *HandlingEventFactory) CreateHandlingEvent(registrationTime time.Time, completionTime time.Time, trackingID TrackingID,
95 voyageNumber voyage.Number, unLocode location.UNLocode, eventType HandlingEventType) (HandlingEvent, error) {
96
97 if _, err := f.CargoRepository.Find(trackingID); err != nil {
98 return HandlingEvent{}, err
99 }
100
101 if _, err := f.VoyageRepository.Find(voyageNumber); err != nil {
102 // TODO: This is pretty ugly, but when creating a Receive event, the voyage number is not known.
103 if len(voyageNumber) > 0 {
104 return HandlingEvent{}, err
105 }
106 }
107
108 if _, err := f.LocationRepository.Find(unLocode); err != nil {
109 return HandlingEvent{}, err
110 }
111
112 return HandlingEvent{
113 TrackingID: trackingID,
114 Activity: HandlingActivity{
115 Type: eventType,
116 Location: unLocode,
117 VoyageNumber: voyageNumber,
118 },
119 }, nil
120 }
0 package cargo
1
2 import (
3 "time"
4
5 "github.com/go-kit/kit/examples/shipping/location"
6 "github.com/go-kit/kit/examples/shipping/voyage"
7 )
8
9 // Leg describes the transportation between two locations on a voyage.
10 type Leg struct {
11 VoyageNumber voyage.Number `json:"voyage_number"`
12 LoadLocation location.UNLocode `json:"from"`
13 UnloadLocation location.UNLocode `json:"to"`
14 LoadTime time.Time `json:"load_time"`
15 UnloadTime time.Time `json:"unload_time"`
16 }
17
18 // NewLeg creates a new itinerary leg.
19 func NewLeg(voyageNumber voyage.Number, loadLocation, unloadLocation location.UNLocode, loadTime, unloadTime time.Time) Leg {
20 return Leg{
21 VoyageNumber: voyageNumber,
22 LoadLocation: loadLocation,
23 UnloadLocation: unloadLocation,
24 LoadTime: loadTime,
25 UnloadTime: unloadTime,
26 }
27 }
28
29 // Itinerary specifies steps required to transport a cargo from its origin to
30 // destination.
31 type Itinerary struct {
32 Legs []Leg `json:"legs"`
33 }
34
35 // InitialDepartureLocation returns the start of the itinerary.
36 func (i Itinerary) InitialDepartureLocation() location.UNLocode {
37 if i.IsEmpty() {
38 return location.UNLocode("")
39 }
40 return i.Legs[0].LoadLocation
41 }
42
43 // FinalArrivalLocation returns the end of the itinerary.
44 func (i Itinerary) FinalArrivalLocation() location.UNLocode {
45 if i.IsEmpty() {
46 return location.UNLocode("")
47 }
48 return i.Legs[len(i.Legs)-1].UnloadLocation
49 }
50
51 // FinalArrivalTime returns the expected arrival time at final destination.
52 func (i Itinerary) FinalArrivalTime() time.Time {
53 return i.Legs[len(i.Legs)-1].UnloadTime
54 }
55
56 // IsEmpty checks if the itinerary contains at least one leg.
57 func (i Itinerary) IsEmpty() bool {
58 return i.Legs == nil || len(i.Legs) == 0
59 }
60
61 // IsExpected checks if the given handling event is expected when executing
62 // this itinerary.
63 func (i Itinerary) IsExpected(event HandlingEvent) bool {
64 if i.IsEmpty() {
65 return true
66 }
67
68 switch event.Activity.Type {
69 case Receive:
70 return i.InitialDepartureLocation() == event.Activity.Location
71 case Load:
72 for _, l := range i.Legs {
73 if l.LoadLocation == event.Activity.Location && l.VoyageNumber == event.Activity.VoyageNumber {
74 return true
75 }
76 }
77 return false
78 case Unload:
79 for _, l := range i.Legs {
80 if l.UnloadLocation == event.Activity.Location && l.VoyageNumber == event.Activity.VoyageNumber {
81 return true
82 }
83 }
84 return false
85 case Claim:
86 return i.FinalArrivalLocation() == event.Activity.Location
87 }
88
89 return true
90 }
0 package handling
1
2 import (
3 "time"
4
5 "golang.org/x/net/context"
6
7 "github.com/go-kit/kit/endpoint"
8 "github.com/go-kit/kit/examples/shipping/cargo"
9 "github.com/go-kit/kit/examples/shipping/location"
10 "github.com/go-kit/kit/examples/shipping/voyage"
11 )
12
13 type registerIncidentRequest struct {
14 ID cargo.TrackingID
15 Location location.UNLocode
16 Voyage voyage.Number
17 EventType cargo.HandlingEventType
18 CompletionTime time.Time
19 }
20
21 type registerIncidentResponse struct {
22 Err error `json:"error,omitempty"`
23 }
24
25 func (r registerIncidentResponse) error() error { return r.Err }
26
27 func makeRegisterIncidentEndpoint(hs Service) endpoint.Endpoint {
28 return func(ctx context.Context, request interface{}) (interface{}, error) {
29 req := request.(registerIncidentRequest)
30 err := hs.RegisterHandlingEvent(req.CompletionTime, req.ID, req.Voyage, req.Location, req.EventType)
31 return registerIncidentResponse{Err: err}, nil
32 }
33 }
0 package handling
1
2 import (
3 "time"
4
5 "github.com/go-kit/kit/examples/shipping/cargo"
6 "github.com/go-kit/kit/examples/shipping/location"
7 "github.com/go-kit/kit/examples/shipping/voyage"
8 "github.com/go-kit/kit/log"
9 )
10
11 type loggingService struct {
12 logger log.Logger
13 Service
14 }
15
16 // NewLoggingService returns a new instance of a logging Service.
17 func NewLoggingService(logger log.Logger, s Service) Service {
18 return &loggingService{logger, s}
19 }
20
21 func (s *loggingService) RegisterHandlingEvent(completionTime time.Time, trackingID cargo.TrackingID, voyageNumber voyage.Number,
22 unLocode location.UNLocode, eventType cargo.HandlingEventType) (err error) {
23 defer func(begin time.Time) {
24 s.logger.Log(
25 "method", "register_incident",
26 "tracking_id", trackingID,
27 "location", unLocode,
28 "voyage", voyageNumber,
29 "event_type", eventType,
30 "completion_time", completionTime,
31 "took", time.Since(begin),
32 "err", err,
33 )
34 }(time.Now())
35 return s.Service.RegisterHandlingEvent(completionTime, trackingID, voyageNumber, unLocode, eventType)
36 }
0 // Package handling provides the use-case for registering incidents. Used by
1 // views facing the people handling the cargo along its route.
2 package handling
3
4 import (
5 "errors"
6 "time"
7
8 "github.com/go-kit/kit/examples/shipping/cargo"
9 "github.com/go-kit/kit/examples/shipping/inspection"
10 "github.com/go-kit/kit/examples/shipping/location"
11 "github.com/go-kit/kit/examples/shipping/voyage"
12 )
13
14 // ErrInvalidArgument is returned when one or more arguments are invalid.
15 var ErrInvalidArgument = errors.New("invalid argument")
16
17 // EventHandler provides a means of subscribing to registered handling events.
18 type EventHandler interface {
19 CargoWasHandled(cargo.HandlingEvent)
20 }
21
22 // Service provides handling operations.
23 type Service interface {
24 // RegisterHandlingEvent registers a handling event in the system, and
25 // notifies interested parties that a cargo has been handled.
26 RegisterHandlingEvent(completionTime time.Time, trackingID cargo.TrackingID, voyageNumber voyage.Number,
27 unLocode location.UNLocode, eventType cargo.HandlingEventType) error
28 }
29
30 type service struct {
31 handlingEventRepository cargo.HandlingEventRepository
32 handlingEventFactory cargo.HandlingEventFactory
33 handlingEventHandler EventHandler
34 }
35
36 func (s *service) RegisterHandlingEvent(completionTime time.Time, trackingID cargo.TrackingID, voyage voyage.Number,
37 loc location.UNLocode, eventType cargo.HandlingEventType) error {
38 if completionTime.IsZero() || trackingID == "" || loc == "" || eventType == cargo.NotHandled {
39 return ErrInvalidArgument
40 }
41
42 e, err := s.handlingEventFactory.CreateHandlingEvent(time.Now(), completionTime, trackingID, voyage, loc, eventType)
43 if err != nil {
44 return err
45 }
46
47 s.handlingEventRepository.Store(e)
48 s.handlingEventHandler.CargoWasHandled(e)
49
50 return nil
51 }
52
53 // NewService creates a handling event service with necessary dependencies.
54 func NewService(r cargo.HandlingEventRepository, f cargo.HandlingEventFactory, h EventHandler) Service {
55 return &service{
56 handlingEventRepository: r,
57 handlingEventFactory: f,
58 handlingEventHandler: h,
59 }
60 }
61
62 type handlingEventHandler struct {
63 InspectionService inspection.Service
64 }
65
66 func (h *handlingEventHandler) CargoWasHandled(event cargo.HandlingEvent) {
67 h.InspectionService.InspectCargo(event.TrackingID)
68 }
69
70 // NewEventHandler returns a new instance of a EventHandler.
71 func NewEventHandler(s inspection.Service) EventHandler {
72 return &handlingEventHandler{
73 InspectionService: s,
74 }
75 }
0 package handling
1
2 import (
3 "encoding/json"
4 "net/http"
5 "time"
6
7 "github.com/gorilla/mux"
8 "golang.org/x/net/context"
9
10 "github.com/go-kit/kit/examples/shipping/cargo"
11 "github.com/go-kit/kit/examples/shipping/location"
12 "github.com/go-kit/kit/examples/shipping/voyage"
13 kitlog "github.com/go-kit/kit/log"
14 kithttp "github.com/go-kit/kit/transport/http"
15 )
16
17 // MakeHandler returns a handler for the handling service.
18 func MakeHandler(ctx context.Context, hs Service, logger kitlog.Logger) http.Handler {
19 r := mux.NewRouter()
20
21 opts := []kithttp.ServerOption{
22 kithttp.ServerErrorLogger(logger),
23 kithttp.ServerErrorEncoder(encodeError),
24 }
25
26 registerIncidentHandler := kithttp.NewServer(
27 ctx,
28 makeRegisterIncidentEndpoint(hs),
29 decodeRegisterIncidentRequest,
30 encodeResponse,
31 opts...,
32 )
33
34 r.Handle("/handling/v1/incidents", registerIncidentHandler).Methods("POST")
35
36 return r
37 }
38
39 func decodeRegisterIncidentRequest(r *http.Request) (interface{}, error) {
40 var body struct {
41 CompletionTime time.Time `json:"completion_time"`
42 TrackingID string `json:"tracking_id"`
43 VoyageNumber string `json:"voyage"`
44 Location string `json:"location"`
45 EventType string `json:"event_type"`
46 }
47
48 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
49 return nil, err
50 }
51
52 return registerIncidentRequest{
53 CompletionTime: body.CompletionTime,
54 ID: cargo.TrackingID(body.TrackingID),
55 Voyage: voyage.Number(body.VoyageNumber),
56 Location: location.UNLocode(body.Location),
57 EventType: stringToEventType(body.EventType),
58 }, nil
59 }
60
61 func stringToEventType(s string) cargo.HandlingEventType {
62 types := map[string]cargo.HandlingEventType{
63 cargo.Receive.String(): cargo.Receive,
64 cargo.Load.String(): cargo.Load,
65 cargo.Unload.String(): cargo.Unload,
66 cargo.Customs.String(): cargo.Customs,
67 cargo.Claim.String(): cargo.Claim,
68 }
69 return types[s]
70 }
71
72 func encodeResponse(w http.ResponseWriter, response interface{}) error {
73 if e, ok := response.(errorer); ok && e.error() != nil {
74 encodeError(w, e.error())
75 return nil
76 }
77 w.Header().Set("Content-Type", "application/json; charset=utf-8")
78 return json.NewEncoder(w).Encode(response)
79 }
80
81 type errorer interface {
82 error() error
83 }
84
85 // encode errors from business-logic
86 func encodeError(w http.ResponseWriter, err error) {
87 switch err {
88 case cargo.ErrUnknown:
89 w.WriteHeader(http.StatusNotFound)
90 case ErrInvalidArgument:
91 w.WriteHeader(http.StatusBadRequest)
92 default:
93 w.WriteHeader(http.StatusInternalServerError)
94 }
95 w.Header().Set("Content-Type", "application/json; charset=utf-8")
96 json.NewEncoder(w).Encode(map[string]interface{}{
97 "error": err.Error(),
98 })
99 }
0 // Package inspection provides means to inspect cargos.
1 package inspection
2
3 import "github.com/go-kit/kit/examples/shipping/cargo"
4
5 // EventHandler provides means of subscribing to inspection events.
6 type EventHandler interface {
7 CargoWasMisdirected(*cargo.Cargo)
8 CargoHasArrived(*cargo.Cargo)
9 }
10
11 // Service provides cargo inspection operations.
12 type Service interface {
13 // InspectCargo inspects cargo and send relevant notifications to
14 // interested parties, for example if a cargo has been misdirected, or
15 // unloaded at the final destination.
16 InspectCargo(trackingID cargo.TrackingID)
17 }
18
19 type service struct {
20 cargoRepository cargo.Repository
21 handlingEventRepository cargo.HandlingEventRepository
22 cargoEventHandler EventHandler
23 }
24
25 // TODO: Should be transactional
26 func (s *service) InspectCargo(trackingID cargo.TrackingID) {
27 c, err := s.cargoRepository.Find(trackingID)
28 if err != nil {
29 return
30 }
31
32 h := s.handlingEventRepository.QueryHandlingHistory(trackingID)
33
34 c.DeriveDeliveryProgress(h)
35
36 if c.Delivery.IsMisdirected {
37 s.cargoEventHandler.CargoWasMisdirected(c)
38 }
39
40 if c.Delivery.IsUnloadedAtDestination {
41 s.cargoEventHandler.CargoHasArrived(c)
42 }
43
44 s.cargoRepository.Store(c)
45 }
46
47 // NewService creates a inspection service with necessary dependencies.
48 func NewService(cargoRepository cargo.Repository, handlingEventRepository cargo.HandlingEventRepository, eventHandler EventHandler) Service {
49 return &service{cargoRepository, handlingEventRepository, eventHandler}
50 }
0 // Package location provides the Location aggregate.
1 package location
2
3 import "errors"
4
5 // UNLocode is the United Nations location code that uniquely identifies a
6 // particular location.
7 //
8 // http://www.unece.org/cefact/locode/
9 // http://www.unece.org/cefact/locode/DocColumnDescription.htm#LOCODE
10 type UNLocode string
11
12 // Location is a location is our model is stops on a journey, such as cargo
13 // origin or destination, or carrier movement endpoints.
14 type Location struct {
15 UNLocode UNLocode
16 Name string
17 }
18
19 // ErrUnknown is used when a location could not be found.
20 var ErrUnknown = errors.New("unknown location")
21
22 // Repository provides access a location store.
23 type Repository interface {
24 Find(locode UNLocode) (Location, error)
25 FindAll() []Location
26 }
0 package location
1
2 // Sample UN locodes.
3 var (
4 SESTO UNLocode = "SESTO"
5 AUMEL UNLocode = "AUMEL"
6 CNHKG UNLocode = "CNHKG"
7 USNYC UNLocode = "USNYC"
8 USCHI UNLocode = "USCHI"
9 JNTKO UNLocode = "JNTKO"
10 DEHAM UNLocode = "DEHAM"
11 NLRTM UNLocode = "NLRTM"
12 FIHEL UNLocode = "FIHEL"
13 )
14
15 // Sample locations.
16 var (
17 Stockholm = Location{SESTO, "Stockholm"}
18 Melbourne = Location{AUMEL, "Melbourne"}
19 Hongkong = Location{CNHKG, "Hongkong"}
20 NewYork = Location{USNYC, "New York"}
21 Chicago = Location{USCHI, "Chicago"}
22 Tokyo = Location{JNTKO, "Tokyo"}
23 Hamburg = Location{DEHAM, "Hamburg"}
24 Rotterdam = Location{NLRTM, "Rotterdam"}
25 Helsinki = Location{FIHEL, "Helsinki"}
26 )
0 package main
1
2 import (
3 "flag"
4 "fmt"
5 "net/http"
6 "os"
7 "os/signal"
8 "sync"
9 "syscall"
10 "time"
11
12 "github.com/go-kit/kit/log"
13 "golang.org/x/net/context"
14
15 "github.com/go-kit/kit/examples/shipping/booking"
16 "github.com/go-kit/kit/examples/shipping/cargo"
17 "github.com/go-kit/kit/examples/shipping/handling"
18 "github.com/go-kit/kit/examples/shipping/inspection"
19 "github.com/go-kit/kit/examples/shipping/location"
20 "github.com/go-kit/kit/examples/shipping/repository"
21 "github.com/go-kit/kit/examples/shipping/routing"
22 "github.com/go-kit/kit/examples/shipping/tracking"
23 )
24
25 const (
26 defaultPort = "8080"
27 defaultRoutingServiceURL = "http://localhost:7878"
28 )
29
30 func main() {
31 var (
32 addr = envString("PORT", defaultPort)
33 rsurl = envString("ROUTINGSERVICE_URL", defaultRoutingServiceURL)
34
35 httpAddr = flag.String("http.addr", ":"+addr, "HTTP listen address")
36 routingServiceURL = flag.String("service.routing", rsurl, "routing service URL")
37
38 ctx = context.Background()
39 )
40
41 flag.Parse()
42
43 var logger log.Logger
44 logger = log.NewLogfmtLogger(os.Stderr)
45 logger = &serializedLogger{Logger: logger}
46 logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)
47
48 var (
49 cargos = repository.NewCargo()
50 locations = repository.NewLocation()
51 voyages = repository.NewVoyage()
52 handlingEvents = repository.NewHandlingEvent()
53 )
54
55 // Configure some questionable dependencies.
56 var (
57 handlingEventFactory = cargo.HandlingEventFactory{
58 CargoRepository: cargos,
59 VoyageRepository: voyages,
60 LocationRepository: locations,
61 }
62 handlingEventHandler = handling.NewEventHandler(
63 inspection.NewService(cargos, handlingEvents, nil),
64 )
65 )
66
67 // Facilitate testing by adding some cargos.
68 storeTestData(cargos)
69
70 var rs routing.Service
71 rs = routing.NewProxyingMiddleware(*routingServiceURL, ctx)(rs)
72
73 var bs booking.Service
74 bs = booking.NewService(cargos, locations, handlingEvents, rs)
75 bs = booking.NewLoggingService(log.NewContext(logger).With("component", "booking"), bs)
76
77 var ts tracking.Service
78 ts = tracking.NewService(cargos, handlingEvents)
79 ts = tracking.NewLoggingService(log.NewContext(logger).With("component", "tracking"), ts)
80
81 var hs handling.Service
82 hs = handling.NewService(handlingEvents, handlingEventFactory, handlingEventHandler)
83 hs = handling.NewLoggingService(log.NewContext(logger).With("component", "handling"), hs)
84
85 httpLogger := log.NewContext(logger).With("component", "http")
86
87 mux := http.NewServeMux()
88
89 mux.Handle("/booking/v1/", booking.MakeHandler(ctx, bs, httpLogger))
90 mux.Handle("/tracking/v1/", tracking.MakeHandler(ctx, ts, httpLogger))
91 mux.Handle("/handling/v1/", handling.MakeHandler(ctx, hs, httpLogger))
92
93 http.Handle("/", accessControl(mux))
94
95 errs := make(chan error, 2)
96 go func() {
97 logger.Log("transport", "http", "address", *httpAddr, "msg", "listening")
98 errs <- http.ListenAndServe(*httpAddr, nil)
99 }()
100 go func() {
101 c := make(chan os.Signal)
102 signal.Notify(c, syscall.SIGINT)
103 errs <- fmt.Errorf("%s", <-c)
104 }()
105
106 logger.Log("terminated", <-errs)
107 }
108
109 func accessControl(h http.Handler) http.Handler {
110 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
111 w.Header().Set("Access-Control-Allow-Origin", "*")
112 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
113 w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type")
114
115 if r.Method == "OPTIONS" {
116 return
117 }
118
119 h.ServeHTTP(w, r)
120 })
121 }
122
123 func envString(env, fallback string) string {
124 e := os.Getenv(env)
125 if e == "" {
126 return fallback
127 }
128 return e
129 }
130
131 func storeTestData(r cargo.Repository) {
132 test1 := cargo.New("FTL456", cargo.RouteSpecification{
133 Origin: location.AUMEL,
134 Destination: location.SESTO,
135 ArrivalDeadline: time.Now().AddDate(0, 0, 7),
136 })
137 _ = r.Store(test1)
138
139 test2 := cargo.New("ABC123", cargo.RouteSpecification{
140 Origin: location.SESTO,
141 Destination: location.CNHKG,
142 ArrivalDeadline: time.Now().AddDate(0, 0, 14),
143 })
144 _ = r.Store(test2)
145 }
146
147 type serializedLogger struct {
148 mtx sync.Mutex
149 log.Logger
150 }
151
152 func (l *serializedLogger) Log(keyvals ...interface{}) error {
153 l.mtx.Lock()
154 defer l.mtx.Unlock()
155 return l.Logger.Log(keyvals...)
156 }
0 // Package repository provides implementations of all the domain repositories.
1 package repository
2
3 import (
4 "sync"
5
6 "github.com/go-kit/kit/examples/shipping/cargo"
7 "github.com/go-kit/kit/examples/shipping/location"
8 "github.com/go-kit/kit/examples/shipping/voyage"
9 )
10
11 type cargoRepository struct {
12 mtx sync.RWMutex
13 cargos map[cargo.TrackingID]*cargo.Cargo
14 }
15
16 func (r *cargoRepository) Store(c *cargo.Cargo) error {
17 r.mtx.Lock()
18 defer r.mtx.Unlock()
19 r.cargos[c.TrackingID] = c
20 return nil
21 }
22
23 func (r *cargoRepository) Find(trackingID cargo.TrackingID) (*cargo.Cargo, error) {
24 r.mtx.RLock()
25 defer r.mtx.RUnlock()
26 if val, ok := r.cargos[trackingID]; ok {
27 return val, nil
28 }
29 return nil, cargo.ErrUnknown
30 }
31
32 func (r *cargoRepository) FindAll() []*cargo.Cargo {
33 r.mtx.RLock()
34 defer r.mtx.RUnlock()
35 c := make([]*cargo.Cargo, 0, len(r.cargos))
36 for _, val := range r.cargos {
37 c = append(c, val)
38 }
39 return c
40 }
41
42 // NewCargo returns a new instance of a in-memory cargo repository.
43 func NewCargo() cargo.Repository {
44 return &cargoRepository{
45 cargos: make(map[cargo.TrackingID]*cargo.Cargo),
46 }
47 }
48
49 type locationRepository struct {
50 locations map[location.UNLocode]location.Location
51 }
52
53 func (r *locationRepository) Find(locode location.UNLocode) (location.Location, error) {
54 if l, ok := r.locations[locode]; ok {
55 return l, nil
56 }
57 return location.Location{}, location.ErrUnknown
58 }
59
60 func (r *locationRepository) FindAll() []location.Location {
61 l := make([]location.Location, 0, len(r.locations))
62 for _, val := range r.locations {
63 l = append(l, val)
64 }
65 return l
66 }
67
68 // NewLocation returns a new instance of a in-memory location repository.
69 func NewLocation() location.Repository {
70 r := &locationRepository{
71 locations: make(map[location.UNLocode]location.Location),
72 }
73
74 r.locations[location.SESTO] = location.Stockholm
75 r.locations[location.AUMEL] = location.Melbourne
76 r.locations[location.CNHKG] = location.Hongkong
77 r.locations[location.JNTKO] = location.Tokyo
78 r.locations[location.NLRTM] = location.Rotterdam
79 r.locations[location.DEHAM] = location.Hamburg
80
81 return r
82 }
83
84 type voyageRepository struct {
85 voyages map[voyage.Number]*voyage.Voyage
86 }
87
88 func (r *voyageRepository) Find(voyageNumber voyage.Number) (*voyage.Voyage, error) {
89 if v, ok := r.voyages[voyageNumber]; ok {
90 return v, nil
91 }
92
93 return nil, voyage.ErrUnknown
94 }
95
96 // NewVoyage returns a new instance of a in-memory voyage repository.
97 func NewVoyage() voyage.Repository {
98 r := &voyageRepository{
99 voyages: make(map[voyage.Number]*voyage.Voyage),
100 }
101
102 r.voyages[voyage.V100.Number] = voyage.V100
103 r.voyages[voyage.V300.Number] = voyage.V300
104 r.voyages[voyage.V400.Number] = voyage.V400
105
106 r.voyages[voyage.V0100S.Number] = voyage.V0100S
107 r.voyages[voyage.V0200T.Number] = voyage.V0200T
108 r.voyages[voyage.V0300A.Number] = voyage.V0300A
109 r.voyages[voyage.V0301S.Number] = voyage.V0301S
110 r.voyages[voyage.V0400S.Number] = voyage.V0400S
111
112 return r
113 }
114
115 type handlingEventRepository struct {
116 mtx sync.RWMutex
117 events map[cargo.TrackingID][]cargo.HandlingEvent
118 }
119
120 func (r *handlingEventRepository) Store(e cargo.HandlingEvent) {
121 r.mtx.Lock()
122 defer r.mtx.Unlock()
123 // Make array if it's the first event with this tracking ID.
124 if _, ok := r.events[e.TrackingID]; !ok {
125 r.events[e.TrackingID] = make([]cargo.HandlingEvent, 0)
126 }
127 r.events[e.TrackingID] = append(r.events[e.TrackingID], e)
128 }
129
130 func (r *handlingEventRepository) QueryHandlingHistory(trackingID cargo.TrackingID) cargo.HandlingHistory {
131 r.mtx.RLock()
132 defer r.mtx.RUnlock()
133 return cargo.HandlingHistory{HandlingEvents: r.events[trackingID]}
134 }
135
136 // NewHandlingEvent returns a new instance of a in-memory handling event repository.
137 func NewHandlingEvent() cargo.HandlingEventRepository {
138 return &handlingEventRepository{
139 events: make(map[cargo.TrackingID][]cargo.HandlingEvent),
140 }
141 }
0 package routing
1
2 import (
3 "encoding/json"
4 "net/http"
5 "net/url"
6 "time"
7
8 "golang.org/x/net/context"
9
10 "github.com/go-kit/kit/circuitbreaker"
11 "github.com/go-kit/kit/endpoint"
12 "github.com/go-kit/kit/examples/shipping/cargo"
13 "github.com/go-kit/kit/examples/shipping/location"
14 "github.com/go-kit/kit/examples/shipping/voyage"
15 kithttp "github.com/go-kit/kit/transport/http"
16 )
17
18 type proxyService struct {
19 context.Context
20 FetchRoutesEndpoint endpoint.Endpoint
21 Service
22 }
23
24 func (s proxyService) FetchRoutesForSpecification(rs cargo.RouteSpecification) []cargo.Itinerary {
25 response, err := s.FetchRoutesEndpoint(s.Context, fetchRoutesRequest{
26 From: string(rs.Origin),
27 To: string(rs.Destination),
28 })
29 if err != nil {
30 return []cargo.Itinerary{}
31 }
32
33 resp := response.(fetchRoutesResponse)
34
35 var itineraries []cargo.Itinerary
36 for _, r := range resp.Paths {
37 var legs []cargo.Leg
38 for _, e := range r.Edges {
39 legs = append(legs, cargo.Leg{
40 VoyageNumber: voyage.Number(e.Voyage),
41 LoadLocation: location.UNLocode(e.Origin),
42 UnloadLocation: location.UNLocode(e.Destination),
43 LoadTime: e.Departure,
44 UnloadTime: e.Arrival,
45 })
46 }
47
48 itineraries = append(itineraries, cargo.Itinerary{Legs: legs})
49 }
50
51 return itineraries
52 }
53
54 // ServiceMiddleware defines a middleware for a routing service.
55 type ServiceMiddleware func(Service) Service
56
57 // NewProxyingMiddleware returns a new instance of a proxying middleware.
58 func NewProxyingMiddleware(proxyURL string, ctx context.Context) ServiceMiddleware {
59 return func(next Service) Service {
60 var e endpoint.Endpoint
61 e = makeFetchRoutesEndpoint(ctx, proxyURL)
62 e = circuitbreaker.Hystrix("fetch-routes")(e)
63 return proxyService{ctx, e, next}
64 }
65 }
66
67 type fetchRoutesRequest struct {
68 From string
69 To string
70 }
71
72 type fetchRoutesResponse struct {
73 Paths []struct {
74 Edges []struct {
75 Origin string `json:"origin"`
76 Destination string `json:"destination"`
77 Voyage string `json:"voyage"`
78 Departure time.Time `json:"departure"`
79 Arrival time.Time `json:"arrival"`
80 } `json:"edges"`
81 } `json:"paths"`
82 }
83
84 func makeFetchRoutesEndpoint(ctx context.Context, instance string) endpoint.Endpoint {
85 u, err := url.Parse(instance)
86 if err != nil {
87 panic(err)
88 }
89 if u.Path == "" {
90 u.Path = "/paths"
91 }
92 return kithttp.NewClient(
93 "GET", u,
94 encodeFetchRoutesRequest,
95 decodeFetchRoutesResponse,
96 ).Endpoint()
97 }
98
99 func decodeFetchRoutesResponse(resp *http.Response) (interface{}, error) {
100 var response fetchRoutesResponse
101 if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
102 return nil, err
103 }
104 return response, nil
105 }
106
107 func encodeFetchRoutesRequest(r *http.Request, request interface{}) error {
108 req := request.(fetchRoutesRequest)
109
110 vals := r.URL.Query()
111 vals.Add("from", req.From)
112 vals.Add("to", req.To)
113 r.URL.RawQuery = vals.Encode()
114
115 return nil
116 }
0 // Package routing provides the routing domain service. It does not actually
1 // implement the routing service but merely acts as a proxy for a separate
2 // bounded context.
3 package routing
4
5 import "github.com/go-kit/kit/examples/shipping/cargo"
6
7 // Service provides access to an external routing service.
8 type Service interface {
9 // FetchRoutesForSpecification finds all possible routes that satisfy a
10 // given specification.
11 FetchRoutesForSpecification(rs cargo.RouteSpecification) []cargo.Itinerary
12 }
0 package tracking
1
2 import (
3 "golang.org/x/net/context"
4
5 "github.com/go-kit/kit/endpoint"
6 )
7
8 type trackCargoRequest struct {
9 ID string
10 }
11
12 type trackCargoResponse struct {
13 Cargo *Cargo `json:"cargo,omitempty"`
14 Err error `json:"error,omitempty"`
15 }
16
17 func (r trackCargoResponse) error() error { return r.Err }
18
19 func makeTrackCargoEndpoint(ts Service) endpoint.Endpoint {
20 return func(ctx context.Context, request interface{}) (interface{}, error) {
21 req := request.(trackCargoRequest)
22 c, err := ts.Track(req.ID)
23 return trackCargoResponse{Cargo: &c, Err: err}, nil
24 }
25 }
0 package tracking
1
2 import (
3 "time"
4
5 "github.com/go-kit/kit/log"
6 )
7
8 type loggingService struct {
9 logger log.Logger
10 Service
11 }
12
13 // NewLoggingService returns a new instance of a logging Service.
14 func NewLoggingService(logger log.Logger, s Service) Service {
15 return &loggingService{logger, s}
16 }
17
18 func (s *loggingService) Track(id string) (c Cargo, err error) {
19 defer func(begin time.Time) {
20 s.logger.Log("method", "track", "tracking_id", id, "took", time.Since(begin), "err", err)
21 }(time.Now())
22 return s.Service.Track(id)
23 }
0 // Package tracking provides the use-case of tracking a cargo. Used by views
1 // facing the end-user.
2 package tracking
3
4 import (
5 "errors"
6 "fmt"
7 "strings"
8 "time"
9
10 "github.com/go-kit/kit/examples/shipping/cargo"
11 )
12
13 // ErrInvalidArgument is returned when one or more arguments are invalid.
14 var ErrInvalidArgument = errors.New("invalid argument")
15
16 // Service is the interface that provides the basic Track method.
17 type Service interface {
18 // Track returns a cargo matching a tracking ID.
19 Track(id string) (Cargo, error)
20 }
21
22 type service struct {
23 cargos cargo.Repository
24 handlingEvents cargo.HandlingEventRepository
25 }
26
27 func (s *service) Track(id string) (Cargo, error) {
28 if id == "" {
29 return Cargo{}, ErrInvalidArgument
30 }
31 c, err := s.cargos.Find(cargo.TrackingID(id))
32 if err != nil {
33 return Cargo{}, err
34 }
35 return assemble(c, s.handlingEvents), nil
36 }
37
38 // NewService returns a new instance of the default Service.
39 func NewService(cargos cargo.Repository, handlingEvents cargo.HandlingEventRepository) Service {
40 return &service{
41 cargos: cargos,
42 handlingEvents: handlingEvents,
43 }
44 }
45
46 // Cargo is a read model for tracking views.
47 type Cargo struct {
48 TrackingID string `json:"tracking_id"`
49 StatusText string `json:"status_text"`
50 Origin string `json:"origin"`
51 Destination string `json:"destination"`
52 ETA time.Time `json:"eta"`
53 NextExpectedActivity string `json:"next_expected_activity"`
54 ArrivalDeadline time.Time `json:"arrival_deadline"`
55 Events []Event `json:"events"`
56 }
57
58 // Leg is a read model for booking views.
59 type Leg struct {
60 VoyageNumber string `json:"voyage_number"`
61 From string `json:"from"`
62 To string `json:"to"`
63 LoadTime time.Time `json:"load_time"`
64 UnloadTime time.Time `json:"unload_time"`
65 }
66
67 // Event is a read model for tracking views.
68 type Event struct {
69 Description string `json:"description"`
70 Expected bool `json:"expected"`
71 }
72
73 func assemble(c *cargo.Cargo, her cargo.HandlingEventRepository) Cargo {
74 return Cargo{
75 TrackingID: string(c.TrackingID),
76 Origin: string(c.Origin),
77 Destination: string(c.RouteSpecification.Destination),
78 ETA: c.Delivery.ETA,
79 NextExpectedActivity: nextExpectedActivity(c),
80 ArrivalDeadline: c.RouteSpecification.ArrivalDeadline,
81 StatusText: assembleStatusText(c),
82 Events: assembleEvents(c, her),
83 }
84 }
85
86 func assembleLegs(c cargo.Cargo) []Leg {
87 var legs []Leg
88 for _, l := range c.Itinerary.Legs {
89 legs = append(legs, Leg{
90 VoyageNumber: string(l.VoyageNumber),
91 From: string(l.LoadLocation),
92 To: string(l.UnloadLocation),
93 LoadTime: l.LoadTime,
94 UnloadTime: l.UnloadTime,
95 })
96 }
97 return legs
98 }
99
100 func nextExpectedActivity(c *cargo.Cargo) string {
101 a := c.Delivery.NextExpectedActivity
102 prefix := "Next expected activity is to"
103
104 switch a.Type {
105 case cargo.Load:
106 return fmt.Sprintf("%s %s cargo onto voyage %s in %s.", prefix, strings.ToLower(a.Type.String()), a.VoyageNumber, a.Location)
107 case cargo.Unload:
108 return fmt.Sprintf("%s %s cargo off of voyage %s in %s.", prefix, strings.ToLower(a.Type.String()), a.VoyageNumber, a.Location)
109 case cargo.NotHandled:
110 return "There are currently no expected activities for this cargo."
111 }
112
113 return fmt.Sprintf("%s %s cargo in %s.", prefix, strings.ToLower(a.Type.String()), a.Location)
114 }
115
116 func assembleStatusText(c *cargo.Cargo) string {
117 switch c.Delivery.TransportStatus {
118 case cargo.NotReceived:
119 return "Not received"
120 case cargo.InPort:
121 return fmt.Sprintf("In port %s", c.Delivery.LastKnownLocation)
122 case cargo.OnboardCarrier:
123 return fmt.Sprintf("Onboard voyage %s", c.Delivery.CurrentVoyage)
124 case cargo.Claimed:
125 return "Claimed"
126 default:
127 return "Unknown"
128 }
129 }
130
131 func assembleEvents(c *cargo.Cargo, r cargo.HandlingEventRepository) []Event {
132 h := r.QueryHandlingHistory(c.TrackingID)
133
134 var events []Event
135 for _, e := range h.HandlingEvents {
136 var description string
137
138 switch e.Activity.Type {
139 case cargo.NotHandled:
140 description = "Cargo has not yet been received."
141 case cargo.Receive:
142 description = fmt.Sprintf("Received in %s, at %s", e.Activity.Location, time.Now().Format(time.RFC3339))
143 case cargo.Load:
144 description = fmt.Sprintf("Loaded onto voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))
145 case cargo.Unload:
146 description = fmt.Sprintf("Unloaded off voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))
147 case cargo.Claim:
148 description = fmt.Sprintf("Claimed in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))
149 case cargo.Customs:
150 description = fmt.Sprintf("Cleared customs in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))
151 default:
152 description = "[Unknown status]"
153 }
154
155 events = append(events, Event{
156 Description: description,
157 Expected: c.Itinerary.IsExpected(e),
158 })
159 }
160
161 return events
162 }
0 package tracking
1
2 import (
3 "encoding/json"
4 "errors"
5 "net/http"
6
7 "github.com/gorilla/mux"
8 "golang.org/x/net/context"
9
10 "github.com/go-kit/kit/examples/shipping/cargo"
11 kitlog "github.com/go-kit/kit/log"
12 kithttp "github.com/go-kit/kit/transport/http"
13 )
14
15 // MakeHandler returns a handler for the tracking service.
16 func MakeHandler(ctx context.Context, ts Service, logger kitlog.Logger) http.Handler {
17 r := mux.NewRouter()
18
19 opts := []kithttp.ServerOption{
20 kithttp.ServerErrorLogger(logger),
21 kithttp.ServerErrorEncoder(encodeError),
22 }
23
24 trackCargoHandler := kithttp.NewServer(
25 ctx,
26 makeTrackCargoEndpoint(ts),
27 decodeTrackCargoRequest,
28 encodeResponse,
29 opts...,
30 )
31
32 r.Handle("/tracking/v1/cargos/{id}", trackCargoHandler).Methods("GET")
33
34 return r
35 }
36
37 func decodeTrackCargoRequest(r *http.Request) (interface{}, error) {
38 vars := mux.Vars(r)
39 id, ok := vars["id"]
40 if !ok {
41 return nil, errors.New("bad route")
42 }
43 return trackCargoRequest{ID: id}, nil
44 }
45
46 func encodeResponse(w http.ResponseWriter, response interface{}) error {
47 if e, ok := response.(errorer); ok && e.error() != nil {
48 encodeError(w, e.error())
49 return nil
50 }
51 w.Header().Set("Content-Type", "application/json; charset=utf-8")
52 return json.NewEncoder(w).Encode(response)
53 }
54
55 type errorer interface {
56 error() error
57 }
58
59 // encode errors from business-logic
60 func encodeError(w http.ResponseWriter, err error) {
61 switch err {
62 case cargo.ErrUnknown:
63 w.WriteHeader(http.StatusNotFound)
64 case ErrInvalidArgument:
65 w.WriteHeader(http.StatusBadRequest)
66 default:
67 w.WriteHeader(http.StatusInternalServerError)
68 }
69 w.Header().Set("Content-Type", "application/json; charset=utf-8")
70 json.NewEncoder(w).Encode(map[string]interface{}{
71 "error": err.Error(),
72 })
73 }
0 package voyage
1
2 import "github.com/go-kit/kit/examples/shipping/location"
3
4 // A set of sample voyages.
5 var (
6 V100 = New("V100", Schedule{
7 []CarrierMovement{
8 {DepartureLocation: location.Hongkong, ArrivalLocation: location.Tokyo},
9 {DepartureLocation: location.Tokyo, ArrivalLocation: location.NewYork},
10 },
11 })
12
13 V300 = New("V300", Schedule{
14 []CarrierMovement{
15 {DepartureLocation: location.Tokyo, ArrivalLocation: location.Rotterdam},
16 {DepartureLocation: location.Rotterdam, ArrivalLocation: location.Hamburg},
17 {DepartureLocation: location.Hamburg, ArrivalLocation: location.Melbourne},
18 {DepartureLocation: location.Melbourne, ArrivalLocation: location.Tokyo},
19 },
20 })
21
22 V400 = New("V400", Schedule{
23 []CarrierMovement{
24 {DepartureLocation: location.Hamburg, ArrivalLocation: location.Stockholm},
25 {DepartureLocation: location.Stockholm, ArrivalLocation: location.Helsinki},
26 {DepartureLocation: location.Helsinki, ArrivalLocation: location.Hamburg},
27 },
28 })
29 )
30
31 // These voyages are hard-coded into the current pathfinder. Make sure
32 // they exist.
33 var (
34 V0100S = New("0100S", Schedule{[]CarrierMovement{}})
35 V0200T = New("0200T", Schedule{[]CarrierMovement{}})
36 V0300A = New("0300A", Schedule{[]CarrierMovement{}})
37 V0301S = New("0301S", Schedule{[]CarrierMovement{}})
38 V0400S = New("0400S", Schedule{[]CarrierMovement{}})
39 )
0 // Package voyage provides the Voyage aggregate.
1 package voyage
2
3 import (
4 "errors"
5 "time"
6
7 "github.com/go-kit/kit/examples/shipping/location"
8 )
9
10 // Number uniquely identifies a particular Voyage.
11 type Number string
12
13 // Voyage is a uniquely identifiable series of carrier movements.
14 type Voyage struct {
15 Number Number
16 Schedule Schedule
17 }
18
19 // New creates a voyage with a voyage number and a provided schedule.
20 func New(n Number, s Schedule) *Voyage {
21 return &Voyage{Number: n, Schedule: s}
22 }
23
24 // Schedule describes a voyage schedule.
25 type Schedule struct {
26 CarrierMovements []CarrierMovement
27 }
28
29 // CarrierMovement is a vessel voyage from one location to another.
30 type CarrierMovement struct {
31 DepartureLocation location.Location
32 ArrivalLocation location.Location
33 DepartureTime time.Time
34 ArrivalTime time.Time
35 }
36
37 // ErrUnknown is used when a voyage could not be found.
38 var ErrUnknown = errors.New("unknown voyage")
39
40 // Repository provides access a voyage store.
41 type Repository interface {
42 Find(Number) (*Voyage, error)
43 }
11
22 import (
33 "fmt"
4 "reflect"
45
56 "golang.org/x/net/context"
67 "google.golang.org/grpc"
1718 method string
1819 enc EncodeRequestFunc
1920 dec DecodeResponseFunc
20 grpcReply interface{}
21 grpcReply reflect.Type
2122 before []RequestFunc
2223 }
2324
3233 options ...ClientOption,
3334 ) *Client {
3435 c := &Client{
35 client: cc,
36 method: fmt.Sprintf("/pb.%s/%s", serviceName, method),
37 enc: enc,
38 dec: dec,
39 grpcReply: grpcReply,
40 before: []RequestFunc{},
36 client: cc,
37 method: fmt.Sprintf("/pb.%s/%s", serviceName, method),
38 enc: enc,
39 dec: dec,
40 // We are using reflect.Indirect here to allow both reply structs and
41 // pointers to these reply structs. New consumers of the client should
42 // use structs directly, while existing consumers will not break if they
43 // remain to use pointers to structs.
44 grpcReply: reflect.TypeOf(
45 reflect.Indirect(
46 reflect.ValueOf(grpcReply),
47 ).Interface(),
48 ),
49 before: []RequestFunc{},
4150 }
4251 for _, option := range options {
4352 option(c)
7281 }
7382 ctx = metadata.NewContext(ctx, *md)
7483
75 if err = grpc.Invoke(ctx, c.method, req, c.grpcReply, c.client); err != nil {
84 grpcReply := reflect.New(c.grpcReply).Interface()
85 if err = grpc.Invoke(ctx, c.method, req, grpcReply, c.client); err != nil {
7686 return nil, fmt.Errorf("Invoke: %v", err)
7787 }
7888
79 response, err := c.dec(ctx, c.grpcReply)
89 response, err := c.dec(ctx, grpcReply)
8090 if err != nil {
8191 return nil, fmt.Errorf("Decode: %v", err)
8292 }
0 # package transport/httprp
1
2 `package transport/httprp` provides an HTTP reverse-proxy transport.
3
4 ## Rationale
5
6 HTTP server applications often associate multiple handlers with a single HTTP listener, each handler differentiated by the request URI and/or HTTP method. Handlers that perform business-logic in the app can implement the `Endpoint` interface and be exposed using the `package transport/http` server. Handlers that need to proxy the request to another HTTP endpoint can do so with this package by simply specifying the base URL to forward the request to.
7
8 ## Usage
9
10 The following example uses the [Gorilla Mux](https://github.com/gorilla/mux) router to illustrate how a mixture of proxying and non-proxying request handlers can be used with a single listener:
11
12 ```go
13 import (
14 "net/http"
15 "net/url"
16
17 kithttp "github.com/go-kit/kit/transport/http"
18 kithttprp "github.com/go-kit/kit/transport/httprp"
19 "github.com/gorilla/mux"
20 "golang.org/x/net/context"
21 )
22
23 func main() {
24 router := mux.NewRouter()
25
26 // server HTTP endpoint handled here
27 router.Handle("/foo",
28 kithttp.NewServer(
29 context.Background(),
30 func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
31 func(*http.Request) (interface{}, error) { return struct{}{}, nil },
32 func(http.ResponseWriter, interface{}) error { return nil },
33 )).Methods("GET")
34
35 // proxy endpoint, forwards requests to http://other.service.local/base/bar
36 remoteServiceURL, _ := url.Parse("http://other.service.local/base")
37 router.Handle("/bar",
38 kithttprp.NewServer(
39 context.Background(),
40 remoteServiceURL,
41 )).Methods("GET")
42
43 http.ListenAndServe(":8080", router)
44 }
45 ```
46
47 You can also supply a set of `RequestFunc` functions to be run before proxying the request. This can be useful for adding request headers required by the backend system (e.g. API tokens).
0 package httprp
1
2 import (
3 "net/http"
4 "net/http/httputil"
5 "net/url"
6
7 "golang.org/x/net/context"
8 )
9
10 // RequestFunc may take information from an HTTP request and put it into a
11 // request context. BeforeFuncs are executed prior to invoking the
12 // endpoint.
13 type RequestFunc func(context.Context, *http.Request) context.Context
14
15 // Server is a proxying request handler.
16 type Server struct {
17 ctx context.Context
18 proxy http.Handler
19 before []RequestFunc
20 errorEncoder func(w http.ResponseWriter, err error)
21 }
22
23 // NewServer constructs a new server that implements http.Server and will proxy
24 // requests to the given base URL using its scheme, host, and base path.
25 // If the target's path is "/base" and the incoming request was for "/dir",
26 // the target request will be for /base/dir.
27 func NewServer(
28 ctx context.Context,
29 baseURL *url.URL,
30 options ...ServerOption,
31 ) *Server {
32 s := &Server{
33 ctx: ctx,
34 proxy: httputil.NewSingleHostReverseProxy(baseURL),
35 }
36 for _, option := range options {
37 option(s)
38 }
39 return s
40 }
41
42 // ServerOption sets an optional parameter for servers.
43 type ServerOption func(*Server)
44
45 // ServerBefore functions are executed on the HTTP request object before the
46 // request is decoded.
47 func ServerBefore(before ...RequestFunc) ServerOption {
48 return func(s *Server) { s.before = before }
49 }
50
51 // ServeHTTP implements http.Handler.
52 func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
53 ctx, cancel := context.WithCancel(s.ctx)
54 defer cancel()
55
56 for _, f := range s.before {
57 ctx = f(ctx, r)
58 }
59
60 s.proxy.ServeHTTP(w, r)
61 }
0 package httprp_test
1
2 import (
3 "io/ioutil"
4 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "testing"
8
9 "golang.org/x/net/context"
10
11 httptransport "github.com/go-kit/kit/transport/httprp"
12 )
13
14 func TestServerHappyPathSingleServer(t *testing.T) {
15 originServer := httptest.NewServer(
16 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17 w.WriteHeader(http.StatusOK)
18 w.Write([]byte("hey"))
19 }))
20 defer originServer.Close()
21 originURL, _ := url.Parse(originServer.URL)
22
23 handler := httptransport.NewServer(
24 context.Background(),
25 originURL,
26 )
27 proxyServer := httptest.NewServer(handler)
28 defer proxyServer.Close()
29
30 resp, _ := http.Get(proxyServer.URL)
31 if want, have := http.StatusOK, resp.StatusCode; want != have {
32 t.Errorf("want %d, have %d", want, have)
33 }
34
35 responseBody, _ := ioutil.ReadAll(resp.Body)
36 if want, have := "hey", string(responseBody); want != have {
37 t.Errorf("want %d, have %d", want, have)
38 }
39 }
40
41 func TestServerHappyPathSingleServerWithServerOptions(t *testing.T) {
42 const (
43 headerKey = "X-TEST-HEADER"
44 headerVal = "go-kit-proxy"
45 )
46
47 originServer := httptest.NewServer(
48 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49 if want, have := headerVal, r.Header.Get(headerKey); want != have {
50 t.Errorf("want %d, have %d", want, have)
51 }
52
53 w.WriteHeader(http.StatusOK)
54 w.Write([]byte("hey"))
55 }))
56 defer originServer.Close()
57 originURL, _ := url.Parse(originServer.URL)
58
59 handler := httptransport.NewServer(
60 context.Background(),
61 originURL,
62 httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context {
63 r.Header.Add(headerKey, headerVal)
64 return ctx
65 }),
66 )
67 proxyServer := httptest.NewServer(handler)
68 defer proxyServer.Close()
69
70 resp, _ := http.Get(proxyServer.URL)
71 if want, have := http.StatusOK, resp.StatusCode; want != have {
72 t.Errorf("want %d, have %d", want, have)
73 }
74
75 responseBody, _ := ioutil.ReadAll(resp.Body)
76 if want, have := "hey", string(responseBody); want != have {
77 t.Errorf("want %d, have %d", want, have)
78 }
79 }
80
81 func TestServerOriginServerNotFoundResponse(t *testing.T) {
82 originServer := httptest.NewServer(http.NotFoundHandler())
83 defer originServer.Close()
84 originURL, _ := url.Parse(originServer.URL)
85
86 handler := httptransport.NewServer(
87 context.Background(),
88 originURL,
89 )
90 proxyServer := httptest.NewServer(handler)
91 defer proxyServer.Close()
92
93 resp, _ := http.Get(proxyServer.URL)
94 if want, have := http.StatusNotFound, resp.StatusCode; want != have {
95 t.Errorf("want %d, have %d", want, have)
96 }
97 }
98
99 func TestServerOriginServerUnreachable(t *testing.T) {
100 // create a server, then promptly shut it down
101 originServer := httptest.NewServer(
102 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
103 w.WriteHeader(http.StatusOK)
104 }))
105 originURL, _ := url.Parse(originServer.URL)
106 originServer.Close()
107
108 handler := httptransport.NewServer(
109 context.Background(),
110 originURL,
111 )
112 proxyServer := httptest.NewServer(handler)
113 defer proxyServer.Close()
114
115 resp, _ := http.Get(proxyServer.URL)
116 if want, have := http.StatusInternalServerError, resp.StatusCode; want != have {
117 t.Errorf("want %d, have %d", want, have)
118 }
119 }