Merge pull request #210 from marcusolsson/shipping-example
Add shipping example
Peter Bourgon
8 years ago
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 | } |