From 1b3cbff698056811200ba5406a890a30d1e2e14f Mon Sep 17 00:00:00 2001 From: adamross Date: Fri, 15 Jun 2018 15:11:53 -0700 Subject: [PATCH] Add shipping service. --- src/shippingservice/.gitkeep | 0 src/shippingservice/Dockerfile | 31 ++++++++ src/shippingservice/README.md | 17 +++++ src/shippingservice/main.go | 79 +++++++++++++++++++++ src/shippingservice/quote.go | 41 +++++++++++ src/shippingservice/shippingservice_test.go | 75 +++++++++++++++++++ src/shippingservice/tracker.go | 42 +++++++++++ 7 files changed, 285 insertions(+) delete mode 100644 src/shippingservice/.gitkeep create mode 100644 src/shippingservice/Dockerfile create mode 100644 src/shippingservice/README.md create mode 100644 src/shippingservice/main.go create mode 100644 src/shippingservice/quote.go create mode 100644 src/shippingservice/shippingservice_test.go create mode 100644 src/shippingservice/tracker.go diff --git a/src/shippingservice/.gitkeep b/src/shippingservice/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/shippingservice/Dockerfile b/src/shippingservice/Dockerfile new file mode 100644 index 0000000..b42267d --- /dev/null +++ b/src/shippingservice/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.10.3-alpine3.7 as deps + +RUN apk add --no-cache \ + ca-certificates \ + curl \ + git \ + gcc \ + libffi-dev \ + make \ + musl-dev \ + protobuf \ + tar + +ENV PATH=$PATH:$GOPATH/bin +RUN go get -u google.golang.org/grpc && \ + go get github.com/golang/protobuf/protoc-gen-go + +FROM deps as builder +WORKDIR $GOPATH/src/microservices-demo/shipping +COPY src/shippingservice $GOPATH/src/microservices-demo/shipping +COPY pb/demo.proto $GOPATH/src/microservices-demo/pb/demo.proto + +RUN protoc -I ../pb/ ../pb/demo.proto --go_out=plugins=grpc:../pb +RUN go build -o /shipping/shipit + +FROM golang:1.10.3-alpine3.7 as release +COPY --from=builder /shipping/shipit /shipit +ENV APP_PORT=50051 + +# @TODO add light init system. +ENTRYPOINT /shipit diff --git a/src/shippingservice/README.md b/src/shippingservice/README.md new file mode 100644 index 0000000..a98d1ed --- /dev/null +++ b/src/shippingservice/README.md @@ -0,0 +1,17 @@ +# Shipping Service + +The Shipping service provides price quote, tracking IDs, and the impression of order fulfillment & shipping processes. + +## Build + +From repository root, run: + +``` +docker build --file src/shippingservice/Dockerfile . +``` + +## Test + +``` +go test . +``` \ No newline at end of file diff --git a/src/shippingservice/main.go b/src/shippingservice/main.go new file mode 100644 index 0000000..e3d0709 --- /dev/null +++ b/src/shippingservice/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "log" + "net" + "os" + + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + + pb "microservices-demo/pb" +) + + +const ( + default_port = "50051" +) + +// server controls RPC service responses. +type server struct{} + +// GetQuote produces a shipping quote (cost) in USD. +func (s *server) GetQuote(ctx context.Context, in *pb.GetQuoteRequest) (*pb.GetQuoteResponse, error) { + + // 1. Our quote system requires the total number of items to be shipped. + count := 0 + for _, item := range in.Items { + count += int(item.Quantity) + } + + // 2. Generate a quote based on the total number of items to be shipped. + quote := CreateQuoteFromCount(count) + + // 3. Generate a response. + return &pb.GetQuoteResponse{ + CostUsd: &pb.MoneyAmount{ + Decimal: quote.Dollars, + Fractional: quote.Cents, + }, + }, nil + +} + +// ShipOrder mocks that the requested items will be shipped. +// It supplies a tracking ID for notional lookup of shipment delivery status. +func (s *server) ShipOrder(ctx context.Context, in *pb.ShipOrderRequest) (*pb.ShipOrderResponse, error) { + // 1. Create a Tracking ID + baseAddress := fmt.Sprintf("%s, %s, %s", in.Address.StreetAddress_1, in.Address.StreetAddress_2, in.Address.City) + id := CreateTrackingId(baseAddress) + + // 2. Generate a response. + return &pb.ShipOrderResponse{ + TrackingId: id, + }, nil +} + +func main() { + port := default_port + if value, ok := os.LookupEnv("APP_PORT"); ok { + port = value + } + port = fmt.Sprintf(":%s", port) + + lis, err := net.Listen("tcp", port) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + pb.RegisterShippingServiceServer(s, &server{}) + log.Printf("Shipping Service listening on port %s", port) + + // Register reflection service on gRPC server. + reflection.Register(s) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} \ No newline at end of file diff --git a/src/shippingservice/quote.go b/src/shippingservice/quote.go new file mode 100644 index 0000000..036b4c5 --- /dev/null +++ b/src/shippingservice/quote.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "math" +) + +// Quote represents a currency value. +type Quote struct { + Dollars uint32 + Cents uint32 +} + +// String representation of the Quote. +func (q Quote) String() string { + return fmt.Sprintf("$%d.%d", q.Dollars, q.Cents) +} + +// CreateQuoteFromCount takes a number of items and returns a Price struct. +func CreateQuoteFromCount(count int) Quote { + return CreateQuoteFromFloat(quoteByCountFloat(count)) +} + +// CreateQuoteFromFloat takes a price represented as a float and creates a Price struct. +func CreateQuoteFromFloat(value float64) Quote { + units, fraction := math.Modf(value) + return Quote{ + uint32(units), + uint32(math.Trunc(fraction * 100)), + } +} + +// quoteByCountFloat takes a number of items and generates a price quote represented as a float. +func quoteByCountFloat(count int) float64 { + if count == 0 { + return 0 + } + count64 := float64(count) + var p float64 = 1 + (count64 * 0.2) + return count64 + math.Pow(3, p) +} diff --git a/src/shippingservice/shippingservice_test.go b/src/shippingservice/shippingservice_test.go new file mode 100644 index 0000000..3c9ea4f --- /dev/null +++ b/src/shippingservice/shippingservice_test.go @@ -0,0 +1,75 @@ +package main + +import( + "testing" + "golang.org/x/net/context" + + pb "microservices-demo/pb" +) + +// TestGetQuote is a basic check on the GetQuote RPC service. +func TestGetQuote(t *testing.T) { + s := server{} + + // A basic test case to test logic and protobuf interactions. + req := &pb.GetQuoteRequest{ + Address: &pb.Address{ + StreetAddress_1: "Muffin Man", + StreetAddress_2: "Drury Lane", + City: "London", + Country: "England", + }, + Items: []*pb.CartItem{ + { + ProductId: "23", + Quantity: 1, + }, + { + ProductId: "46", + Quantity: 3, + }, + }, + } + + res, err := s.GetQuote(context.Background(), req) + if err != nil { + t.Errorf("TestGetQuote (%v) failed", err) + } + if res.CostUsd.Decimal != 11 || res.CostUsd.Fractional != 22 { + t.Errorf("TestGetQuote: Quote value '%d.%d' does not match expected '%s'", res.CostUsd.Decimal, res.CostUsd.Fractional, "11.22") + } +} + +// TestShipOrder is a basic check on the ShipOrder RPC service. +func TestShipOrder(t *testing.T) { + s := server{} + + // A basic test case to test logic and protobuf interactions. + req := &pb.ShipOrderRequest{ + Address: &pb.Address{ + StreetAddress_1: "Muffin Man", + StreetAddress_2: "Drury Lane", + City: "London", + Country: "England", + }, + Items: []*pb.CartItem{ + { + ProductId: "23", + Quantity: 1, + }, + { + ProductId: "46", + Quantity: 3, + }, + }, + } + + res, err := s.ShipOrder(context.Background(), req) + if err != nil { + t.Errorf("TestShipOrder (%v) failed", err) + } + // @todo improve quality of this test to check for a pattern such as '[A-Z]{2}-\d+-\d+'. + if len(res.TrackingId) != 18 { + t.Errorf("TestShipOrder: Tracking ID is malformed - has %d characters, %d expected", len(res.TrackingId), 18) + } +} \ No newline at end of file diff --git a/src/shippingservice/tracker.go b/src/shippingservice/tracker.go new file mode 100644 index 0000000..de8259a --- /dev/null +++ b/src/shippingservice/tracker.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "math/rand" + "time" +) + +// seeded determines if the random number generator is ready. +var seeded bool = false + +// CreateTrackingId generates a tracking ID. +func CreateTrackingId(salt string) string { + if !seeded { + rand.Seed(time.Now().UnixNano()) + seeded = true + } + + return fmt.Sprintf("%c%c-%d%s-%d%s", + getRandomLetterCode(), + getRandomLetterCode(), + len(salt), + getRandomNumber(3), + len(salt)/2, + getRandomNumber(7), + ) +} + +// getRandomLetterCode generates a code point value for a capital letter. +func getRandomLetterCode() uint32 { + return 65 + uint32(rand.Intn(27)) +} + +// getRandomNumber generates a string representation of a number with the requested number of digits. +func getRandomNumber(digits int) string { + str := "" + for i := 0; i < digits; i++ { + str = fmt.Sprintf("%s%d", str, rand.Intn(10)) + } + + return str +}