Your shopping cart is empty!
+Items you add to your shopping cart will appear here.
+ Continue Shopping +diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..aa8aace --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,46 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Build and Test + +on: + push: + branches: + - 'main' + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '1.20' + cache: true + + - name: build + run: go build ./... + - name: vet + run: go vet ./... + - name: test + run: go test ./... + - name: testrace + run: go test -race ./... + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc5f465 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Online Boutique + +This directory contains the port of the Google Cloud's [`Online +Boutique`][boutique] demo application to Service Weaver. + +```mermaid +%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%% +graph TD + %% Nodes. + github.com/ServiceWeaver/weaver/Main(weaver.Main) + github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice/T(adservice.T) + github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T(cartservice.T) + github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache(cartservice.cartCache) + github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T(checkoutservice.T) + github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T(currencyservice.T) + github.com/ServiceWeaver/weaver/examples/onlineboutique/emailservice/T(emailservice.T) + github.com/ServiceWeaver/weaver/examples/onlineboutique/paymentservice/T(paymentservice.T) + github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T(productcatalogservice.T) + github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice/T(recommendationservice.T) + github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T(shippingservice.T) + + %% Edges. + github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice/T + github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T + github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T + github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T + github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T + github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice/T + github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T + github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T --> github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache + github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T --> github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T + github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T --> github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T + github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T --> github.com/ServiceWeaver/weaver/examples/onlineboutique/emailservice/T + github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T --> github.com/ServiceWeaver/weaver/examples/onlineboutique/paymentservice/T + github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T --> github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T + github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T --> github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T + github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice/T --> github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T +``` + +Here are the changes made to the original application: + +* All of the services that weren't written in `Go` have been ported to `Go`. +* All of the networking calls have been replaced with the corresponding + Service Weaver calls. +* All of the logging/tracing/monitoring calls have been replaced with the + corresponding Service Weaver calls. +* The code is organized as a single Go module. + +[boutique]: https://github.com/GoogleCloudPlatform/microservices-demo diff --git a/adservice/service.go b/adservice/service.go new file mode 100644 index 0000000..12e64ee --- /dev/null +++ b/adservice/service.go @@ -0,0 +1,120 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package adservice + +import ( + "context" + "math/rand" + "strings" + + "golang.org/x/exp/maps" + + "github.com/ServiceWeaver/weaver" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +const ( + maxAdsToServe = 2 +) + +// Ad represents an advertisement. +type Ad struct { + weaver.AutoMarshal + RedirectURL string // URL to redirect to when an ad is clicked. + Text string // Short advertisement text to display. +} + +type T interface { + GetAds(ctx context.Context, keywords []string) ([]Ad, error) +} + +type impl struct { + weaver.Implements[T] + ads map[string]Ad +} + +func (s *impl) Init(context.Context) error { + s.Logger().Info("Ad Service started") + s.ads = createAdsMap() + return nil +} + +// GetAds returns a list of ads that best match the given context keywords. +func (s *impl) GetAds(ctx context.Context, keywords []string) ([]Ad, error) { + s.Logger().Info("received ad request", "keywords", keywords) + span := trace.SpanFromContext(ctx) + var allAds []Ad + if len(keywords) > 0 { + span.AddEvent("Constructing Ads using context", trace.WithAttributes( + attribute.String("Context Keys", strings.Join(keywords, ",")), + attribute.Int("Context Keys length", len(keywords)), + )) + for _, kw := range keywords { + allAds = append(allAds, s.getAdsByCategory(kw)...) + } + if allAds == nil { + // Serve random ads. + span.AddEvent("No Ads found based on context. Constructing random Ads.") + allAds = s.getRandomAds() + } + } else { + span.AddEvent("No Context provided. Constructing random Ads.") + allAds = s.getRandomAds() + } + return allAds, nil +} + +func (s *impl) getAdsByCategory(category string) []Ad { + return []Ad{s.ads[category]} +} + +func (s *impl) getRandomAds() []Ad { + ads := make([]Ad, maxAdsToServe) + vals := maps.Values(s.ads) + for i := 0; i < maxAdsToServe; i++ { + ads[i] = vals[rand.Intn(len(vals))] + } + return ads +} + +func createAdsMap() map[string]Ad { + return map[string]Ad{ + "hair": { + RedirectURL: "/product/2ZYFJ3GM2N", + Text: "Hairdryer for sale. 50% off.", + }, + "clothing": { + RedirectURL: "/product/66VCHSJNUP", + Text: "Tank top for sale. 20% off.", + }, + "accessories": { + RedirectURL: "/product/1YMWWN1N4O", + Text: "Watch for sale. Buy one, get second kit for free", + }, + "footwear": { + RedirectURL: "/product/L9ECAV7KIM", + Text: "Loafers for sale. Buy one, get second one for free", + }, + "decor": { + RedirectURL: "/product/0PUK6V6EV0", + Text: "Candle holder for sale. 30% off.", + }, + "kitchen": { + RedirectURL: "/product/9SIQT8TOJO", + Text: "Bamboo glass jar for sale. 10% off.", + }, + } +} diff --git a/adservice/weaver_gen.go b/adservice/weaver_gen.go new file mode 100644 index 0000000..c1e684c --- /dev/null +++ b/adservice/weaver_gen.go @@ -0,0 +1,272 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package adservice + +import ( + "context" + "errors" + "fmt" + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/runtime/codegen" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "reflect" +) + +var _ codegen.LatestVersion = codegen.Version[[0][17]struct{}](` + +ERROR: You generated this file with 'weaver generate' v0.18.0 (codegen +version v0.17.0). The generated code is incompatible with the version of the +github.com/ServiceWeaver/weaver module that you're using. The weaver module +version can be found in your go.mod file or by running the following command. + + go list -m github.com/ServiceWeaver/weaver + +We recommend updating the weaver module and the 'weaver generate' command by +running the following. + + go get github.com/ServiceWeaver/weaver@latest + go install github.com/ServiceWeaver/weaver/cmd/weaver@latest + +Then, re-run 'weaver generate' and re-build your code. If the problem persists, +please file an issue at https://github.com/ServiceWeaver/weaver/issues. + +`) + +func init() { + codegen.Register(codegen.Registration{ + Name: "github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice/T", + Iface: reflect.TypeOf((*T)(nil)).Elem(), + Impl: reflect.TypeOf(impl{}), + LocalStubFn: func(impl any, caller string, tracer trace.Tracer) any { + return t_local_stub{impl: impl.(T), tracer: tracer, getAdsMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice/T", Method: "GetAds", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, getAdsMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice/T", Method: "GetAds", Remote: true})} + }, + ServerStubFn: func(impl any, addLoad func(uint64, float64)) codegen.Server { + return t_server_stub{impl: impl.(T), addLoad: addLoad} + }, + RefData: "", + }) +} + +// weaver.InstanceOf checks. +var _ weaver.InstanceOf[T] = (*impl)(nil) + +// weaver.Router checks. +var _ weaver.Unrouted = (*impl)(nil) + +// Local stub implementations. + +type t_local_stub struct { + impl T + tracer trace.Tracer + getAdsMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) GetAds(ctx context.Context, a0 []string) (r0 []Ad, err error) { + // Update metrics. + begin := s.getAdsMetrics.Begin() + defer func() { s.getAdsMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "adservice.T.GetAds", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.GetAds(ctx, a0) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + getAdsMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) GetAds(ctx context.Context, a0 []string) (r0 []Ad, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.getAdsMetrics.Begin() + defer func() { s.getAdsMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "adservice.T.GetAds", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Encode arguments. + enc := codegen.NewEncoder() + serviceweaver_enc_slice_string_4af10117(enc, a0) + var shardKey uint64 + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 0, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + r0 = serviceweaver_dec_slice_Ad_86ae3655(dec) + err = dec.Error() + return +} + +// Server stub implementations. + +type t_server_stub struct { + impl T + addLoad func(key uint64, load float64) +} + +// Check that t_server_stub implements the codegen.Server interface. +var _ codegen.Server = (*t_server_stub)(nil) + +// GetStubFn implements the codegen.Server interface. +func (s t_server_stub) GetStubFn(method string) func(ctx context.Context, args []byte) ([]byte, error) { + switch method { + case "GetAds": + return s.getAds + default: + return nil + } +} + +func (s t_server_stub) getAds(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 []string + a0 = serviceweaver_dec_slice_string_4af10117(dec) + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + r0, appErr := s.impl.GetAds(ctx, a0) + + // Encode the results. + enc := codegen.NewEncoder() + serviceweaver_enc_slice_Ad_86ae3655(enc, r0) + enc.Error(appErr) + return enc.Data(), nil +} + +// AutoMarshal implementations. + +var _ codegen.AutoMarshal = (*Ad)(nil) + +type __is_Ad[T ~struct { + weaver.AutoMarshal + RedirectURL string + Text string +}] struct{} + +var _ __is_Ad[Ad] + +func (x *Ad) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("Ad.WeaverMarshal: nil receiver")) + } + enc.String(x.RedirectURL) + enc.String(x.Text) +} + +func (x *Ad) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("Ad.WeaverUnmarshal: nil receiver")) + } + x.RedirectURL = dec.String() + x.Text = dec.String() +} + +// Encoding/decoding implementations. + +func serviceweaver_enc_slice_string_4af10117(enc *codegen.Encoder, arg []string) { + if arg == nil { + enc.Len(-1) + return + } + enc.Len(len(arg)) + for i := 0; i < len(arg); i++ { + enc.String(arg[i]) + } +} + +func serviceweaver_dec_slice_string_4af10117(dec *codegen.Decoder) []string { + n := dec.Len() + if n == -1 { + return nil + } + res := make([]string, n) + for i := 0; i < n; i++ { + res[i] = dec.String() + } + return res +} + +func serviceweaver_enc_slice_Ad_86ae3655(enc *codegen.Encoder, arg []Ad) { + if arg == nil { + enc.Len(-1) + return + } + enc.Len(len(arg)) + for i := 0; i < len(arg); i++ { + (arg[i]).WeaverMarshal(enc) + } +} + +func serviceweaver_dec_slice_Ad_86ae3655(dec *codegen.Decoder) []Ad { + n := dec.Len() + if n == -1 { + return nil + } + res := make([]Ad, n) + for i := 0; i < n; i++ { + (&res[i]).WeaverUnmarshal(dec) + } + return res +} diff --git a/cartservice/cache.go b/cartservice/cache.go new file mode 100644 index 0000000..3f9f443 --- /dev/null +++ b/cartservice/cache.go @@ -0,0 +1,78 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cartservice + +import ( + "context" + + "github.com/ServiceWeaver/weaver" + lru "github.com/hashicorp/golang-lru/v2" +) + +const cacheSize = 1 << 20 // 1M entries + +type errNotFound struct{} + +var _ error = errNotFound{} + +func (e errNotFound) Error() string { return "not found" } + +// TODO(spetrovic): Allow the cache struct to reside in a different package. + +type cartCache interface { + Add(context.Context, string, []CartItem) error + Get(context.Context, string) ([]CartItem, error) + Remove(context.Context, string) (bool, error) +} + +type cartCacheImpl struct { + weaver.Implements[cartCache] + weaver.WithRouter[cartCacheRouter] + + cache *lru.Cache[string, []CartItem] +} + +func (c *cartCacheImpl) Init(context.Context) error { + cache, err := lru.New[string, []CartItem](cacheSize) + c.cache = cache + return err +} + +// Add adds the given (key, val) pair to the cache. +func (c *cartCacheImpl) Add(_ context.Context, key string, val []CartItem) error { + c.cache.Add(key, val) + return nil +} + +// Get returns the value associated with the given key in the cache, or +// ErrNotFound if there is no associated value. +func (c *cartCacheImpl) Get(_ context.Context, key string) ([]CartItem, error) { + val, ok := c.cache.Get(key) + if !ok { + return nil, errNotFound{} + } + return val, nil +} + +// Remove removes an entry with the given key from the cache. +func (c *cartCacheImpl) Remove(_ context.Context, key string) (bool, error) { + return c.cache.Remove(key), nil +} + +type cartCacheRouter struct{} + +func (cartCacheRouter) Add(_ context.Context, key string, value []CartItem) string { return key } +func (cartCacheRouter) Get(_ context.Context, key string) string { return key } +func (cartCacheRouter) Remove(_ context.Context, key string) string { return key } diff --git a/cartservice/service.go b/cartservice/service.go new file mode 100644 index 0000000..0f54882 --- /dev/null +++ b/cartservice/service.go @@ -0,0 +1,60 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cartservice + +import ( + "context" + + "github.com/ServiceWeaver/weaver" +) + +type CartItem struct { + weaver.AutoMarshal + ProductID string + Quantity int32 +} + +type T interface { + AddItem(ctx context.Context, userID string, item CartItem) error + GetCart(ctx context.Context, userID string) ([]CartItem, error) + EmptyCart(ctx context.Context, userID string) error +} + +type impl struct { + weaver.Implements[T] + cache weaver.Ref[cartCache] + store *cartStore +} + +func (s *impl) Init(context.Context) error { + store, err := newCartStore(s.Logger(), s.cache.Get()) + s.store = store + return err +} + +// AddItem adds a given item to the user's cart. +func (s *impl) AddItem(ctx context.Context, userID string, item CartItem) error { + return s.store.AddItem(ctx, userID, item.ProductID, item.Quantity) +} + +// GetCart returns the items in the user's cart. +func (s *impl) GetCart(ctx context.Context, userID string) ([]CartItem, error) { + return s.store.GetCart(ctx, userID) +} + +// EmptyCart empties the user's cart. +func (s *impl) EmptyCart(ctx context.Context, userID string) error { + return s.store.EmptyCart(ctx, userID) +} diff --git a/cartservice/store.go b/cartservice/store.go new file mode 100644 index 0000000..84a45c1 --- /dev/null +++ b/cartservice/store.go @@ -0,0 +1,79 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cartservice + +import ( + "context" + "errors" + + "golang.org/x/exp/slog" +) + +type cartStore struct { + logger *slog.Logger + cache cartCache +} + +func newCartStore(logger *slog.Logger, cache cartCache) (*cartStore, error) { + return &cartStore{logger: logger, cache: cache}, nil +} + +func (c *cartStore) AddItem(ctx context.Context, userID, productID string, quantity int32) error { + c.logger.Info("AddItem called", "userID", userID, "productID", productID, "quantity", quantity) + // Get the cart from the cache. + cart, err := c.cache.Get(ctx, userID) + if err != nil { + if errors.Is(err, errNotFound{}) { // cache miss + cart = nil + } else { + return err + } + } + + // Copy the cart since the cache may be local, and we don't want to + // overwrite the cache value directly. + copy := make([]CartItem, 0, len(cart)+1) + found := false + for _, item := range cart { + if item.ProductID == productID { + item.Quantity += quantity + found = true + } + copy = append(copy, item) + } + if !found { + copy = append(copy, CartItem{ + ProductID: productID, + Quantity: quantity, + }) + } + + return c.cache.Add(ctx, userID, copy) +} + +func (c *cartStore) EmptyCart(ctx context.Context, userID string) error { + c.logger.Info("EmptyCart called", "userID", userID) + _, err := c.cache.Remove(ctx, userID) + return err +} + +func (c *cartStore) GetCart(ctx context.Context, userID string) ([]CartItem, error) { + c.logger.Info("GetCart called", "userID", userID) + cart, err := c.cache.Get(ctx, userID) + if err != nil && errors.Is(err, errNotFound{}) { + return []CartItem{}, nil + } + return cart, err +} diff --git a/cartservice/weaver_gen.go b/cartservice/weaver_gen.go new file mode 100644 index 0000000..a7e4fa7 --- /dev/null +++ b/cartservice/weaver_gen.go @@ -0,0 +1,872 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package cartservice + +import ( + "context" + "errors" + "fmt" + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/runtime/codegen" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "reflect" +) + +var _ codegen.LatestVersion = codegen.Version[[0][17]struct{}](` + +ERROR: You generated this file with 'weaver generate' v0.18.0 (codegen +version v0.17.0). The generated code is incompatible with the version of the +github.com/ServiceWeaver/weaver module that you're using. The weaver module +version can be found in your go.mod file or by running the following command. + + go list -m github.com/ServiceWeaver/weaver + +We recommend updating the weaver module and the 'weaver generate' command by +running the following. + + go get github.com/ServiceWeaver/weaver@latest + go install github.com/ServiceWeaver/weaver/cmd/weaver@latest + +Then, re-run 'weaver generate' and re-build your code. If the problem persists, +please file an issue at https://github.com/ServiceWeaver/weaver/issues. + +`) + +func init() { + codegen.Register(codegen.Registration{ + Name: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T", + Iface: reflect.TypeOf((*T)(nil)).Elem(), + Impl: reflect.TypeOf(impl{}), + LocalStubFn: func(impl any, caller string, tracer trace.Tracer) any { + return t_local_stub{impl: impl.(T), tracer: tracer, addItemMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T", Method: "AddItem", Remote: false}), emptyCartMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T", Method: "EmptyCart", Remote: false}), getCartMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T", Method: "GetCart", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, addItemMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T", Method: "AddItem", Remote: true}), emptyCartMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T", Method: "EmptyCart", Remote: true}), getCartMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T", Method: "GetCart", Remote: true})} + }, + ServerStubFn: func(impl any, addLoad func(uint64, float64)) codegen.Server { + return t_server_stub{impl: impl.(T), addLoad: addLoad} + }, + RefData: "⟦e78910e9:wEaVeReDgE:github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T→github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache⟧\n", + }) + codegen.Register(codegen.Registration{ + Name: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache", + Iface: reflect.TypeOf((*cartCache)(nil)).Elem(), + Impl: reflect.TypeOf(cartCacheImpl{}), + Routed: true, + LocalStubFn: func(impl any, caller string, tracer trace.Tracer) any { + return cartCache_local_stub{impl: impl.(cartCache), tracer: tracer, addMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache", Method: "Add", Remote: false}), getMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache", Method: "Get", Remote: false}), removeMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache", Method: "Remove", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return cartCache_client_stub{stub: stub, addMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache", Method: "Add", Remote: true}), getMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache", Method: "Get", Remote: true}), removeMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache", Method: "Remove", Remote: true})} + }, + ServerStubFn: func(impl any, addLoad func(uint64, float64)) codegen.Server { + return cartCache_server_stub{impl: impl.(cartCache), addLoad: addLoad} + }, + RefData: "", + }) +} + +// weaver.InstanceOf checks. +var _ weaver.InstanceOf[T] = (*impl)(nil) +var _ weaver.InstanceOf[cartCache] = (*cartCacheImpl)(nil) + +// weaver.Router checks. +var _ weaver.Unrouted = (*impl)(nil) +var _ weaver.RoutedBy[cartCacheRouter] = (*cartCacheImpl)(nil) + +// Component "cartCacheImpl", router "cartCacheRouter" checks. +var _ func(_ context.Context, key string, value []CartItem) string = (&cartCacheRouter{}).Add // routed +var _ func(_ context.Context, key string) string = (&cartCacheRouter{}).Get // routed +var _ func(_ context.Context, key string) string = (&cartCacheRouter{}).Remove // routed + +// Local stub implementations. + +type t_local_stub struct { + impl T + tracer trace.Tracer + addItemMetrics *codegen.MethodMetrics + emptyCartMetrics *codegen.MethodMetrics + getCartMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) AddItem(ctx context.Context, a0 string, a1 CartItem) (err error) { + // Update metrics. + begin := s.addItemMetrics.Begin() + defer func() { s.addItemMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "cartservice.T.AddItem", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.AddItem(ctx, a0, a1) +} + +func (s t_local_stub) EmptyCart(ctx context.Context, a0 string) (err error) { + // Update metrics. + begin := s.emptyCartMetrics.Begin() + defer func() { s.emptyCartMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "cartservice.T.EmptyCart", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.EmptyCart(ctx, a0) +} + +func (s t_local_stub) GetCart(ctx context.Context, a0 string) (r0 []CartItem, err error) { + // Update metrics. + begin := s.getCartMetrics.Begin() + defer func() { s.getCartMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "cartservice.T.GetCart", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.GetCart(ctx, a0) +} + +type cartCache_local_stub struct { + impl cartCache + tracer trace.Tracer + addMetrics *codegen.MethodMetrics + getMetrics *codegen.MethodMetrics + removeMetrics *codegen.MethodMetrics +} + +// Check that cartCache_local_stub implements the cartCache interface. +var _ cartCache = (*cartCache_local_stub)(nil) + +func (s cartCache_local_stub) Add(ctx context.Context, a0 string, a1 []CartItem) (err error) { + // Update metrics. + begin := s.addMetrics.Begin() + defer func() { s.addMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "cartservice.cartCache.Add", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.Add(ctx, a0, a1) +} + +func (s cartCache_local_stub) Get(ctx context.Context, a0 string) (r0 []CartItem, err error) { + // Update metrics. + begin := s.getMetrics.Begin() + defer func() { s.getMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "cartservice.cartCache.Get", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.Get(ctx, a0) +} + +func (s cartCache_local_stub) Remove(ctx context.Context, a0 string) (r0 bool, err error) { + // Update metrics. + begin := s.removeMetrics.Begin() + defer func() { s.removeMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "cartservice.cartCache.Remove", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.Remove(ctx, a0) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + addItemMetrics *codegen.MethodMetrics + emptyCartMetrics *codegen.MethodMetrics + getCartMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) AddItem(ctx context.Context, a0 string, a1 CartItem) (err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.addItemMetrics.Begin() + defer func() { s.addItemMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "cartservice.T.AddItem", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Preallocate a buffer of the right size. + size := 0 + size += (4 + len(a0)) + size += serviceweaver_size_CartItem_e3591e56(&a1) + enc := codegen.NewEncoder() + enc.Reset(size) + + // Encode arguments. + enc.String(a0) + (a1).WeaverMarshal(enc) + var shardKey uint64 + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 0, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + err = dec.Error() + return +} + +func (s t_client_stub) EmptyCart(ctx context.Context, a0 string) (err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.emptyCartMetrics.Begin() + defer func() { s.emptyCartMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "cartservice.T.EmptyCart", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Preallocate a buffer of the right size. + size := 0 + size += (4 + len(a0)) + enc := codegen.NewEncoder() + enc.Reset(size) + + // Encode arguments. + enc.String(a0) + var shardKey uint64 + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 1, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + err = dec.Error() + return +} + +func (s t_client_stub) GetCart(ctx context.Context, a0 string) (r0 []CartItem, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.getCartMetrics.Begin() + defer func() { s.getCartMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "cartservice.T.GetCart", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Preallocate a buffer of the right size. + size := 0 + size += (4 + len(a0)) + enc := codegen.NewEncoder() + enc.Reset(size) + + // Encode arguments. + enc.String(a0) + var shardKey uint64 + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 2, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + r0 = serviceweaver_dec_slice_CartItem_7a7ff11c(dec) + err = dec.Error() + return +} + +type cartCache_client_stub struct { + stub codegen.Stub + addMetrics *codegen.MethodMetrics + getMetrics *codegen.MethodMetrics + removeMetrics *codegen.MethodMetrics +} + +// Check that cartCache_client_stub implements the cartCache interface. +var _ cartCache = (*cartCache_client_stub)(nil) + +func (s cartCache_client_stub) Add(ctx context.Context, a0 string, a1 []CartItem) (err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.addMetrics.Begin() + defer func() { s.addMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "cartservice.cartCache.Add", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Encode arguments. + enc := codegen.NewEncoder() + enc.String(a0) + serviceweaver_enc_slice_CartItem_7a7ff11c(enc, a1) + + // Set the shardKey. + var r cartCacheRouter + shardKey := _hashCartCache(r.Add(ctx, a0, a1)) + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 0, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + err = dec.Error() + return +} + +func (s cartCache_client_stub) Get(ctx context.Context, a0 string) (r0 []CartItem, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.getMetrics.Begin() + defer func() { s.getMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "cartservice.cartCache.Get", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Preallocate a buffer of the right size. + size := 0 + size += (4 + len(a0)) + enc := codegen.NewEncoder() + enc.Reset(size) + + // Encode arguments. + enc.String(a0) + + // Set the shardKey. + var r cartCacheRouter + shardKey := _hashCartCache(r.Get(ctx, a0)) + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 1, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + r0 = serviceweaver_dec_slice_CartItem_7a7ff11c(dec) + err = dec.Error() + return +} + +func (s cartCache_client_stub) Remove(ctx context.Context, a0 string) (r0 bool, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.removeMetrics.Begin() + defer func() { s.removeMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "cartservice.cartCache.Remove", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Preallocate a buffer of the right size. + size := 0 + size += (4 + len(a0)) + enc := codegen.NewEncoder() + enc.Reset(size) + + // Encode arguments. + enc.String(a0) + + // Set the shardKey. + var r cartCacheRouter + shardKey := _hashCartCache(r.Remove(ctx, a0)) + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 2, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + r0 = dec.Bool() + err = dec.Error() + return +} + +// Server stub implementations. + +type t_server_stub struct { + impl T + addLoad func(key uint64, load float64) +} + +// Check that t_server_stub implements the codegen.Server interface. +var _ codegen.Server = (*t_server_stub)(nil) + +// GetStubFn implements the codegen.Server interface. +func (s t_server_stub) GetStubFn(method string) func(ctx context.Context, args []byte) ([]byte, error) { + switch method { + case "AddItem": + return s.addItem + case "EmptyCart": + return s.emptyCart + case "GetCart": + return s.getCart + default: + return nil + } +} + +func (s t_server_stub) addItem(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 string + a0 = dec.String() + var a1 CartItem + (&a1).WeaverUnmarshal(dec) + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + appErr := s.impl.AddItem(ctx, a0, a1) + + // Encode the results. + enc := codegen.NewEncoder() + enc.Error(appErr) + return enc.Data(), nil +} + +func (s t_server_stub) emptyCart(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 string + a0 = dec.String() + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + appErr := s.impl.EmptyCart(ctx, a0) + + // Encode the results. + enc := codegen.NewEncoder() + enc.Error(appErr) + return enc.Data(), nil +} + +func (s t_server_stub) getCart(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 string + a0 = dec.String() + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + r0, appErr := s.impl.GetCart(ctx, a0) + + // Encode the results. + enc := codegen.NewEncoder() + serviceweaver_enc_slice_CartItem_7a7ff11c(enc, r0) + enc.Error(appErr) + return enc.Data(), nil +} + +type cartCache_server_stub struct { + impl cartCache + addLoad func(key uint64, load float64) +} + +// Check that cartCache_server_stub implements the codegen.Server interface. +var _ codegen.Server = (*cartCache_server_stub)(nil) + +// GetStubFn implements the codegen.Server interface. +func (s cartCache_server_stub) GetStubFn(method string) func(ctx context.Context, args []byte) ([]byte, error) { + switch method { + case "Add": + return s.add + case "Get": + return s.get + case "Remove": + return s.remove + default: + return nil + } +} + +func (s cartCache_server_stub) add(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 string + a0 = dec.String() + var a1 []CartItem + a1 = serviceweaver_dec_slice_CartItem_7a7ff11c(dec) + var r cartCacheRouter + s.addLoad(_hashCartCache(r.Add(ctx, a0, a1)), 1.0) + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + appErr := s.impl.Add(ctx, a0, a1) + + // Encode the results. + enc := codegen.NewEncoder() + enc.Error(appErr) + return enc.Data(), nil +} + +func (s cartCache_server_stub) get(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 string + a0 = dec.String() + var r cartCacheRouter + s.addLoad(_hashCartCache(r.Get(ctx, a0)), 1.0) + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + r0, appErr := s.impl.Get(ctx, a0) + + // Encode the results. + enc := codegen.NewEncoder() + serviceweaver_enc_slice_CartItem_7a7ff11c(enc, r0) + enc.Error(appErr) + return enc.Data(), nil +} + +func (s cartCache_server_stub) remove(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 string + a0 = dec.String() + var r cartCacheRouter + s.addLoad(_hashCartCache(r.Remove(ctx, a0)), 1.0) + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + r0, appErr := s.impl.Remove(ctx, a0) + + // Encode the results. + enc := codegen.NewEncoder() + enc.Bool(r0) + enc.Error(appErr) + return enc.Data(), nil +} + +// AutoMarshal implementations. + +var _ codegen.AutoMarshal = (*CartItem)(nil) + +type __is_CartItem[T ~struct { + weaver.AutoMarshal + ProductID string + Quantity int32 +}] struct{} + +var _ __is_CartItem[CartItem] + +func (x *CartItem) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("CartItem.WeaverMarshal: nil receiver")) + } + enc.String(x.ProductID) + enc.Int32(x.Quantity) +} + +func (x *CartItem) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("CartItem.WeaverUnmarshal: nil receiver")) + } + x.ProductID = dec.String() + x.Quantity = dec.Int32() +} + +// Router methods. + +// _hashCartCache returns a 64 bit hash of the provided value. +func _hashCartCache(r string) uint64 { + var h codegen.Hasher + h.WriteString(string(r)) + return h.Sum64() +} + +// _orderedCodeCartCache returns an order-preserving serialization of the provided value. +func _orderedCodeCartCache(r string) codegen.OrderedCode { + var enc codegen.OrderedEncoder + enc.WriteString(string(r)) + return enc.Encode() +} + +// Encoding/decoding implementations. + +func serviceweaver_enc_slice_CartItem_7a7ff11c(enc *codegen.Encoder, arg []CartItem) { + if arg == nil { + enc.Len(-1) + return + } + enc.Len(len(arg)) + for i := 0; i < len(arg); i++ { + (arg[i]).WeaverMarshal(enc) + } +} + +func serviceweaver_dec_slice_CartItem_7a7ff11c(dec *codegen.Decoder) []CartItem { + n := dec.Len() + if n == -1 { + return nil + } + res := make([]CartItem, n) + for i := 0; i < n; i++ { + (&res[i]).WeaverUnmarshal(dec) + } + return res +} + +// Size implementations. + +// serviceweaver_size_CartItem_e3591e56 returns the size (in bytes) of the serialization +// of the provided type. +func serviceweaver_size_CartItem_e3591e56(x *CartItem) int { + size := 0 + size += 0 + size += (4 + len(x.ProductID)) + size += 4 + return size +} diff --git a/checkoutservice/service.go b/checkoutservice/service.go new file mode 100644 index 0000000..421659e --- /dev/null +++ b/checkoutservice/service.go @@ -0,0 +1,153 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package checkoutservice + +import ( + "context" + "fmt" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/emailservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/paymentservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" + "github.com/google/uuid" +) + +type PlaceOrderRequest struct { + weaver.AutoMarshal + UserID string + UserCurrency string + Address shippingservice.Address + Email string + CreditCard paymentservice.CreditCardInfo +} + +type T interface { + PlaceOrder(ctx context.Context, req PlaceOrderRequest) (types.Order, error) +} + +type impl struct { + weaver.Implements[T] + + catalogService weaver.Ref[productcatalogservice.T] + cartService weaver.Ref[cartservice.T] + currencyService weaver.Ref[currencyservice.T] + shippingService weaver.Ref[shippingservice.T] + emailService weaver.Ref[emailservice.T] + paymentService weaver.Ref[paymentservice.T] +} + +func (s *impl) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (types.Order, error) { + s.Logger().Info("[PlaceOrder]", "user_id", req.UserID, "user_currency", req.UserCurrency) + + prep, err := s.prepareOrderItemsAndShippingQuoteFromCart(ctx, req.UserID, req.UserCurrency, req.Address) + if err != nil { + return types.Order{}, err + } + + total := money.T{ + CurrencyCode: req.UserCurrency, + Units: 0, + Nanos: 0, + } + total = money.Must(money.Sum(total, prep.shippingCostLocalized)) + for _, it := range prep.orderItems { + multPrice := money.MultiplySlow(it.Cost, uint32(it.Item.Quantity)) + total = money.Must(money.Sum(total, multPrice)) + } + + txID, err := s.paymentService.Get().Charge(ctx, total, req.CreditCard) + if err != nil { + return types.Order{}, fmt.Errorf("failed to charge card: %w", err) + } + s.Logger().Info("payment went through", "transaction_id", txID) + + shippingTrackingID, err := s.shippingService.Get().ShipOrder(ctx, req.Address, prep.cartItems) + if err != nil { + return types.Order{}, fmt.Errorf("shipping error: %w", err) + } + + _ = s.cartService.Get().EmptyCart(ctx, req.UserID) + + order := types.Order{ + OrderID: uuid.New().String(), + ShippingTrackingID: shippingTrackingID, + ShippingCost: prep.shippingCostLocalized, + ShippingAddress: req.Address, + Items: prep.orderItems, + } + + if err := s.emailService.Get().SendOrderConfirmation(ctx, req.Email, order); err != nil { + s.Logger().Error("failed to send order confirmation", "err", err, "email", req.Email) + } else { + s.Logger().Info("order confirmation email sent", "email", req.Email) + } + return order, nil +} + +type orderPrep struct { + orderItems []types.OrderItem + cartItems []cartservice.CartItem + shippingCostLocalized money.T +} + +func (s *impl) prepareOrderItemsAndShippingQuoteFromCart(ctx context.Context, userID, userCurrency string, address shippingservice.Address) (orderPrep, error) { + var out orderPrep + cartItems, err := s.cartService.Get().GetCart(ctx, userID) + if err != nil { + return out, fmt.Errorf("failed to get user cart during checkout: %w", err) + } + orderItems, err := s.prepOrderItems(ctx, cartItems, userCurrency) + if err != nil { + return out, fmt.Errorf("failed to prepare order: %w", err) + } + shippingUSD, err := s.shippingService.Get().GetQuote(ctx, address, cartItems) + if err != nil { + return out, fmt.Errorf("failed to get shipping quote: %w", err) + } + shippingPrice, err := s.currencyService.Get().Convert(ctx, shippingUSD, userCurrency) + if err != nil { + return out, fmt.Errorf("failed to convert shipping cost to currency: %w", err) + } + + out.shippingCostLocalized = shippingPrice + out.cartItems = cartItems + out.orderItems = orderItems + return out, nil +} + +func (s *impl) prepOrderItems(ctx context.Context, items []cartservice.CartItem, userCurrency string) ([]types.OrderItem, error) { + out := make([]types.OrderItem, len(items)) + for i, item := range items { + product, err := s.catalogService.Get().GetProduct(ctx, item.ProductID) + if err != nil { + return nil, fmt.Errorf("failed to get product #%q: %w", item.ProductID, err) + } + price, err := s.currencyService.Get().Convert(ctx, product.PriceUSD, userCurrency) + if err != nil { + return nil, fmt.Errorf("failed to convert price of %q to %s: %w", item.ProductID, userCurrency, err) + } + out[i] = types.OrderItem{ + Item: item, + Cost: price, + } + } + return out, nil +} diff --git a/checkoutservice/weaver_gen.go b/checkoutservice/weaver_gen.go new file mode 100644 index 0000000..10d8993 --- /dev/null +++ b/checkoutservice/weaver_gen.go @@ -0,0 +1,236 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package checkoutservice + +import ( + "context" + "errors" + "fmt" + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/paymentservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types" + "github.com/ServiceWeaver/weaver/runtime/codegen" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "reflect" +) + +var _ codegen.LatestVersion = codegen.Version[[0][17]struct{}](` + +ERROR: You generated this file with 'weaver generate' v0.18.0 (codegen +version v0.17.0). The generated code is incompatible with the version of the +github.com/ServiceWeaver/weaver module that you're using. The weaver module +version can be found in your go.mod file or by running the following command. + + go list -m github.com/ServiceWeaver/weaver + +We recommend updating the weaver module and the 'weaver generate' command by +running the following. + + go get github.com/ServiceWeaver/weaver@latest + go install github.com/ServiceWeaver/weaver/cmd/weaver@latest + +Then, re-run 'weaver generate' and re-build your code. If the problem persists, +please file an issue at https://github.com/ServiceWeaver/weaver/issues. + +`) + +func init() { + codegen.Register(codegen.Registration{ + Name: "github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T", + Iface: reflect.TypeOf((*T)(nil)).Elem(), + Impl: reflect.TypeOf(impl{}), + LocalStubFn: func(impl any, caller string, tracer trace.Tracer) any { + return t_local_stub{impl: impl.(T), tracer: tracer, placeOrderMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T", Method: "PlaceOrder", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, placeOrderMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T", Method: "PlaceOrder", Remote: true})} + }, + ServerStubFn: func(impl any, addLoad func(uint64, float64)) codegen.Server { + return t_server_stub{impl: impl.(T), addLoad: addLoad} + }, + RefData: "⟦4c9a54a7:wEaVeReDgE:github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T→github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T⟧\n⟦74479326:wEaVeReDgE:github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T→github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T⟧\n⟦7395fba7:wEaVeReDgE:github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T→github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T⟧\n⟦ae088216:wEaVeReDgE:github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T→github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T⟧\n⟦43860cf2:wEaVeReDgE:github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T→github.com/ServiceWeaver/weaver/examples/onlineboutique/emailservice/T⟧\n⟦54f6b59f:wEaVeReDgE:github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T→github.com/ServiceWeaver/weaver/examples/onlineboutique/paymentservice/T⟧\n", + }) +} + +// weaver.InstanceOf checks. +var _ weaver.InstanceOf[T] = (*impl)(nil) + +// weaver.Router checks. +var _ weaver.Unrouted = (*impl)(nil) + +// Local stub implementations. + +type t_local_stub struct { + impl T + tracer trace.Tracer + placeOrderMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) PlaceOrder(ctx context.Context, a0 PlaceOrderRequest) (r0 types.Order, err error) { + // Update metrics. + begin := s.placeOrderMetrics.Begin() + defer func() { s.placeOrderMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "checkoutservice.T.PlaceOrder", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.PlaceOrder(ctx, a0) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + placeOrderMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) PlaceOrder(ctx context.Context, a0 PlaceOrderRequest) (r0 types.Order, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.placeOrderMetrics.Begin() + defer func() { s.placeOrderMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "checkoutservice.T.PlaceOrder", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Encode arguments. + enc := codegen.NewEncoder() + (a0).WeaverMarshal(enc) + var shardKey uint64 + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 0, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + (&r0).WeaverUnmarshal(dec) + err = dec.Error() + return +} + +// Server stub implementations. + +type t_server_stub struct { + impl T + addLoad func(key uint64, load float64) +} + +// Check that t_server_stub implements the codegen.Server interface. +var _ codegen.Server = (*t_server_stub)(nil) + +// GetStubFn implements the codegen.Server interface. +func (s t_server_stub) GetStubFn(method string) func(ctx context.Context, args []byte) ([]byte, error) { + switch method { + case "PlaceOrder": + return s.placeOrder + default: + return nil + } +} + +func (s t_server_stub) placeOrder(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 PlaceOrderRequest + (&a0).WeaverUnmarshal(dec) + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + r0, appErr := s.impl.PlaceOrder(ctx, a0) + + // Encode the results. + enc := codegen.NewEncoder() + (r0).WeaverMarshal(enc) + enc.Error(appErr) + return enc.Data(), nil +} + +// AutoMarshal implementations. + +var _ codegen.AutoMarshal = (*PlaceOrderRequest)(nil) + +type __is_PlaceOrderRequest[T ~struct { + weaver.AutoMarshal + UserID string + UserCurrency string + Address shippingservice.Address + Email string + CreditCard paymentservice.CreditCardInfo +}] struct{} + +var _ __is_PlaceOrderRequest[PlaceOrderRequest] + +func (x *PlaceOrderRequest) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("PlaceOrderRequest.WeaverMarshal: nil receiver")) + } + enc.String(x.UserID) + enc.String(x.UserCurrency) + (x.Address).WeaverMarshal(enc) + enc.String(x.Email) + (x.CreditCard).WeaverMarshal(enc) +} + +func (x *PlaceOrderRequest) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("PlaceOrderRequest.WeaverUnmarshal: nil receiver")) + } + x.UserID = dec.String() + x.UserCurrency = dec.String() + (&x.Address).WeaverUnmarshal(dec) + x.Email = dec.String() + (&x.CreditCard).WeaverUnmarshal(dec) +} diff --git a/colocated.toml b/colocated.toml new file mode 100644 index 0000000..55ea6c1 --- /dev/null +++ b/colocated.toml @@ -0,0 +1,22 @@ +[serviceweaver] +binary = "./onlineboutique" +rollout = "5m" +colocate = [ + [ + "main", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice/T", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/cartCache", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/emailservice/T", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/paymentservice/T", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice/T", + "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T", + ] +] + +[gke] +regions = ["us-west1"] +listeners.boutique = {public_hostname = "onlineboutique.example.com"} diff --git a/currencyservice/data/currency_conversion.json b/currencyservice/data/currency_conversion.json new file mode 100644 index 0000000..bd28709 --- /dev/null +++ b/currencyservice/data/currency_conversion.json @@ -0,0 +1,35 @@ +{ + "EUR": "1.0", + "USD": "1.1305", + "JPY": "126.40", + "BGN": "1.9558", + "CZK": "25.592", + "DKK": "7.4609", + "GBP": "0.85970", + "HUF": "315.51", + "PLN": "4.2996", + "RON": "4.7463", + "SEK": "10.5375", + "CHF": "1.1360", + "ISK": "136.80", + "NOK": "9.8040", + "HRK": "7.4210", + "RUB": "74.4208", + "TRY": "6.1247", + "AUD": "1.6072", + "BRL": "4.2682", + "CAD": "1.5128", + "CNY": "7.5857", + "HKD": "8.8743", + "IDR": "15999.40", + "ILS": "4.0875", + "INR": "79.4320", + "KRW": "1275.05", + "MXN": "21.7999", + "MYR": "4.6289", + "NZD": "1.6679", + "PHP": "59.083", + "SGD": "1.5349", + "THB": "36.012", + "ZAR": "16.0583" +} \ No newline at end of file diff --git a/currencyservice/service.go b/currencyservice/service.go new file mode 100644 index 0000000..c2a100e --- /dev/null +++ b/currencyservice/service.go @@ -0,0 +1,106 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package currencyservice + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "math" + "strconv" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" + "golang.org/x/exp/maps" +) + +var ( + //go:embed data/currency_conversion.json + currencyData []byte +) + +type T interface { + GetSupportedCurrencies(ctx context.Context) ([]string, error) + Convert(ctx context.Context, from money.T, toCode string) (money.T, error) +} + +type impl struct { + weaver.Implements[T] + conversionMap map[string]float64 +} + +func (s *impl) Init(context.Context) error { + m, err := createConversionMap() + s.conversionMap = m + return err +} + +// GetSupportedCurrencies returns the list of supported currencies. +func (s *impl) GetSupportedCurrencies(ctx context.Context) ([]string, error) { + s.Logger().Info("Getting supported currencies...") + return maps.Keys(s.conversionMap), nil +} + +// Convert converts between currencies. +func (s *impl) Convert(ctx context.Context, from money.T, toCode string) (money.T, error) { + unsupportedErr := func(code string) (money.T, error) { + return money.T{}, fmt.Errorf("unsupported currency code %q", from.CurrencyCode) + } + + // Convert: from --> EUR + fromRate, ok := s.conversionMap[from.CurrencyCode] + if !ok { + return unsupportedErr(from.CurrencyCode) + } + euros := carry(float64(from.Units)/fromRate, float64(from.Nanos)/fromRate) + + // Convert: EUR -> toCode + toRate, ok := s.conversionMap[toCode] + if !ok { + return unsupportedErr(toCode) + } + to := carry(float64(euros.Units)*toRate, float64(euros.Nanos)*toRate) + to.CurrencyCode = toCode + return to, nil +} + +// carry is a helper function that handles decimal/fractional carrying. +func carry(units float64, nanos float64) money.T { + const fractionSize = 1000000000 // 1B + nanos += math.Mod(units, 1.0) * fractionSize + units = math.Floor(units) + math.Floor(nanos/fractionSize) + nanos = math.Mod(nanos, fractionSize) + return money.T{ + Units: int64(units), + Nanos: int32(nanos), + } +} + +func createConversionMap() (map[string]float64, error) { + m := map[string]string{} + if err := json.Unmarshal(currencyData, &m); err != nil { + return nil, err + } + conv := make(map[string]float64, len(m)) + for k, v := range m { + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return nil, err + } + conv[k] = f + } + return conv, nil +} diff --git a/currencyservice/weaver_gen.go b/currencyservice/weaver_gen.go new file mode 100644 index 0000000..3d2133f --- /dev/null +++ b/currencyservice/weaver_gen.go @@ -0,0 +1,315 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package currencyservice + +import ( + "context" + "errors" + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" + "github.com/ServiceWeaver/weaver/runtime/codegen" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "reflect" +) + +var _ codegen.LatestVersion = codegen.Version[[0][17]struct{}](` + +ERROR: You generated this file with 'weaver generate' v0.18.0 (codegen +version v0.17.0). The generated code is incompatible with the version of the +github.com/ServiceWeaver/weaver module that you're using. The weaver module +version can be found in your go.mod file or by running the following command. + + go list -m github.com/ServiceWeaver/weaver + +We recommend updating the weaver module and the 'weaver generate' command by +running the following. + + go get github.com/ServiceWeaver/weaver@latest + go install github.com/ServiceWeaver/weaver/cmd/weaver@latest + +Then, re-run 'weaver generate' and re-build your code. If the problem persists, +please file an issue at https://github.com/ServiceWeaver/weaver/issues. + +`) + +func init() { + codegen.Register(codegen.Registration{ + Name: "github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T", + Iface: reflect.TypeOf((*T)(nil)).Elem(), + Impl: reflect.TypeOf(impl{}), + LocalStubFn: func(impl any, caller string, tracer trace.Tracer) any { + return t_local_stub{impl: impl.(T), tracer: tracer, convertMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T", Method: "Convert", Remote: false}), getSupportedCurrenciesMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T", Method: "GetSupportedCurrencies", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, convertMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T", Method: "Convert", Remote: true}), getSupportedCurrenciesMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T", Method: "GetSupportedCurrencies", Remote: true})} + }, + ServerStubFn: func(impl any, addLoad func(uint64, float64)) codegen.Server { + return t_server_stub{impl: impl.(T), addLoad: addLoad} + }, + RefData: "", + }) +} + +// weaver.InstanceOf checks. +var _ weaver.InstanceOf[T] = (*impl)(nil) + +// weaver.Router checks. +var _ weaver.Unrouted = (*impl)(nil) + +// Local stub implementations. + +type t_local_stub struct { + impl T + tracer trace.Tracer + convertMetrics *codegen.MethodMetrics + getSupportedCurrenciesMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) Convert(ctx context.Context, a0 money.T, a1 string) (r0 money.T, err error) { + // Update metrics. + begin := s.convertMetrics.Begin() + defer func() { s.convertMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "currencyservice.T.Convert", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.Convert(ctx, a0, a1) +} + +func (s t_local_stub) GetSupportedCurrencies(ctx context.Context) (r0 []string, err error) { + // Update metrics. + begin := s.getSupportedCurrenciesMetrics.Begin() + defer func() { s.getSupportedCurrenciesMetrics.End(begin, err != nil, 0, 0) }() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.tracer.Start(ctx, "currencyservice.T.GetSupportedCurrencies", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.GetSupportedCurrencies(ctx) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + convertMetrics *codegen.MethodMetrics + getSupportedCurrenciesMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) Convert(ctx context.Context, a0 money.T, a1 string) (r0 money.T, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.convertMetrics.Begin() + defer func() { s.convertMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "currencyservice.T.Convert", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + // Encode arguments. + enc := codegen.NewEncoder() + (a0).WeaverMarshal(enc) + enc.String(a1) + var shardKey uint64 + + // Call the remote method. + requestBytes = len(enc.Data()) + var results []byte + results, err = s.stub.Run(ctx, 0, enc.Data(), shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + (&r0).WeaverUnmarshal(dec) + err = dec.Error() + return +} + +func (s t_client_stub) GetSupportedCurrencies(ctx context.Context) (r0 []string, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.getSupportedCurrenciesMetrics.Begin() + defer func() { s.getSupportedCurrenciesMetrics.End(begin, err != nil, requestBytes, replyBytes) }() + + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + // Create a child span for this method. + ctx, span = s.stub.Tracer().Start(ctx, "currencyservice.T.GetSupportedCurrencies", trace.WithSpanKind(trace.SpanKindClient)) + } + + defer func() { + // Catch and return any panics detected during encoding/decoding/rpc. + if err == nil { + err = codegen.CatchPanics(recover()) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + } + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + + }() + + var shardKey uint64 + + // Call the remote method. + var results []byte + results, err = s.stub.Run(ctx, 1, nil, shardKey) + replyBytes = len(results) + if err != nil { + err = errors.Join(weaver.RemoteCallError, err) + return + } + + // Decode the results. + dec := codegen.NewDecoder(results) + r0 = serviceweaver_dec_slice_string_4af10117(dec) + err = dec.Error() + return +} + +// Server stub implementations. + +type t_server_stub struct { + impl T + addLoad func(key uint64, load float64) +} + +// Check that t_server_stub implements the codegen.Server interface. +var _ codegen.Server = (*t_server_stub)(nil) + +// GetStubFn implements the codegen.Server interface. +func (s t_server_stub) GetStubFn(method string) func(ctx context.Context, args []byte) ([]byte, error) { + switch method { + case "Convert": + return s.convert + case "GetSupportedCurrencies": + return s.getSupportedCurrencies + default: + return nil + } +} + +func (s t_server_stub) convert(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // Decode arguments. + dec := codegen.NewDecoder(args) + var a0 money.T + (&a0).WeaverUnmarshal(dec) + var a1 string + a1 = dec.String() + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + r0, appErr := s.impl.Convert(ctx, a0, a1) + + // Encode the results. + enc := codegen.NewEncoder() + (r0).WeaverMarshal(enc) + enc.Error(appErr) + return enc.Data(), nil +} + +func (s t_server_stub) getSupportedCurrencies(ctx context.Context, args []byte) (res []byte, err error) { + // Catch and return any panics detected during encoding/decoding/rpc. + defer func() { + if err == nil { + err = codegen.CatchPanics(recover()) + } + }() + + // TODO(rgrandl): The deferred function above will recover from panics in the + // user code: fix this. + // Call the local method. + r0, appErr := s.impl.GetSupportedCurrencies(ctx) + + // Encode the results. + enc := codegen.NewEncoder() + serviceweaver_enc_slice_string_4af10117(enc, r0) + enc.Error(appErr) + return enc.Data(), nil +} + +// Encoding/decoding implementations. + +func serviceweaver_enc_slice_string_4af10117(enc *codegen.Encoder, arg []string) { + if arg == nil { + enc.Len(-1) + return + } + enc.Len(len(arg)) + for i := 0; i < len(arg); i++ { + enc.String(arg[i]) + } +} + +func serviceweaver_dec_slice_string_4af10117(dec *codegen.Decoder) []string { + n := dec.Len() + if n == -1 { + return nil + } + res := make([]string, n) + for i := 0; i < n; i++ { + res[i] = dec.String() + } + return res +} diff --git a/emailservice/service.go b/emailservice/service.go new file mode 100644 index 0000000..835f5f0 --- /dev/null +++ b/emailservice/service.go @@ -0,0 +1,63 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package emailservice + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "html/template" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types" +) + +var ( + //go:embed templates/confirmation.html + tmplData string + + tmpl = template.Must(template.New("email"). + Funcs(template.FuncMap{ + "div": func(x, y int32) int32 { return x / y }, + }). + Parse(tmplData)) +) + +type T interface { + SendOrderConfirmation(ctx context.Context, email string, order types.Order) error +} + +type impl struct { + weaver.Implements[T] +} + +// SendOrderConfirmation sends the confirmation email for the order to the +// given email address. +func (s *impl) SendOrderConfirmation(ctx context.Context, email string, order types.Order) error { + var buf bytes.Buffer + if err := tmpl.Execute(&buf, order); err != nil { + return err + } + confirmation := buf.String() + return s.sendEmail(email, confirmation) +} + +func (s *impl) sendEmail(email, confirmation string) error { + s.Logger().Info(fmt.Sprintf( + "A request to send email confirmation to %s has been received:\n%s", + email, confirmation)) + return nil +} diff --git a/emailservice/templates/confirmation.html b/emailservice/templates/confirmation.html new file mode 100644 index 0000000..6b46ef0 --- /dev/null +++ b/emailservice/templates/confirmation.html @@ -0,0 +1,52 @@ + + + +
+Thanks for shopping with us!
+
#{{ .OrderID }}
+#{{ .ShippingTrackingID }}
+{{ .ShippingCost.Units }}. {{ printf "%02d" (div .ShippingCost.Nanos 10000000) }} {{ .ShippingCost.CurrencyCode }}
+{{ .ShippingAddress.StreetAddress }}, {{.ShippingAddress.City}}, {{.ShippingAddress.State}} {{.ShippingAddress.ZipCode}} {{.ShippingAddress.Country}}
+Item No. | +Quantity | +Price | +
---|---|---|
#{{ $item.Item.ProductID }} | +{{ $item.Item.Quantity }} | +{{ $item.Cost.Units }}.{{ printf "%02d" (div $item.Cost.Nanos 10000000) }} {{ $item.Cost.CurrencyCode }} | +
containing the session-id. */ +footer .footer-top p:nth-child(3) { + margin-top: 56px; +} + +footer .footer-top .footer-social, +footer .footer-top .footer-app, +footer .footer-links, +footer .footer-top .social, +footer .footer-top .app { + display: block; + align-items: center; +} + +footer .footer-top .footer-social { + padding: 31px; +} + +footer .footer-top .footer-social h4 { + margin-bottom: 0; +} + +footer .footer-top .footer-social div { + width: 50%; +} + +/* Home */ + +main { + flex: 1 0 auto; + background-color: #F9F9F9; +} + +@media (min-width: 992px) { + .home .container-fluid { + height: calc(100vh - 91px); /* 91px is the height of the top/header bars. */ + } + .home .container-fluid > .row > .col-4 { + height: calc(100vh - 91px); + } + .home .container-fluid > .row > .col-lg-8 { + height: calc(100vh - 91px); + overflow-y: scroll; + } +} + +.home-mobile-hero-banner { + height: 200px; + background: url(/static/images/folded-clothes-on-white-chair-wide.jpg) no-repeat top center; + background-size: cover; +} + +.home-desktop-left-image { + background: url(/static/images/folded-clothes-on-white-chair.jpg) no-repeat center; + background-size: cover; +} + +.hot-products-row h3 { + margin-bottom: 32px; + margin-top: 56px; + font-size: 36px; + font-weight: normal; +} + +.hot-products-row { + padding-bottom: 70px; + padding-left: 10%; + padding-right: 10%; +} + +.hot-product-card { + margin-bottom: 52px; + padding-left: 16px; + padding-right: 16px; +} + +.hot-product-card img { + width: 100%; + height: auto; + border-radius: 20% 0 20% 20%; +} + +.hot-product-card-name { + margin-top: 8px; + font-size: 18px; +} + +.hot-product-card-price { + font-size: 14px; +} + +.hot-product-card > a:first-child { + position: relative; + display: block; +} + +.hot-product-card-img-overlay { + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + border-radius: 20% 0 20% 20%; + background-color: transparent; +} + +.hot-product-card:hover .hot-product-card-img-overlay { + background-color: rgba(71, 0, 29, 0.2); +} + +/* +This chunk ensures the left/right padding of the footer is +similar to that of the hot-products-row. +*/ +.home-desktop-footer-row { + padding-left: 9%; + padding-right: 9%; + background-color: #570D2E; +} + +/* Ad */ + +.ad { + position: relative; + background-color: #FF9A9B; + font-size: 24px; + text-align: center; +} + +/* "Ad" text. */ +.ad strong { + position: absolute; + top: 6px; + left: 12px; + font-size: 14px; + font-weight: normal; +} + +.ad a { + color: black; +} + +/* Product */ + +.h-product { + margin-top: 56px; + margin-bottom: 112px; + max-width: 1200px; + background-color: #F9F9F9; +} + +.h-product > .row { + align-items: flex-end; +} + +.h-product .product-image { + width: 100%; + border-radius: 20% 20% 0 20%; +} + +.h-product .product-price { + font-size: 28px; +} + +.h-product .product-info .product-wrapper { + margin-left: 15px; +} + +.h-product .product-info h2 { + margin-bottom: 16px; + margin-top: 16px; + font-size: 56px; + line-height: 1.14; + font-weight: normal; + color: #111111; +} + +.h-product .input-group-text, +.h-product .btn.btn-info { + font-size: 18px; + line-height: 1.89; + letter-spacing: 3.6px; + text-align: center; + color: #111111; + border-radius: 0; +} + +.product-quantity-dropdown { + position: relative; + width: 100px; +} + +.product-quantity-dropdown select { + width: 100%; + height: 45px; + border: 1px solid #acacac; + padding: 10px 16px; + border-radius: 8px; +} + +.product-quantity-dropdown img { + position: absolute; + right: 25px; + top: 20px; + width: 10px; + height: 5px; +} + +.h-product .cymbal-button-primary { + margin-top: 16px; +} + +/* Platform Banner */ + +.local, +.aws-platform, +.onprem-platform, +.azure-platform, +.alibaba-platform, +.gcp-platform { + position: fixed; + top: 0; + left: 0; + width: 10px; + height: 100vh; + color: white; + font-size: 24px; + z-index: 999; +} + +.aws-platform, +.aws-platform .platform-flag { + background-color: #ff9900; +} + +.onprem-platform, +.onprem-platform .platform-flag { + background-color: #34A853; +} + +.gcp-platform, +.gcp-platform .platform-flag { + background-color: #4285f4; +} + + +.azure-platform, +.azure-platform .platform-flag { + background-color: #f35426; +} + +.alibaba-platform, +.alibaba-platform .platform-flag { + background-color: #ffC300; +} + +.local, +.local .platform-flag { + background-color: #2c0678; +} + +.platform-flag { + position: absolute; + top: 98px; + left: 0; + width: 190px; + height: 50px; + display: flex; + justify-content: center; + align-items: center; +} + +/* Recommendation */ + +.recommendations { + background: #F9F9F9; + padding-bottom: 55px; +} + +.recommendations .container { + max-width: 1174px; +} + +@media (max-width: 992px) { + .recommendations .container { + max-width: none; + } +} + +.recommendations h2 { + border-top: solid 1px; + padding: 40px 0; + font-weight: normal; + text-align: center; +} + +.recommendations h5 { + margin-top: 8px; + font-weight: normal; + font-size: 18px; +} + +.recommendations img { + height: 100%; + width: 100%; + border-radius: 20% 0 20% 20%; +} + +select { + -webkit-appearance: none; + -webkit-border-radius: 0px; +} + +/* Cymbal */ + +/* +If we ever decide to create a separate Cymbal CSS library for Cymbal components, +the rules below could be extracted. +*/ + +.cymbal-button-primary, .cymbal-button-secondary { + display: inline-block; + border: solid 1px #CE0631; + padding: 8px 16px; + outline: none; + font-size: 14px; + border-radius: 22px; + cursor: pointer; +} + +.cymbal-button-primary:focus, .cymbal-button-secondary:focus { + outline: none; /* To override browser (Chrome) default blue outline. */ +} + +.cymbal-button-primary { + background-color: #CE0631; + color: white; +} + +.cymbal-button-secondary { + background: none; + color: #CE0631; +} + +.cymbal-form-field { + position: relative; + margin-top: 24px; +} + +.cymbal-form-field label { + width: 100%; + margin: 0; + padding: 8px 16px 0 16px; + font-size: 12px; + line-height: 1.8em; /* Without this, there might be a 1px gap underneath. */ + font-weight: normal; + border-radius: 4px 4px 0px 0px; + color: #5C6063; + background-color: white; +} + +.cymbal-form-field input[type='email'], +.cymbal-form-field input[type='password'], +.cymbal-form-field select, +.cymbal-form-field input[type='text'] { + width: 100%; + border: none; + border-bottom: 1px solid #9AA0A6; + padding: 0 16px 8px 16px; + outline: none; + color: #1E2021; +} + +.cymbal-form-field .cymbal-dropdown-chevron { + position: absolute; + right: 25px; + width: 10px; + height: 5px; +} diff --git a/frontend/templates/ad.html b/frontend/templates/ad.html new file mode 100755 index 0000000..403308f --- /dev/null +++ b/frontend/templates/ad.html @@ -0,0 +1,26 @@ + + +{{ define "text_ad" }} +
Items you add to your shopping cart will appear here.
+ Continue Shopping +Something has failed. Below are some details for debugging.
+ +HTTP Status: {{.status_code}} {{.status}}
++ {{- .error -}} ++