Codebase list golang-github-go-kit-kit / 6af91a5
Merge pull request #210 from marcusolsson/shipping-example Add shipping example Peter Bourgon 8 years ago
26 changed file(s) with 2342 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
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 }