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 @@ + + + + + Your Order Confirmation + + + + +

Your Order Confirmation

+

Thanks for shopping with us!

+

Order ID

+

#{{ .OrderID }}

+

Shipping

+

#{{ .ShippingTrackingID }}

+

{{ .ShippingCost.Units }}. {{ printf "%02d" (div .ShippingCost.Nanos 10000000) }} {{ .ShippingCost.CurrencyCode }}

+

{{ .ShippingAddress.StreetAddress }}, {{.ShippingAddress.City}}, {{.ShippingAddress.State}} {{.ShippingAddress.ZipCode}} {{.ShippingAddress.Country}}

+

Items

+ + + + + + + {{range $index, $item := .Items}} + + + + + + {{ end }} +
Item No.QuantityPrice
#{{ $item.Item.ProductID }}{{ $item.Item.Quantity }}{{ $item.Cost.Units }}.{{ printf "%02d" (div $item.Cost.Nanos 10000000) }} {{ $item.Cost.CurrencyCode }}
+ + diff --git a/emailservice/weaver_gen.go b/emailservice/weaver_gen.go new file mode 100644 index 0000000..0fa1f98 --- /dev/null +++ b/emailservice/weaver_gen.go @@ -0,0 +1,197 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package emailservice + +import ( + "context" + "errors" + "github.com/ServiceWeaver/weaver" + "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/emailservice/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, sendOrderConfirmationMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/emailservice/T", Method: "SendOrderConfirmation", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, sendOrderConfirmationMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/emailservice/T", Method: "SendOrderConfirmation", 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 + sendOrderConfirmationMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) SendOrderConfirmation(ctx context.Context, a0 string, a1 types.Order) (err error) { + // Update metrics. + begin := s.sendOrderConfirmationMetrics.Begin() + defer func() { s.sendOrderConfirmationMetrics.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, "emailservice.T.SendOrderConfirmation", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.SendOrderConfirmation(ctx, a0, a1) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + sendOrderConfirmationMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) SendOrderConfirmation(ctx context.Context, a0 string, a1 types.Order) (err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.sendOrderConfirmationMetrics.Begin() + defer func() { s.sendOrderConfirmationMetrics.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, "emailservice.T.SendOrderConfirmation", 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) + (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 +} + +// 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 "SendOrderConfirmation": + return s.sendOrderConfirmation + default: + return nil + } +} + +func (s t_server_stub) sendOrderConfirmation(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 types.Order + (&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.SendOrderConfirmation(ctx, a0, a1) + + // Encode the results. + enc := codegen.NewEncoder() + enc.Error(appErr) + return enc.Data(), nil +} diff --git a/frontend/frontend.go b/frontend/frontend.go new file mode 100644 index 0000000..4e05829 --- /dev/null +++ b/frontend/frontend.go @@ -0,0 +1,163 @@ +// 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 frontend + +import ( + "context" + "embed" + "fmt" + "io/fs" + "net" + "net/http" + "os" + "strings" + "sync" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice" +) + +const ( + cookieMaxAge = 60 * 60 * 48 + + cookiePrefix = "shop_" + cookieSessionID = cookiePrefix + "session-id" + cookieCurrency = cookiePrefix + "currency" +) + +var ( + //go:embed static/* + staticFS embed.FS + + validEnvs = []string{"local", "gcp"} + + addrMu sync.Mutex + localAddr string +) + +type platformDetails struct { + css string + provider string +} + +func (plat *platformDetails) setPlatformDetails(env string) { + if env == "gcp" { + plat.provider = "Google Cloud" + plat.css = "gcp-platform" + } else { + plat.provider = "local" + plat.css = "local" + } +} + +// Server is the application frontend. +type Server struct { + weaver.Implements[weaver.Main] + + handler http.Handler + platform platformDetails + hostname string + + catalogService weaver.Ref[productcatalogservice.T] + currencyService weaver.Ref[currencyservice.T] + cartService weaver.Ref[cartservice.T] + recommendationService weaver.Ref[recommendationservice.T] + checkoutService weaver.Ref[checkoutservice.T] + shippingService weaver.Ref[shippingservice.T] + adService weaver.Ref[adservice.T] + + boutique weaver.Listener +} + +func Serve(ctx context.Context, s *Server) error { + // Find out where we're running. + // Set ENV_PLATFORM (default to local if not set; use env var if set; + // otherwise detect GCP, which overrides env). + var env = os.Getenv("ENV_PLATFORM") + // Only override from env variable if set + valid env + if env == "" || !stringinSlice(validEnvs, env) { + fmt.Println("env platform is either empty or invalid") + env = "local" + } + // Autodetect GCP + addrs, err := net.LookupHost("metadata.google.internal.") + if err == nil && len(addrs) >= 0 { + s.Logger().Debug("Detected Google metadata server, setting ENV_PLATFORM to GCP.", "address", addrs) + env = "gcp" + } + s.Logger().Debug("ENV_PLATFORM", "platform", env) + s.platform = platformDetails{} + s.platform.setPlatformDetails(strings.ToLower(env)) + s.hostname, err = os.Hostname() + if err != nil { + s.Logger().Debug(`cannot get hostname for frontend: using "unknown"`) + s.hostname = "unknown" + } + + // Setup the handler. + staticHTML, err := fs.Sub(fs.FS(staticFS), "static") + if err != nil { + return err + } + r := http.NewServeMux() + + // Helper that adds a handler with HTTP metric instrumentation. + instrument := func(label string, fn func(http.ResponseWriter, *http.Request), methods []string) http.Handler { + allowed := map[string]struct{}{} + for _, method := range methods { + allowed[method] = struct{}{} + } + handler := func(w http.ResponseWriter, r *http.Request) { + if _, ok := allowed[r.Method]; len(allowed) > 0 && !ok { + msg := fmt.Sprintf("method %q not allowed", r.Method) + http.Error(w, msg, http.StatusMethodNotAllowed) + return + } + fn(w, r) + } + return weaver.InstrumentHandlerFunc(label, handler) + } + + const get = http.MethodGet + const post = http.MethodPost + const head = http.MethodHead + r.Handle("/", instrument("home", s.homeHandler, []string{get, head})) + r.Handle("/product/", instrument("product", s.productHandler, []string{get, head})) + r.Handle("/cart", instrument("cart", s.cartHandler, []string{get, head, post})) + r.Handle("/cart/empty", instrument("cart_empty", s.emptyCartHandler, []string{post})) + r.Handle("/setCurrency", instrument("setcurrency", s.setCurrencyHandler, []string{post})) + r.Handle("/logout", instrument("logout", s.logoutHandler, []string{get})) + r.Handle("/cart/checkout", instrument("cart_checkout", s.placeOrderHandler, []string{post})) + r.Handle("/static/", weaver.InstrumentHandler("static", http.StripPrefix("/static/", http.FileServer(http.FS(staticHTML))))) + r.Handle("/robots.txt", instrument("robots", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "User-agent: *\nDisallow: /") }, nil)) + r.HandleFunc(weaver.HealthzURL, weaver.HealthzHandler) + + // Set handler. + var handler http.Handler = r + // TODO(spetrovic): Use the Service Weaver per-component config to provisionaly + // add these stats. + handler = ensureSessionID(handler) // add session ID + handler = newLogHandler(s.Logger(), handler) // add logging + s.handler = handler + + s.Logger().Debug("Frontend available", "addr", s.boutique) + return http.Serve(s.boutique, s.handler) +} diff --git a/frontend/handlers.go b/frontend/handlers.go new file mode 100644 index 0000000..b59efc7 --- /dev/null +++ b/frontend/handlers.go @@ -0,0 +1,555 @@ +// 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 frontend + +import ( + "context" + "embed" + "errors" + "fmt" + "html/template" + "math/rand" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice" + "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/money" + "golang.org/x/exp/slog" +) + +const ( + avoidNoopCurrencyConversionRPC = false +) + +var ( + isCymbalBrand = strings.ToLower(os.Getenv("CYMBAL_BRANDING")) == "true" + + //go:embed templates/* + templateFS embed.FS + templates = template.Must(template.New(""). + Funcs(template.FuncMap{ + "renderMoney": renderMoney, + "renderCurrencyLogo": renderCurrencyLogo, + }).ParseFS(templateFS, "templates/*.html")) + + allowlistedCurrencies = map[string]bool{ + "USD": true, + "EUR": true, + "CAD": true, + "JPY": true, + "GBP": true, + "TRY": true, + } + + defaultCurrency = "USD" +) + +func (fe *Server) homeHandler(w http.ResponseWriter, r *http.Request) { + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + logger.Info("home", "currency", currentCurrency(r)) + currencies, err := fe.getCurrencies(r.Context()) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve currencies: %w", err), http.StatusInternalServerError) + return + } + products, err := fe.catalogService.Get().ListProducts(r.Context()) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve products: %w", err), http.StatusInternalServerError) + return + } + cart, err := fe.cartService.Get().GetCart(r.Context(), sessionID(r)) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve cart: %w", err), http.StatusInternalServerError) + return + } + + type productView struct { + Item productcatalogservice.Product + Price money.T + } + ps := make([]productView, len(products)) + for i, p := range products { + price, err := fe.currencyService.Get().Convert(r.Context(), p.PriceUSD, currentCurrency(r)) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("failed to do currency conversion for product %s: %w", p.ID, err), http.StatusInternalServerError) + return + } + ps[i] = productView{p, price} + } + + if err := templates.ExecuteTemplate(w, "home", map[string]interface{}{ + "session_id": sessionID(r), + "request_id": r.Context().Value(ctxKeyRequestID{}), + "hostname": fe.hostname, + "user_currency": currentCurrency(r), + "show_currency": true, + "currencies": currencies, + "products": ps, + "cart_size": cartSize(cart), + "banner_color": os.Getenv("BANNER_COLOR"), + "ad": fe.chooseAd(r.Context(), []string{}, logger), + "platform_css": fe.platform.css, + "platform_name": fe.platform.provider, + "is_cymbal_brand": isCymbalBrand, + }); err != nil { + logger.Error("generate home page", "err", err) + } +} + +func (fe *Server) productHandler(w http.ResponseWriter, r *http.Request) { + _, id := filepath.Split(r.URL.Path) + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + if id == "" { + fe.renderHTTPError(r, w, errors.New("product id not specified"), http.StatusBadRequest) + return + } + logger.Debug("serving product page", "id", id, "currency", currentCurrency(r)) + + p, err := fe.catalogService.Get().GetProduct(r.Context(), id) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve product: %w", err), http.StatusInternalServerError) + return + } + currencies, err := fe.getCurrencies(r.Context()) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve currencies: %w", err), http.StatusInternalServerError) + return + } + + cart, err := fe.cartService.Get().GetCart(r.Context(), sessionID(r)) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve cart: %w", err), http.StatusInternalServerError) + return + } + + price, err := fe.convertCurrency(r.Context(), p.PriceUSD, currentCurrency(r)) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("failed to convert currency: %w", err), http.StatusInternalServerError) + return + } + + recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), []string{id}) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("failed to get product recommendations: %w", err), http.StatusInternalServerError) + return + } + + product := struct { + Item productcatalogservice.Product + Price money.T + }{p, price} + + if err := templates.ExecuteTemplate(w, "product", map[string]interface{}{ + "session_id": sessionID(r), + "request_id": r.Context().Value(ctxKeyRequestID{}), + "hostname": fe.hostname, + "ad": fe.chooseAd(r.Context(), p.Categories, logger), + "user_currency": currentCurrency(r), + "show_currency": true, + "currencies": currencies, + "product": product, + "recommendations": recommendations, + "cart_size": cartSize(cart), + "platform_css": fe.platform.css, + "platform_name": fe.platform.provider, + "is_cymbal_brand": isCymbalBrand, + }); err != nil { + logger.Error("generate product page", "err", err) + } +} + +func (fe *Server) cartHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet || r.Method == http.MethodHead { + fe.viewCartHandler(w, r) + return + } + if r.Method == http.MethodPost { + fe.addToCartHandler(w, r) + return + } + msg := fmt.Sprintf("method %q not allowed", r.Method) + http.Error(w, msg, http.StatusMethodNotAllowed) +} + +func (fe *Server) addToCartHandler(w http.ResponseWriter, r *http.Request) { + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32) + productID := r.FormValue("product_id") + if productID == "" || quantity == 0 { + fe.renderHTTPError(r, w, errors.New("invalid form input"), http.StatusBadRequest) + return + } + logger.Debug("adding to cart", "product", productID, "quantity", quantity) + + p, err := fe.catalogService.Get().GetProduct(r.Context(), productID) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve product: %w", err), http.StatusInternalServerError) + return + } + + if err := fe.cartService.Get().AddItem(r.Context(), sessionID(r), cartservice.CartItem{ + ProductID: p.ID, + Quantity: int32(quantity), + }); err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("failed to add to cart: %w", err), http.StatusInternalServerError) + return + } + w.Header().Set("location", "/cart") + w.WriteHeader(http.StatusFound) +} + +func (fe *Server) emptyCartHandler(w http.ResponseWriter, r *http.Request) { + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + logger.Debug("emptying cart") + + if err := fe.cartService.Get().EmptyCart(r.Context(), sessionID(r)); err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("failed to empty cart: %w", err), http.StatusInternalServerError) + return + } + w.Header().Set("location", "/") + w.WriteHeader(http.StatusFound) +} + +func (fe *Server) viewCartHandler(w http.ResponseWriter, r *http.Request) { + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + logger.Debug("view user cart") + currencies, err := fe.getCurrencies(r.Context()) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve currencies: %w", err), http.StatusInternalServerError) + return + } + cart, err := fe.cartService.Get().GetCart(r.Context(), sessionID(r)) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve cart: %w", err), http.StatusInternalServerError) + return + } + + recommendations, err := fe.getRecommendations(r.Context(), sessionID(r), cartIDs(cart)) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("failed to get product recommendations: %w", err), http.StatusInternalServerError) + return + } + + shippingCost, err := fe.getShippingQuote(r.Context(), cart, currentCurrency(r)) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("failed to get shipping quote: %w", err), http.StatusInternalServerError) + return + } + + type cartItemView struct { + Item productcatalogservice.Product + Quantity int32 + Price *money.T + } + items := make([]cartItemView, len(cart)) + totalPrice := money.T{CurrencyCode: currentCurrency(r)} + for i, item := range cart { + p, err := fe.catalogService.Get().GetProduct(r.Context(), item.ProductID) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve product #%s: %w", item.ProductID, err), http.StatusInternalServerError) + return + } + price, err := fe.convertCurrency(r.Context(), p.PriceUSD, currentCurrency(r)) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not convert currency for product #%s: %w", item.ProductID, err), http.StatusInternalServerError) + return + } + + multPrice := money.MultiplySlow(price, uint32(item.Quantity)) + items[i] = cartItemView{ + Item: p, + Quantity: item.Quantity, + Price: &multPrice} + totalPrice = money.Must(money.Sum(totalPrice, multPrice)) + } + totalPrice = money.Must(money.Sum(totalPrice, shippingCost)) + year := time.Now().Year() + + if err := templates.ExecuteTemplate(w, "cart", map[string]interface{}{ + "session_id": sessionID(r), + "request_id": r.Context().Value(ctxKeyRequestID{}), + "hostname": fe.hostname, + "user_currency": currentCurrency(r), + "currencies": currencies, + "recommendations": recommendations, + "cart_size": cartSize(cart), + "shipping_cost": shippingCost, + "show_currency": true, + "total_cost": totalPrice, + "items": items, + "expiration_years": []int{year, year + 1, year + 2, year + 3, year + 4}, + "platform_css": fe.platform.css, + "platform_name": fe.platform.provider, + "is_cymbal_brand": isCymbalBrand, + }); err != nil { + logger.Error("generate cart page", "err", err) + } +} + +func (fe *Server) placeOrderHandler(w http.ResponseWriter, r *http.Request) { + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + logger.Debug("placing order") + + var ( + email = r.FormValue("email") + streetAddress = r.FormValue("street_address") + zipCode, _ = strconv.ParseInt(r.FormValue("zip_code"), 10, 32) + city = r.FormValue("city") + state = r.FormValue("state") + country = r.FormValue("country") + ccNumber = r.FormValue("credit_card_number") + ccMonth, _ = strconv.ParseInt(r.FormValue("credit_card_expiration_month"), 10, 32) + ccYear, _ = strconv.ParseInt(r.FormValue("credit_card_expiration_year"), 10, 32) + ccCVV, _ = strconv.ParseInt(r.FormValue("credit_card_cvv"), 10, 32) + ) + + order, err := fe.checkoutService.Get().PlaceOrder(r.Context(), checkoutservice.PlaceOrderRequest{ + Email: email, + CreditCard: paymentservice.CreditCardInfo{ + Number: ccNumber, + ExpirationMonth: time.Month(ccMonth), + ExpirationYear: int(ccYear), + CVV: int32(ccCVV)}, + UserID: sessionID(r), + UserCurrency: currentCurrency(r), + Address: shippingservice.Address{ + StreetAddress: streetAddress, + City: city, + State: state, + ZipCode: int32(zipCode), + Country: country}, + }) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("failed to complete the order: %w", err), http.StatusInternalServerError) + return + } + logger.Info("order placed", "id", order.OrderID) + + totalPaid := order.ShippingCost + for _, item := range order.Items { + multPrice := money.MultiplySlow(item.Cost, uint32(item.Item.Quantity)) + totalPaid = money.Must(money.Sum(totalPaid, multPrice)) + } + + currencies, err := fe.getCurrencies(r.Context()) + if err != nil { + fe.renderHTTPError(r, w, fmt.Errorf("could not retrieve currencies: %w", err), http.StatusInternalServerError) + return + } + + recommendations, _ := fe.getRecommendations(r.Context(), sessionID(r), nil /*productIDs*/) + + if err := templates.ExecuteTemplate(w, "order", map[string]interface{}{ + "session_id": sessionID(r), + "request_id": r.Context().Value(ctxKeyRequestID{}), + "hostname": fe.hostname, + "user_currency": currentCurrency(r), + "show_currency": false, + "currencies": currencies, + "order": order, + "total_paid": &totalPaid, + "recommendations": recommendations, + "platform_css": fe.platform.css, + "platform_name": fe.platform.provider, + "is_cymbal_brand": isCymbalBrand, + }); err != nil { + logger.Error("generate order page", "err", err) + } +} + +func (fe *Server) logoutHandler(w http.ResponseWriter, r *http.Request) { + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + logger.Debug("logging out") + for _, c := range r.Cookies() { + c.Expires = time.Now().Add(-time.Hour * 24 * 365) + c.MaxAge = -1 + http.SetCookie(w, c) + } + w.Header().Set("Location", "/") + w.WriteHeader(http.StatusFound) +} + +func (fe *Server) setCurrencyHandler(w http.ResponseWriter, r *http.Request) { + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + cur := r.FormValue("currency_code") + logger.Debug("setting currency", "curr.new", cur, "curr.old", currentCurrency(r)) + + if cur != "" { + http.SetCookie(w, &http.Cookie{ + Name: cookieCurrency, + Value: cur, + MaxAge: cookieMaxAge, + }) + } + referer := r.Header.Get("referer") + if referer == "" { + referer = "/" + } + w.Header().Set("Location", referer) + w.WriteHeader(http.StatusFound) +} + +// chooseAd queries for advertisements available and randomly chooses one, if +// available. It ignores the error retrieving the ad since it is not critical. +func (fe *Server) chooseAd(ctx context.Context, ctxKeys []string, logger *slog.Logger) *adservice.Ad { + ctx, cancel := context.WithTimeout(ctx, time.Millisecond*100) + defer cancel() + ads, err := fe.adService.Get().GetAds(ctx, ctxKeys) + if err != nil { + logger.Error("failed to retrieve ads", "err", err) + return nil + } + return &ads[rand.Intn(len(ads))] +} + +func (fe *Server) getCurrencies(ctx context.Context) ([]string, error) { + codes, err := fe.currencyService.Get().GetSupportedCurrencies(ctx) + if err != nil { + return nil, err + } + var out []string + for _, c := range codes { + if _, ok := allowlistedCurrencies[c]; ok { + out = append(out, c) + } + } + return out, nil +} + +func (fe *Server) convertCurrency(ctx context.Context, money money.T, currency string) (money.T, error) { + if avoidNoopCurrencyConversionRPC && money.CurrencyCode == currency { + return money, nil + } + return fe.currencyService.Get().Convert(ctx, money, currency) +} + +func (fe *Server) getShippingQuote(ctx context.Context, items []cartservice.CartItem, currency string) (money.T, error) { + quote, err := fe.shippingService.Get().GetQuote(ctx, shippingservice.Address{}, items) + if err != nil { + return money.T{}, err + } + return fe.convertCurrency(ctx, quote, currency) +} + +func (fe *Server) getRecommendations(ctx context.Context, userID string, productIDs []string) ([]productcatalogservice.Product, error) { + recommendationIDs, err := fe.recommendationService.Get().ListRecommendations(ctx, userID, productIDs) + if err != nil { + return nil, err + } + out := make([]productcatalogservice.Product, len(recommendationIDs)) + for i, id := range recommendationIDs { + p, err := fe.catalogService.Get().GetProduct(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get recommended product info (#%s): %w", id, err) + } + out[i] = p + } + if len(out) > 4 { + out = out[:4] // take only first four to fit the UI + } + return out, err +} + +func (fe *Server) renderHTTPError(r *http.Request, w http.ResponseWriter, err error, code int) { + logger := r.Context().Value(ctxKeyLogger{}).(*slog.Logger) + logger.Error("request error", "err", err) + errMsg := fmt.Sprintf("%+v", err) + + w.WriteHeader(code) + + if templateErr := templates.ExecuteTemplate(w, "error", map[string]interface{}{ + "session_id": sessionID(r), + "request_id": r.Context().Value(ctxKeyRequestID{}), + "hostname": fe.hostname, + "error": errMsg, + "status_code": code, + "status": http.StatusText(code), + }); templateErr != nil { + logger.Error("generate error page", "err", templateErr) + } +} + +func currentCurrency(r *http.Request) string { + c, _ := r.Cookie(cookieCurrency) + if c != nil { + return c.Value + } + return defaultCurrency +} + +func sessionID(r *http.Request) string { + v := r.Context().Value(ctxKeySessionID{}) + if v != nil { + return v.(string) + } + return "" +} + +func cartIDs(c []cartservice.CartItem) []string { + out := make([]string, len(c)) + for i, v := range c { + out[i] = v.ProductID + } + return out +} + +// get total # of items in cart +func cartSize(c []cartservice.CartItem) int { + cartSize := 0 + for _, item := range c { + cartSize += int(item.Quantity) + } + return cartSize +} + +func renderMoney(m money.T) string { + currencyLogo := renderCurrencyLogo(m.CurrencyCode) + return fmt.Sprintf("%s%d.%02d", currencyLogo, m.Units, m.Nanos/10000000) +} + +func renderCurrencyLogo(currencyCode string) string { + logos := map[string]string{ + "USD": "$", + "CAD": "$", + "JPY": "¥", + "EUR": "€", + "TRY": "₺", + "GBP": "£", + } + + logo := "$" //default + if val, ok := logos[currencyCode]; ok { + logo = val + } + return logo +} + +func stringinSlice(slice []string, val string) bool { + for _, item := range slice { + if item == val { + return true + } + } + return false +} diff --git a/frontend/middleware.go b/frontend/middleware.go new file mode 100644 index 0000000..7fdc1b1 --- /dev/null +++ b/frontend/middleware.go @@ -0,0 +1,111 @@ +// 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 frontend + +import ( + "context" + "net/http" + "time" + + "github.com/google/uuid" + "golang.org/x/exp/slog" +) + +type ctxKeyLogger struct{} +type ctxKeyRequestID struct{} +type ctxKeySessionID struct{} + +type responseRecorder struct { + b int + status int + w http.ResponseWriter +} + +func (r *responseRecorder) Header() http.Header { return r.w.Header() } + +func (r *responseRecorder) Write(p []byte) (int, error) { + if r.status == 0 { + r.status = http.StatusOK + } + n, err := r.w.Write(p) + r.b += n + return n, err +} + +func (r *responseRecorder) WriteHeader(statusCode int) { + r.status = statusCode + r.w.WriteHeader(statusCode) +} + +type logHandler struct { + logger *slog.Logger + next http.Handler +} + +func newLogHandler(logger *slog.Logger, next http.Handler) http.Handler { + return &logHandler{logger: logger, next: next} +} + +func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + requestID, _ := uuid.NewRandom() + ctx = context.WithValue(ctx, ctxKeyRequestID{}, requestID.String()) + + start := time.Now() + rr := &responseRecorder{w: w} + + logger := lh.logger.With( + "http.req.path", r.URL.Path, + "http.req.method", r.Method, + "http.req.id", requestID) + if v, ok := r.Context().Value(ctxKeySessionID{}).(string); ok { + logger = logger.With("session", v) + } + logger.Debug("request started") + defer func() { + logger.Debug("request complete", + "duration", time.Since(start), + "status", rr.status, + "bytes", rr.b, + ) + }() + + ctx = context.WithValue(ctx, ctxKeyLogger{}, logger) + r = r.WithContext(ctx) + lh.next.ServeHTTP(rr, r) +} + +func ensureSessionID(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var sessionID string + c, err := r.Cookie(cookieSessionID) + if err == http.ErrNoCookie { + u, _ := uuid.NewRandom() + sessionID = u.String() + http.SetCookie(w, &http.Cookie{ + Name: cookieSessionID, + Value: sessionID, + MaxAge: cookieMaxAge, + }) + } else if err != nil { + return + } else { + sessionID = c.Value + } + ctx := context.WithValue(r.Context(), ctxKeySessionID{}, sessionID) + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + } +} diff --git a/frontend/static/favicon-cymbal.ico b/frontend/static/favicon-cymbal.ico new file mode 100644 index 0000000..6f8ea03 Binary files /dev/null and b/frontend/static/favicon-cymbal.ico differ diff --git a/frontend/static/favicon.ico b/frontend/static/favicon.ico new file mode 100755 index 0000000..30acb27 Binary files /dev/null and b/frontend/static/favicon.ico differ diff --git a/frontend/static/icons/Cymbal_NavLogo.svg b/frontend/static/icons/Cymbal_NavLogo.svg new file mode 100644 index 0000000..7edd507 --- /dev/null +++ b/frontend/static/icons/Cymbal_NavLogo.svg @@ -0,0 +1,170 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/static/icons/Hipster_Advert2.svg b/frontend/static/icons/Hipster_Advert2.svg new file mode 100755 index 0000000..94f5663 --- /dev/null +++ b/frontend/static/icons/Hipster_Advert2.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_CartIcon.svg b/frontend/static/icons/Hipster_CartIcon.svg new file mode 100755 index 0000000..9c9cb1f --- /dev/null +++ b/frontend/static/icons/Hipster_CartIcon.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + Hipster + + + + + + + + Hipster + + + + diff --git a/frontend/static/icons/Hipster_CheckOutIcon.svg b/frontend/static/icons/Hipster_CheckOutIcon.svg new file mode 100755 index 0000000..cd0faa1 --- /dev/null +++ b/frontend/static/icons/Hipster_CheckOutIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_CurrencyIcon.svg b/frontend/static/icons/Hipster_CurrencyIcon.svg new file mode 100755 index 0000000..0519717 --- /dev/null +++ b/frontend/static/icons/Hipster_CurrencyIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_DownArrow.svg b/frontend/static/icons/Hipster_DownArrow.svg new file mode 100644 index 0000000..3973008 --- /dev/null +++ b/frontend/static/icons/Hipster_DownArrow.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + Hipster + + + + diff --git a/frontend/static/icons/Hipster_FacebookIcon.svg b/frontend/static/icons/Hipster_FacebookIcon.svg new file mode 100755 index 0000000..41093ad --- /dev/null +++ b/frontend/static/icons/Hipster_FacebookIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_GooglePlayIcon.svg b/frontend/static/icons/Hipster_GooglePlayIcon.svg new file mode 100755 index 0000000..128e761 --- /dev/null +++ b/frontend/static/icons/Hipster_GooglePlayIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_HelpIcon.svg b/frontend/static/icons/Hipster_HelpIcon.svg new file mode 100755 index 0000000..3d50868 --- /dev/null +++ b/frontend/static/icons/Hipster_HelpIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_HeroLogo.svg b/frontend/static/icons/Hipster_HeroLogo.svg new file mode 100755 index 0000000..203d4c1 --- /dev/null +++ b/frontend/static/icons/Hipster_HeroLogo.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_HeroLogoCyan.svg b/frontend/static/icons/Hipster_HeroLogoCyan.svg new file mode 100755 index 0000000..35d343b --- /dev/null +++ b/frontend/static/icons/Hipster_HeroLogoCyan.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_InstagramIcon.svg b/frontend/static/icons/Hipster_InstagramIcon.svg new file mode 100755 index 0000000..1927fb4 --- /dev/null +++ b/frontend/static/icons/Hipster_InstagramIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_KitchenwareOffer.svg b/frontend/static/icons/Hipster_KitchenwareOffer.svg new file mode 100755 index 0000000..4f5a3db --- /dev/null +++ b/frontend/static/icons/Hipster_KitchenwareOffer.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_NavLogo.svg b/frontend/static/icons/Hipster_NavLogo.svg new file mode 100755 index 0000000..bb6dafe --- /dev/null +++ b/frontend/static/icons/Hipster_NavLogo.svg @@ -0,0 +1,142 @@ + + + + + + image/svg+xml + + Hipster + + + + + + + + Hipster + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/static/icons/Hipster_PinterestIcon.svg b/frontend/static/icons/Hipster_PinterestIcon.svg new file mode 100755 index 0000000..e24bfd7 --- /dev/null +++ b/frontend/static/icons/Hipster_PinterestIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_ProfileIcon.svg b/frontend/static/icons/Hipster_ProfileIcon.svg new file mode 100755 index 0000000..7d043a7 --- /dev/null +++ b/frontend/static/icons/Hipster_ProfileIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_SearchIcon.svg b/frontend/static/icons/Hipster_SearchIcon.svg new file mode 100755 index 0000000..36f894d --- /dev/null +++ b/frontend/static/icons/Hipster_SearchIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_TwitterIcon.svg b/frontend/static/icons/Hipster_TwitterIcon.svg new file mode 100755 index 0000000..276b3c6 --- /dev/null +++ b/frontend/static/icons/Hipster_TwitterIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/icons/Hipster_UpDownControl.svg b/frontend/static/icons/Hipster_UpDownControl.svg new file mode 100755 index 0000000..be33647 --- /dev/null +++ b/frontend/static/icons/Hipster_UpDownControl.svg @@ -0,0 +1,7 @@ + + Hipster + + + + + diff --git a/frontend/static/icons/Hipster_YoutubeIcon.svg b/frontend/static/icons/Hipster_YoutubeIcon.svg new file mode 100755 index 0000000..3d018e3 --- /dev/null +++ b/frontend/static/icons/Hipster_YoutubeIcon.svg @@ -0,0 +1 @@ +Hipster \ No newline at end of file diff --git a/frontend/static/images/Advert2BannerImage.png b/frontend/static/images/Advert2BannerImage.png new file mode 100644 index 0000000..ae01275 Binary files /dev/null and b/frontend/static/images/Advert2BannerImage.png differ diff --git a/frontend/static/images/AdvertBannerImage.png b/frontend/static/images/AdvertBannerImage.png new file mode 100644 index 0000000..c0b5b3a Binary files /dev/null and b/frontend/static/images/AdvertBannerImage.png differ diff --git a/frontend/static/images/HeroBannerImage.png b/frontend/static/images/HeroBannerImage.png new file mode 100644 index 0000000..69e9a34 Binary files /dev/null and b/frontend/static/images/HeroBannerImage.png differ diff --git a/frontend/static/images/HeroBannerImage2.png b/frontend/static/images/HeroBannerImage2.png new file mode 100755 index 0000000..26c4cef Binary files /dev/null and b/frontend/static/images/HeroBannerImage2.png differ diff --git a/frontend/static/images/VRHeadsets.png b/frontend/static/images/VRHeadsets.png new file mode 100644 index 0000000..dc9a9f3 Binary files /dev/null and b/frontend/static/images/VRHeadsets.png differ diff --git a/frontend/static/images/credits.txt b/frontend/static/images/credits.txt new file mode 100644 index 0000000..0ca9cea --- /dev/null +++ b/frontend/static/images/credits.txt @@ -0,0 +1,2 @@ +folded-clothes-on-white-chair.jpg,,https://unsplash.com/photos/fr0J5-GIVyg +folded-clothes-on-white-chair-wide.jpg,,https://unsplash.com/photos/fr0J5-GIVyg diff --git a/frontend/static/images/folded-clothes-on-white-chair-wide.jpg b/frontend/static/images/folded-clothes-on-white-chair-wide.jpg new file mode 100644 index 0000000..c675194 Binary files /dev/null and b/frontend/static/images/folded-clothes-on-white-chair-wide.jpg differ diff --git a/frontend/static/images/folded-clothes-on-white-chair.jpg b/frontend/static/images/folded-clothes-on-white-chair.jpg new file mode 100644 index 0000000..23948b7 Binary files /dev/null and b/frontend/static/images/folded-clothes-on-white-chair.jpg differ diff --git a/frontend/static/img/products/bamboo-glass-jar.jpg b/frontend/static/img/products/bamboo-glass-jar.jpg new file mode 100644 index 0000000..a897f19 Binary files /dev/null and b/frontend/static/img/products/bamboo-glass-jar.jpg differ diff --git a/frontend/static/img/products/candle-holder.jpg b/frontend/static/img/products/candle-holder.jpg new file mode 100644 index 0000000..e3e2789 Binary files /dev/null and b/frontend/static/img/products/candle-holder.jpg differ diff --git a/frontend/static/img/products/hairdryer.jpg b/frontend/static/img/products/hairdryer.jpg new file mode 100644 index 0000000..5b4db41 Binary files /dev/null and b/frontend/static/img/products/hairdryer.jpg differ diff --git a/frontend/static/img/products/loafers.jpg b/frontend/static/img/products/loafers.jpg new file mode 100644 index 0000000..f14c196 Binary files /dev/null and b/frontend/static/img/products/loafers.jpg differ diff --git a/frontend/static/img/products/mug.jpg b/frontend/static/img/products/mug.jpg new file mode 100644 index 0000000..3642036 Binary files /dev/null and b/frontend/static/img/products/mug.jpg differ diff --git a/frontend/static/img/products/salt-and-pepper-shakers.jpg b/frontend/static/img/products/salt-and-pepper-shakers.jpg new file mode 100644 index 0000000..b81264e Binary files /dev/null and b/frontend/static/img/products/salt-and-pepper-shakers.jpg differ diff --git a/frontend/static/img/products/sunglasses.jpg b/frontend/static/img/products/sunglasses.jpg new file mode 100644 index 0000000..f31b153 Binary files /dev/null and b/frontend/static/img/products/sunglasses.jpg differ diff --git a/frontend/static/img/products/tank-top.jpg b/frontend/static/img/products/tank-top.jpg new file mode 100644 index 0000000..2e3baa0 Binary files /dev/null and b/frontend/static/img/products/tank-top.jpg differ diff --git a/frontend/static/img/products/watch.jpg b/frontend/static/img/products/watch.jpg new file mode 100644 index 0000000..71f0c11 Binary files /dev/null and b/frontend/static/img/products/watch.jpg differ diff --git a/frontend/static/styles/cart.css b/frontend/static/styles/cart.css new file mode 100755 index 0000000..cb506cf --- /dev/null +++ b/frontend/static/styles/cart.css @@ -0,0 +1,110 @@ +/** + * Copyright 2020 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. + */ + +.cart-sections { + padding-bottom: 120px; + padding-top: 56px; + background-color: #F9F9F9; +} + +.cart-sections h3 { + font-size: 36px; + font-weight: normal; +} + +.cart-sections a.cymbal-button-primary:hover { + text-decoration: none; + color: white; +} + +/* Empty Cart Section */ + +.empty-cart-section { + max-width: 458px; + margin: auto; + text-align: center; +} + +.empty-cart-section a { + display: inline-block; /* So margin-top works. */ + margin-top: 32px; +} + +.empty-cart-section a:hover { + color: white; + text-decoration: none; +} + +/* Cart Summary Section */ + +.cart-summary-empty-cart-button { + margin-right: 10px; +} + +.cart-summary-item-row, +.cart-summary-shipping-row, +.cart-summary-total-row { + padding-bottom: 24px; + padding-top: 24px; + border-top: solid 1px rgba(154, 160, 166, 0.5); +} + +.cart-summary-item-row img { + border-radius: 20% 0 20% 20%; +} + +.cart-summary-item-row-item-id-row { + font-size: 12px; + color: #5C6063; +} + +.cart-summary-item-row h4 { + font-size: 18px; + font-weight: normal; +} + +/* Stick item quantity and cost to the bottom (for wider screens). */ +@media (min-width: 768px) { + .cart-summary-item-row .row:last-child { + position: absolute; + bottom: 0px; + width: 100%; + } +} + +/* Item cost (price). */ +.cart-summary-item-row .row:last-child strong { + font-weight: 500; +} + +.cart-summary-total-row { + font-size: 28px; +} + +/* Cart Checkout Form */ + +.cart-checkout-form h3 { + margin-bottom: 0; +} + +.payment-method-heading { + margin-top: 36px; +} + +/* "Place Order" button */ +.cart-checkout-form .cymbal-button-primary { + margin-top: 36px; +} \ No newline at end of file diff --git a/frontend/static/styles/order.css b/frontend/static/styles/order.css new file mode 100755 index 0000000..5b1d3e9 --- /dev/null +++ b/frontend/static/styles/order.css @@ -0,0 +1,53 @@ +/** + * Copyright 2020 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. + */ + +.order { + background: #F9F9F9; +} + +.order-complete-section { + max-width: 487px; + padding-top: 56px; + padding-bottom: 120px; +} + +.order-complete-section h3 { + margin: 0; + font-size: 36px; + font-weight: normal; +} + +.order-complete-section p { + margin-top: 8px; +} + +.order-complete-section .padding-y-24 { + padding-bottom: 24px; + padding-top: 24px; +} + +.order-complete-section .border-bottom-solid { + border-bottom: 1px solid rgba(154, 160, 166, 0.5); +} + +.order-complete-section .cymbal-button-primary { + margin-top: 24px; +} + +.order-complete-section a.cymbal-button-primary:hover { + text-decoration: none; + color: white; +} diff --git a/frontend/static/styles/styles.css b/frontend/static/styles/styles.css new file mode 100755 index 0000000..3df11ab --- /dev/null +++ b/frontend/static/styles/styles.css @@ -0,0 +1,629 @@ +/** + * Copyright 2020 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. + */ + +/* General */ + +html, body { + height: 100%; +} + +body { + color: #111111; + font-family: 'DM Sans', sans-serif; + display: flex; + flex-direction: column; +} + +/* Header */ + +header { + background-color: #853B5C; + color: white; +} + +/* +This allows the sub-navbar (white strip containing logo) +to be as wide as the browser window. +*/ +header > div:nth-child(2).navbar.sub-navbar { + padding-left: 0; + padding-right: 0; +} +header > div:nth-child(2) > .container { + max-width: none; +} + +header .cart-link { + position: relative; + display: block; + margin-left: 25px; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; +} + +header .cart-size-circle { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 24px; + left: 11px; + width: 16px; + height: 16px; + font-size: 11px; + border-radius: 4px 4px 0 4px; + color: white; + background-color: #853B5C; +} + +header .h-free-shipping { + font-size: 14px; +} + +header .h-controls { + display: flex; + justify-content: flex-end; +} + +header .h-control { + display: flex; + align-items: center; + font-size: 12px; + position: relative; + margin-left: 40px; + color: #605f64; +} + +header .h-control:first-child { + margin-left: 0; +} + +header .h-control input { + border: none; + padding: 0 31px 0 31px; + width: 250px; + height: 24px; + flex-shrink: 0; + background-color: #f2f2f2; + display: flex; + align-items: center; +} + +header .h-control input:focus { + outline: 0; + border: 0; + box-shadow: 0; +} + +header .icon { + width: 20px; + height: 20px; +} + +header .icon.search-icon { + width: 12px; + height: 13px; + position: absolute; + left: 10px; +} + +/* The currency drop-down. */ + +header img.currency-icon, header span.currency-icon { + position: relative; + left: 35px; + top: -1px; + width: 20px; + display: inline-block; + height: 20px; +} + +header span.currency-icon { + font-size: 16px; + text-align: center; +} + +header .h-control select { + display: flex; + align-items: center; + background: transparent; + border-radius: 0; + border: 1px solid #acacac; + width: 130px; + height: 40px; + flex-shrink: 0; + padding: 1px 0 0 45px; + font-size: 16px; + border-radius: 8px; +} + +header .icon.arrow { + position: absolute; + right: 25px; + width: 10px; + height: 5px; +} + +header .h-control::-webkit-input-placeholder { + /* Chrome/Opera/Safari */ + font-size: 12px; + color: #605f64; +} + +header .h-control::-moz-placeholder { + /* Firefox 19+ */ + font-size: 12px; + color: #605f64; +} + +header .h-control :-ms-input-placeholder { + /* IE 10+ */ + font-size: 12px; + color: #605f64; +} + +header .h-control :-moz-placeholder { + /* Firefox 18- */ + font-size: 12px; + color: #605f64; +} + +header .navbar.sub-navbar { + height: 60px; + background-color: white; + font-size: 15px; + color: #b4b2bb; + padding-top: 0; + padding-bottom: 0; + box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25); + z-index: 1; /* Need this to see the box-shadow on the home page. */ +} + +header .navbar.sub-navbar > .container { + padding-left: 26px; + padding-right: 26px; +} + +header .top-left-logo { + height: 40px; +} + +header .top-left-logo-cymbal { + height: 30px; +} + +header .navbar.sub-navbar .navbar-brand { + padding: 0; +} + +header .navbar.sub-navbar a { + color: #b4b2bb; +} + +header .navbar.sub-navbar nav a { + margin: 0 10px; +} + +header .navbar.sub-navbar .controls { + display: flex; + height: 60px; +} + +header .navbar.sub-navbar .controls a img { + width: 20px; + height: 20px; + margin-bottom: 3px; +} + +/* Footer */ + +footer.py-5 { + flex-shrink: 0; + padding: 0 !important; +} + +footer .footer-top { + padding: 60px 0px; + background-color: #570D2E; + color: white; +} + +footer .footer-top a { + color: white; + text-decoration: underline; +} + +/* The

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" }} +

+
+ Ad + + {{.Text}} + +
+
+{{ end }} \ No newline at end of file diff --git a/frontend/templates/cart.html b/frontend/templates/cart.html new file mode 100755 index 0000000..40e7828 --- /dev/null +++ b/frontend/templates/cart.html @@ -0,0 +1,233 @@ + + +{{ define "cart" }} + {{ template "header" . }} + +
+ + {{$.platform_name}} + +
+ +
+ + {{ if eq (len $.items) 0 }} +
+

Your shopping cart is empty!

+

Items you add to your shopping cart will appear here.

+ Continue Shopping +
+ {{ else }} +
+
+ +
+ +
+
+

Cart ({{ $.cart_size }})

+
+
+
+ + + Continue Shopping + +
+
+
+ + {{ range $.items }} +
+
+ + + +
+
+
+
+

{{ .Item.Name }}

+
+
+
+
+ SKU #{{ .Item.ID }} +
+
+
+
+ Quantity: {{ .Quantity }} +
+
+ + {{ renderMoney .Price }} + +
+
+
+
+ {{ end }} + +
+
Shipping
+
{{ renderMoney .shipping_cost }}
+
+ +
+
Total
+
{{ renderMoney .total_cost }}
+
+ +
+ +
+ +
+ +
+
+

Shipping Address

+
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+

Payment Method

+
+
+ +
+
+ + +
+
+ +
+
+ + + +
+
+ + + +
+
+ + +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ {{ end }} + +
+ + {{ if $.recommendations}} + {{ template "recommendations" $.recommendations }} + {{ end }} + + {{ template "footer" . }} + {{ end }} diff --git a/frontend/templates/error.html b/frontend/templates/error.html new file mode 100755 index 0000000..39d9c24 --- /dev/null +++ b/frontend/templates/error.html @@ -0,0 +1,40 @@ + + +{{ define "error" }} + {{ template "header" . }} +
+ + {{$.platform_name}} + +
+
+
+
+

Uh, oh!

+

Something has failed. Below are some details for debugging.

+ +

HTTP Status: {{.status_code}} {{.status}}

+
+                    {{- .error -}}
+                
+
+
+
+ + {{ template "footer" . }} + {{ end }} \ No newline at end of file diff --git a/frontend/templates/footer.html b/frontend/templates/footer.html new file mode 100755 index 0000000..93c4aee --- /dev/null +++ b/frontend/templates/footer.html @@ -0,0 +1,43 @@ + + +{{ define "footer" }} + + + + + + +{{ end }} diff --git a/frontend/templates/header.html b/frontend/templates/header.html new file mode 100755 index 0000000..4be6430 --- /dev/null +++ b/frontend/templates/header.html @@ -0,0 +1,87 @@ + + +{{ define "header" }} + + + + + + + + + {{ if $.is_cymbal_brand }} + Cymbal Shops + {{ else }} + Online Boutique + {{ end }} + + + + + + + + + {{ if $.is_cymbal_brand }} + + {{ else }} + + {{ end }} + + + +
+ + +
+ {{end}} \ No newline at end of file diff --git a/frontend/templates/home.html b/frontend/templates/home.html new file mode 100755 index 0000000..2606082 --- /dev/null +++ b/frontend/templates/home.html @@ -0,0 +1,78 @@ + + +{{ define "home" }} + +{{ template "header" . }} +
+ + {{$.platform_name}} + +
+
+ + +
+ +
+
+ + +
+ +
+ +
+ +
+

Hot Products

+
+ + {{ range $.products }} +
+ + +
+
+
+
{{ .Item.Name }}
+
{{ renderMoney .Price }}
+
+
+ {{ end }} + +
+ + + + +
+ +
+
+ +
+ + +
+ {{ template "footer" . }} +
+ +{{ end }} \ No newline at end of file diff --git a/frontend/templates/order.html b/frontend/templates/order.html new file mode 100755 index 0000000..042140e --- /dev/null +++ b/frontend/templates/order.html @@ -0,0 +1,80 @@ + + +{{ define "order" }} + + {{ template "header" . }} + +
+ + {{$.platform_name}} + +
+ +
+ +
+
+
+

+ Your order is complete! +

+
+
+

We've sent you a confirmation email.

+
+
+
+
+ Confirmation # +
+
+ {{.order.OrderID}} +
+
+
+
+ Tracking # +
+
+ {{.order.ShippingTrackingID}} +
+
+
+
+ Total Paid +
+
+ {{renderMoney .total_paid}} +
+
+ +
+ + {{ if $.recommendations }} + {{ template "recommendations" $.recommendations }} + {{ end }} + +
+ + {{ template "footer" . }} + {{ end }} diff --git a/frontend/templates/product.html b/frontend/templates/product.html new file mode 100755 index 0000000..a7ce7d5 --- /dev/null +++ b/frontend/templates/product.html @@ -0,0 +1,67 @@ + + +{{ define "product" }} +{{ template "header" . }} +
+ + {{$.platform_name}} + +
+ +
+
+
+
+ +
+
+
+ +

{{ $.product.Item.Name }}

+

{{ renderMoney $.product.Price }}

+

{{ $.product.Item.Description }}

+ +
+ +
+ + +
+ +
+
+
+
+
+
+ {{ if $.recommendations}} + {{ template "recommendations" $.recommendations }} + {{ end }} +
+
+ {{ with $.ad }}{{ template "text_ad" . }}{{ end }} +
+
+{{ template "footer" . }} +{{ end }} \ No newline at end of file diff --git a/frontend/templates/recommendations.html b/frontend/templates/recommendations.html new file mode 100755 index 0000000..9563dcb --- /dev/null +++ b/frontend/templates/recommendations.html @@ -0,0 +1,43 @@ + + +{{ define "recommendations" }} +
+
+
+
+

You May Also Like

+
+ {{range . }} +
+
+ + + +
+
+ {{ .Name }} +
+
+
+
+ {{ end }} +
+
+
+
+
+{{ end }} \ No newline at end of file diff --git a/frontend/weaver_gen.go b/frontend/weaver_gen.go new file mode 100644 index 0000000..5a07f24 --- /dev/null +++ b/frontend/weaver_gen.go @@ -0,0 +1,92 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package frontend + +import ( + "context" + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/runtime/codegen" + "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/Main", + Iface: reflect.TypeOf((*weaver.Main)(nil)).Elem(), + Impl: reflect.TypeOf(Server{}), + Listeners: []string{"boutique"}, + LocalStubFn: func(impl any, caller string, tracer trace.Tracer) any { + return main_local_stub{impl: impl.(weaver.Main), tracer: tracer} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { return main_client_stub{stub: stub} }, + ServerStubFn: func(impl any, addLoad func(uint64, float64)) codegen.Server { + return main_server_stub{impl: impl.(weaver.Main), addLoad: addLoad} + }, + RefData: "⟦36ba6b75:wEaVeReDgE:github.com/ServiceWeaver/weaver/Main→github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T⟧\n⟦ad903f0a:wEaVeReDgE:github.com/ServiceWeaver/weaver/Main→github.com/ServiceWeaver/weaver/examples/onlineboutique/currencyservice/T⟧\n⟦ae7426b7:wEaVeReDgE:github.com/ServiceWeaver/weaver/Main→github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice/T⟧\n⟦3324d893:wEaVeReDgE:github.com/ServiceWeaver/weaver/Main→github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice/T⟧\n⟦f76a2b4a:wEaVeReDgE:github.com/ServiceWeaver/weaver/Main→github.com/ServiceWeaver/weaver/examples/onlineboutique/checkoutservice/T⟧\n⟦dd0dfbe8:wEaVeReDgE:github.com/ServiceWeaver/weaver/Main→github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T⟧\n⟦24712bd9:wEaVeReDgE:github.com/ServiceWeaver/weaver/Main→github.com/ServiceWeaver/weaver/examples/onlineboutique/adservice/T⟧\n⟦29a161ab:wEaVeRlIsTeNeRs:github.com/ServiceWeaver/weaver/Main→boutique⟧\n", + }) +} + +// weaver.InstanceOf checks. +var _ weaver.InstanceOf[weaver.Main] = (*Server)(nil) + +// weaver.Router checks. +var _ weaver.Unrouted = (*Server)(nil) + +// Local stub implementations. + +type main_local_stub struct { + impl weaver.Main + tracer trace.Tracer +} + +// Check that main_local_stub implements the weaver.Main interface. +var _ weaver.Main = (*main_local_stub)(nil) + +// Client stub implementations. + +type main_client_stub struct { + stub codegen.Stub +} + +// Check that main_client_stub implements the weaver.Main interface. +var _ weaver.Main = (*main_client_stub)(nil) + +// Server stub implementations. + +type main_server_stub struct { + impl weaver.Main + addLoad func(key uint64, load float64) +} + +// Check that main_server_stub implements the codegen.Server interface. +var _ codegen.Server = (*main_server_stub)(nil) + +// GetStubFn implements the codegen.Server interface. +func (s main_server_stub) GetStubFn(method string) func(ctx context.Context, args []byte) ([]byte, error) { + switch method { + default: + return nil + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ac711c2 --- /dev/null +++ b/go.mod @@ -0,0 +1,54 @@ +module onlineboutique + +go 1.20 + +require ( + github.com/ServiceWeaver/weaver v0.18.0 + github.com/google/uuid v1.3.0 + github.com/hashicorp/golang-lru/v2 v2.0.1 + go.opentelemetry.io/otel v1.16.0 + go.opentelemetry.io/otel/trace v1.16.0 + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/DataDog/hyperloglog v0.0.0-20220804205443-1806d9b66146 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/cel-go v0.17.1 // indirect + github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lightstep/varopt v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.opentelemetry.io/otel/sdk v1.16.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/term v0.10.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.11.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230717213848-3f92550aa753 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/protobuf v1.31.0 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/sqlite v1.24.0 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..553f407 --- /dev/null +++ b/go.sum @@ -0,0 +1,118 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/DataDog/hyperloglog v0.0.0-20220804205443-1806d9b66146 h1:S5WsRc58vIeuhvbz0V0FKs19nTbh5z23DCutLIXJkFA= +github.com/DataDog/hyperloglog v0.0.0-20220804205443-1806d9b66146/go.mod h1:hFPkswc42pKhRbeKDKXy05mRi7J1kJ2vMNbvd9erH0M= +github.com/DataDog/mmh3 v0.0.0-20210722141835-012dc69a9e49 h1:EbzDX8HPk5uE2FsJYxD74QmMw0/3CqSKhEr6teh0ncQ= +github.com/ServiceWeaver/weaver v0.18.0 h1:b62r1E9mYnCaPEH+KOT34FsMjSZNLFSlXEnNLTR0pyM= +github.com/ServiceWeaver/weaver v0.18.0/go.mod h1:/tJzitb+h8nLeHa4Mk7iIGU5ZV4OGB4C9IvIfD8n/1I= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dustin/randbo v0.0.0-20140428231429-7f1b564ca724 h1:1/c0u68+2LRI+XSpduQpV9BnKx1k1P6GTb3MVxCE3w4= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/cel-go v0.17.1 h1:s2151PDGy/eqpCI80/8dl4VL3xTkqI/YubXLXCFw0mw= +github.com/google/cel-go v0.17.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= +github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= +github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/lightstep/varopt v1.3.0 h1:H7OhtEBhYyDhoMu+wJGl4mTqM9TrYYdThG+xLGU3fZQ= +github.com/lightstep/varopt v1.3.0/go.mod h1:3GP18zB7pfvbVUAnJ8xfvYjpwp0CF027QRD5FsfXau0= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 h1:+XWJd3jf75RXJq29mxbuXhCXFDG3S3R4vBUeSI2P7tE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0/go.mod h1:hqgzBPTf4yONMFgdZvL/bK42R/iinTyVQtiWihs3SZc= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= +golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20230717213848-3f92550aa753 h1:lCbbUxUDD+DiXx9Q6F/ttL0aAu7N2pz8XnmMm8ZW4NE= +google.golang.org/genproto/googleapis/api v0.0.0-20230717213848-3f92550aa753/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI= +modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f71b023 --- /dev/null +++ b/main.go @@ -0,0 +1,42 @@ +// 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 main implements a demo shopping application called Online Boutique. +// +// This application is a forked version of Google Cloud Online Boutique +// app [1], with the following changes: +// - It is written entirely in Go. +// - It is written as a single Service Weaver application. +// - It is written to use Service Weaver specific logging/tracing/monitoring. +// +// [1]: https://github.com/GoogleCloudPlatform/microservices-demo +package main + +import ( + "context" + "flag" + "log" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/frontend" +) + +//go:generate ../../cmd/weaver/weaver generate ./... + +func main() { + flag.Parse() + if err := weaver.Run(context.Background(), frontend.Serve); err != nil { + log.Fatal(err) + } +} diff --git a/onlineboutique b/onlineboutique new file mode 100755 index 0000000..9364a16 Binary files /dev/null and b/onlineboutique differ diff --git a/paymentservice/charge.go b/paymentservice/charge.go new file mode 100644 index 0000000..4828c3c --- /dev/null +++ b/paymentservice/charge.go @@ -0,0 +1,75 @@ +// 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 paymentservice + +import ( + "fmt" + "strings" + "time" + + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" + "github.com/google/uuid" + "golang.org/x/exp/slog" +) + +type InvalidCreditCardErr struct{} + +func (e InvalidCreditCardErr) Error() string { + return "invalid credit card" +} + +type UnacceptedCreditCardErr struct{} + +func (e UnacceptedCreditCardErr) Error() string { + return "credit card not accepted; only VISA or MasterCard are accepted" +} + +type ExpiredCreditCardErr struct{} + +func (e ExpiredCreditCardErr) Error() string { + return "credit card expired" +} + +func charge(amount money.T, card CreditCardInfo, logger *slog.Logger) (string, error) { + // Perform some rudimentary validation. + number := strings.ReplaceAll(card.Number, "-", "") + var company string + switch { + case len(number) < 4: + return "", InvalidCreditCardErr{} + case number[0] == '4': + company = "Visa" + case number[0] == '5': + company = "MasterCard" + default: + return "", InvalidCreditCardErr{} + } + if card.CVV < 100 || card.CVV > 9999 { + return "", InvalidCreditCardErr{} + } + if time.Date(card.ExpirationYear, card.ExpirationMonth, 0, 0, 0, 0, 0, time.Local).Before(time.Now()) { + return "", ExpiredCreditCardErr{} + } + + // Card is valid: process the transaction. + logger.Info( + "Transaction processed", + "company", company, + "last_four", number[len(number)-4:], + "currency", amount.CurrencyCode, + "amount", fmt.Sprintf("%d.%d", amount.Units, amount.Nanos), + ) + return uuid.New().String(), nil +} diff --git a/paymentservice/service.go b/paymentservice/service.go new file mode 100644 index 0000000..dfcd663 --- /dev/null +++ b/paymentservice/service.go @@ -0,0 +1,54 @@ +// 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 paymentservice + +import ( + "context" + "time" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" +) + +type CreditCardInfo struct { + weaver.AutoMarshal + Number string + CVV int32 + ExpirationYear int + ExpirationMonth time.Month +} + +// LastFour returns the last four digits of the card number. +func (c CreditCardInfo) LastFour() string { + num := c.Number + if len(num) > 4 { + num = num[len(num)-4:] + } + return num +} + +type T interface { + Charge(ctx context.Context, amount money.T, card CreditCardInfo) (string, error) +} + +type impl struct { + weaver.Implements[T] +} + +// Charge charges the given amount of money to the given credit card, returning +// the transaction id. +func (s *impl) Charge(ctx context.Context, amount money.T, card CreditCardInfo) (string, error) { + return charge(amount, card, s.Logger()) +} diff --git a/paymentservice/weaver_gen.go b/paymentservice/weaver_gen.go new file mode 100644 index 0000000..7c7bfcc --- /dev/null +++ b/paymentservice/weaver_gen.go @@ -0,0 +1,235 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package paymentservice + +import ( + "context" + "errors" + "fmt" + "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" + "time" +) + +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/paymentservice/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, chargeMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/paymentservice/T", Method: "Charge", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, chargeMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/paymentservice/T", Method: "Charge", 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 + chargeMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) Charge(ctx context.Context, a0 money.T, a1 CreditCardInfo) (r0 string, err error) { + // Update metrics. + begin := s.chargeMetrics.Begin() + defer func() { s.chargeMetrics.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, "paymentservice.T.Charge", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.Charge(ctx, a0, a1) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + chargeMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) Charge(ctx context.Context, a0 money.T, a1 CreditCardInfo) (r0 string, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.chargeMetrics.Begin() + defer func() { s.chargeMetrics.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, "paymentservice.T.Charge", 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) + (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) + r0 = dec.String() + 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 "Charge": + return s.charge + default: + return nil + } +} + +func (s t_server_stub) charge(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 CreditCardInfo + (&a1).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.Charge(ctx, a0, a1) + + // Encode the results. + enc := codegen.NewEncoder() + enc.String(r0) + enc.Error(appErr) + return enc.Data(), nil +} + +// AutoMarshal implementations. + +var _ codegen.AutoMarshal = (*CreditCardInfo)(nil) + +type __is_CreditCardInfo[T ~struct { + weaver.AutoMarshal + Number string + CVV int32 + ExpirationYear int + ExpirationMonth time.Month +}] struct{} + +var _ __is_CreditCardInfo[CreditCardInfo] + +func (x *CreditCardInfo) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("CreditCardInfo.WeaverMarshal: nil receiver")) + } + enc.String(x.Number) + enc.Int32(x.CVV) + enc.Int(x.ExpirationYear) + enc.Int((int)(x.ExpirationMonth)) +} + +func (x *CreditCardInfo) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("CreditCardInfo.WeaverUnmarshal: nil receiver")) + } + x.Number = dec.String() + x.CVV = dec.Int32() + x.ExpirationYear = dec.Int() + *(*int)(&x.ExpirationMonth) = dec.Int() +} diff --git a/productcatalogservice/products.json b/productcatalogservice/products.json new file mode 100644 index 0000000..f229ecf --- /dev/null +++ b/productcatalogservice/products.json @@ -0,0 +1,110 @@ +[ + { + "id": "OLJCESPC7Z", + "name": "Sunglasses", + "description": "Add a modern touch to your outfits with these sleek aviator sunglasses.", + "picture": "/static/img/products/sunglasses.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 19, + "nanos": 990000000 + }, + "categories": ["accessories"] + }, + { + "id": "66VCHSJNUP", + "name": "Tank Top", + "description": "Perfectly cropped cotton tank, with a scooped neckline.", + "picture": "/static/img/products/tank-top.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 18, + "nanos": 990000000 + }, + "categories": ["clothing", "tops"] + }, + { + "id": "1YMWWN1N4O", + "name": "Watch", + "description": "This gold-tone stainless steel watch will work with most of your outfits.", + "picture": "/static/img/products/watch.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 109, + "nanos": 990000000 + }, + "categories": ["accessories"] + }, + { + "id": "L9ECAV7KIM", + "name": "Loafers", + "description": "A neat addition to your summer wardrobe.", + "picture": "/static/img/products/loafers.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 89, + "nanos": 990000000 + }, + "categories": ["footwear"] + }, + { + "id": "2ZYFJ3GM2N", + "name": "Hairdryer", + "description": "This lightweight hairdryer has 3 heat and speed settings. It's perfect for travel.", + "picture": "/static/img/products/hairdryer.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 24, + "nanos": 990000000 + }, + "categories": ["hair", "beauty"] + }, + { + "id": "0PUK6V6EV0", + "name": "Candle Holder", + "description": "This small but intricate candle holder is an excellent gift.", + "picture": "/static/img/products/candle-holder.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 18, + "nanos": 990000000 + }, + "categories": ["decor", "home"] + }, + { + "id": "LS4PSXUNUM", + "name": "Salt & Pepper Shakers", + "description": "Add some flavor to your kitchen.", + "picture": "/static/img/products/salt-and-pepper-shakers.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 18, + "nanos": 490000000 + }, + "categories": ["kitchen"] + }, + { + "id": "9SIQT8TOJO", + "name": "Bamboo Glass Jar", + "description": "This bamboo glass jar can hold 57 oz (1.7 l) and is perfect for any kitchen.", + "picture": "/static/img/products/bamboo-glass-jar.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 5, + "nanos": 490000000 + }, + "categories": ["kitchen"] + }, + { + "id": "6E92ZMYYFZ", + "name": "Mug", + "description": "A simple mug with a mustard interior.", + "picture": "/static/img/products/mug.jpg", + "priceUsd": { + "currencyCode": "USD", + "units": 8, + "nanos": 990000000 + }, + "categories": ["kitchen"] + } +] \ No newline at end of file diff --git a/productcatalogservice/service.go b/productcatalogservice/service.go new file mode 100644 index 0000000..d8d3cf9 --- /dev/null +++ b/productcatalogservice/service.go @@ -0,0 +1,164 @@ +// 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 productcatalogservice + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" +) + +var ( + //go:embed products.json + catalogFileData []byte +) + +type NotFoundError struct{} + +func (e NotFoundError) Error() string { return "product not found" } + +type Product struct { + weaver.AutoMarshal + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Picture string `json:"picture"` + PriceUSD money.T `json:"priceUsd"` + + // Categories such as "clothing" or "kitchen" that can be used to look up + // other related products. + Categories []string `json:"categories"` +} + +type T interface { + ListProducts(ctx context.Context) ([]Product, error) + GetProduct(ctx context.Context, productID string) (Product, error) + SearchProducts(ctx context.Context, query string) ([]Product, error) +} + +type impl struct { + weaver.Implements[T] + + extraLatency time.Duration + + mu sync.RWMutex + cat []Product + reloadCatalog bool +} + +func (s *impl) Init(context.Context) error { + var extraLatency time.Duration + if extra := os.Getenv("EXTRA_LATENCY"); extra != "" { + v, err := time.ParseDuration(extra) + if err != nil { + return fmt.Errorf("failed to parse EXTRA_LATENCY (%s) as time.Duration: %+v", v, err) + } + extraLatency = v + s.Logger().Info("extra latency enabled", "duration", extraLatency) + } + s.extraLatency = extraLatency + _, err := s.refreshCatalogFile() + if err != nil { + return fmt.Errorf("could not parse product catalog: %w", err) + } + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGUSR1, syscall.SIGUSR2) + go func() { + for { + sig := <-sigs + s.Logger().Info("Received signal", "signal", sig) + reload := false + if sig == syscall.SIGUSR1 { + reload = true + s.Logger().Info("Enable catalog reloading") + } else { + s.Logger().Info("Disable catalog reloading") + } + s.mu.Lock() + s.reloadCatalog = reload + s.mu.Unlock() + } + }() + + return nil +} + +func (s *impl) refreshCatalogFile() ([]Product, error) { + var products []Product + if err := json.Unmarshal(catalogFileData, &products); err != nil { + return nil, err + } + s.mu.Lock() + defer s.mu.Unlock() + s.cat = products + return products, nil +} + +func (s *impl) getCatalogState() (bool, []Product) { + s.mu.RLock() + defer s.mu.RUnlock() + return s.reloadCatalog, s.cat +} + +func (s *impl) parseCatalog() []Product { + reload, products := s.getCatalogState() + if reload || len(products) == 0 { + var err error + if products, err = s.refreshCatalogFile(); err != nil { + products = nil + } + } + return products +} + +func (s *impl) ListProducts(ctx context.Context) ([]Product, error) { + time.Sleep(s.extraLatency) + return s.parseCatalog(), nil +} + +func (s *impl) GetProduct(ctx context.Context, productID string) (Product, error) { + time.Sleep(s.extraLatency) + for _, p := range s.parseCatalog() { + if p.ID == productID { + return p, nil + } + } + return Product{}, NotFoundError{} +} + +func (s *impl) SearchProducts(ctx context.Context, query string) ([]Product, error) { + time.Sleep(s.extraLatency) + + // Interpret query as a substring match in name or description. + var ps []Product + for _, p := range s.parseCatalog() { + if strings.Contains(strings.ToLower(p.Name), strings.ToLower(query)) || + strings.Contains(strings.ToLower(p.Description), strings.ToLower(query)) { + ps = append(ps, p) + } + } + return ps, nil +} diff --git a/productcatalogservice/weaver_gen.go b/productcatalogservice/weaver_gen.go new file mode 100644 index 0000000..b8b5ff8 --- /dev/null +++ b/productcatalogservice/weaver_gen.go @@ -0,0 +1,486 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package productcatalogservice + +import ( + "context" + "errors" + "fmt" + "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/productcatalogservice/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, getProductMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T", Method: "GetProduct", Remote: false}), listProductsMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T", Method: "ListProducts", Remote: false}), searchProductsMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T", Method: "SearchProducts", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, getProductMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T", Method: "GetProduct", Remote: true}), listProductsMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T", Method: "ListProducts", Remote: true}), searchProductsMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/T", Method: "SearchProducts", 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 + getProductMetrics *codegen.MethodMetrics + listProductsMetrics *codegen.MethodMetrics + searchProductsMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) GetProduct(ctx context.Context, a0 string) (r0 Product, err error) { + // Update metrics. + begin := s.getProductMetrics.Begin() + defer func() { s.getProductMetrics.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, "productcatalogservice.T.GetProduct", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.GetProduct(ctx, a0) +} + +func (s t_local_stub) ListProducts(ctx context.Context) (r0 []Product, err error) { + // Update metrics. + begin := s.listProductsMetrics.Begin() + defer func() { s.listProductsMetrics.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, "productcatalogservice.T.ListProducts", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.ListProducts(ctx) +} + +func (s t_local_stub) SearchProducts(ctx context.Context, a0 string) (r0 []Product, err error) { + // Update metrics. + begin := s.searchProductsMetrics.Begin() + defer func() { s.searchProductsMetrics.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, "productcatalogservice.T.SearchProducts", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.SearchProducts(ctx, a0) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + getProductMetrics *codegen.MethodMetrics + listProductsMetrics *codegen.MethodMetrics + searchProductsMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) GetProduct(ctx context.Context, a0 string) (r0 Product, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.getProductMetrics.Begin() + defer func() { s.getProductMetrics.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, "productcatalogservice.T.GetProduct", 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, 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) ListProducts(ctx context.Context) (r0 []Product, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.listProductsMetrics.Begin() + defer func() { s.listProductsMetrics.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, "productcatalogservice.T.ListProducts", 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_Product_3e9d9e07(dec) + err = dec.Error() + return +} + +func (s t_client_stub) SearchProducts(ctx context.Context, a0 string) (r0 []Product, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.searchProductsMetrics.Begin() + defer func() { s.searchProductsMetrics.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, "productcatalogservice.T.SearchProducts", 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_Product_3e9d9e07(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 "GetProduct": + return s.getProduct + case "ListProducts": + return s.listProducts + case "SearchProducts": + return s.searchProducts + default: + return nil + } +} + +func (s t_server_stub) getProduct(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.GetProduct(ctx, a0) + + // Encode the results. + enc := codegen.NewEncoder() + (r0).WeaverMarshal(enc) + enc.Error(appErr) + return enc.Data(), nil +} + +func (s t_server_stub) listProducts(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.ListProducts(ctx) + + // Encode the results. + enc := codegen.NewEncoder() + serviceweaver_enc_slice_Product_3e9d9e07(enc, r0) + enc.Error(appErr) + return enc.Data(), nil +} + +func (s t_server_stub) searchProducts(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.SearchProducts(ctx, a0) + + // Encode the results. + enc := codegen.NewEncoder() + serviceweaver_enc_slice_Product_3e9d9e07(enc, r0) + enc.Error(appErr) + return enc.Data(), nil +} + +// AutoMarshal implementations. + +var _ codegen.AutoMarshal = (*Product)(nil) + +type __is_Product[T ~struct { + weaver.AutoMarshal + ID string "json:\"id\"" + Name string "json:\"name\"" + Description string "json:\"description\"" + Picture string "json:\"picture\"" + PriceUSD money.T "json:\"priceUsd\"" + Categories []string "json:\"categories\"" +}] struct{} + +var _ __is_Product[Product] + +func (x *Product) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("Product.WeaverMarshal: nil receiver")) + } + enc.String(x.ID) + enc.String(x.Name) + enc.String(x.Description) + enc.String(x.Picture) + (x.PriceUSD).WeaverMarshal(enc) + serviceweaver_enc_slice_string_4af10117(enc, x.Categories) +} + +func (x *Product) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("Product.WeaverUnmarshal: nil receiver")) + } + x.ID = dec.String() + x.Name = dec.String() + x.Description = dec.String() + x.Picture = dec.String() + (&x.PriceUSD).WeaverUnmarshal(dec) + x.Categories = serviceweaver_dec_slice_string_4af10117(dec) +} + +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 +} + +// Encoding/decoding implementations. + +func serviceweaver_enc_slice_Product_3e9d9e07(enc *codegen.Encoder, arg []Product) { + 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_Product_3e9d9e07(dec *codegen.Decoder) []Product { + n := dec.Len() + if n == -1 { + return nil + } + res := make([]Product, n) + for i := 0; i < n; i++ { + (&res[i]).WeaverUnmarshal(dec) + } + return res +} diff --git a/recommendationservice/service.go b/recommendationservice/service.go new file mode 100644 index 0000000..87ceaf0 --- /dev/null +++ b/recommendationservice/service.go @@ -0,0 +1,66 @@ +// 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 recommendationservice + +import ( + "context" + "math/rand" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice" +) + +type T interface { + ListRecommendations(ctx context.Context, userID string, productIDs []string) ([]string, error) +} + +type impl struct { + weaver.Implements[T] + catalogService weaver.Ref[productcatalogservice.T] +} + +func (s *impl) ListRecommendations(ctx context.Context, userID string, userProductIDs []string) ([]string, error) { + // Fetch a list of products from the product catalog. + catalogProducts, err := s.catalogService.Get().ListProducts(ctx) + if err != nil { + return nil, err + } + + // Remove user-provided products from the catalog, to avoid recommending + // them. + userIDs := make(map[string]struct{}, len(userProductIDs)) + for _, id := range userProductIDs { + userIDs[id] = struct{}{} + } + filtered := make([]string, 0, len(catalogProducts)) + for _, product := range catalogProducts { + if _, ok := userIDs[product.ID]; ok { + continue + } + filtered = append(filtered, product.ID) + } + + // Sample from filtered products and return them. + perm := rand.Perm(len(filtered)) + const maxResponses = 5 + ret := make([]string, 0, maxResponses) + for _, idx := range perm { + ret = append(ret, filtered[idx]) + if len(ret) >= maxResponses { + break + } + } + return ret, nil +} diff --git a/recommendationservice/weaver_gen.go b/recommendationservice/weaver_gen.go new file mode 100644 index 0000000..70d6e84 --- /dev/null +++ b/recommendationservice/weaver_gen.go @@ -0,0 +1,223 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package recommendationservice + +import ( + "context" + "errors" + "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/recommendationservice/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, listRecommendationsMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice/T", Method: "ListRecommendations", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, listRecommendationsMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice/T", Method: "ListRecommendations", Remote: true})} + }, + ServerStubFn: func(impl any, addLoad func(uint64, float64)) codegen.Server { + return t_server_stub{impl: impl.(T), addLoad: addLoad} + }, + RefData: "⟦d212c866:wEaVeReDgE:github.com/ServiceWeaver/weaver/examples/onlineboutique/recommendationservice/T→github.com/ServiceWeaver/weaver/examples/onlineboutique/productcatalogservice/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 + listRecommendationsMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) ListRecommendations(ctx context.Context, a0 string, a1 []string) (r0 []string, err error) { + // Update metrics. + begin := s.listRecommendationsMetrics.Begin() + defer func() { s.listRecommendationsMetrics.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, "recommendationservice.T.ListRecommendations", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.ListRecommendations(ctx, a0, a1) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + listRecommendationsMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) ListRecommendations(ctx context.Context, a0 string, a1 []string) (r0 []string, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.listRecommendationsMetrics.Begin() + defer func() { s.listRecommendationsMetrics.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, "recommendationservice.T.ListRecommendations", 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_string_4af10117(enc, 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 = 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 "ListRecommendations": + return s.listRecommendations + default: + return nil + } +} + +func (s t_server_stub) listRecommendations(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 []string + a1 = 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.ListRecommendations(ctx, a0, a1) + + // 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/shippingservice/quote.go b/shippingservice/quote.go new file mode 100644 index 0000000..5dbdb69 --- /dev/null +++ b/shippingservice/quote.go @@ -0,0 +1,45 @@ +// 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 shippingservice + +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 quote. +func createQuoteFromCount(count int) quote { + return createQuoteFromFloat(8.99) +} + +// createQuoteFromFloat takes a price represented as a float and creates a quote. +func createQuoteFromFloat(value float64) quote { + units, fraction := math.Modf(value) + return quote{ + uint32(units), + uint32(math.Trunc(fraction * 100)), + } +} diff --git a/shippingservice/service.go b/shippingservice/service.go new file mode 100644 index 0000000..4001477 --- /dev/null +++ b/shippingservice/service.go @@ -0,0 +1,70 @@ +// 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 shippingservice + +import ( + "context" + "fmt" + + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" +) + +type Address struct { + weaver.AutoMarshal + StreetAddress string + City string + State string + Country string + ZipCode int32 +} + +type T interface { + GetQuote(ctx context.Context, addr Address, items []cartservice.CartItem) (money.T, error) + ShipOrder(ctx context.Context, addr Address, items []cartservice.CartItem) (string, error) +} + +type impl struct { + weaver.Implements[T] +} + +// GetQuote produces a shipping quote (cost) in USD. +func (s *impl) GetQuote(ctx context.Context, addr Address, items []cartservice.CartItem) (money.T, error) { + s.Logger().Info("[GetQuote] received request") + defer s.Logger().Info("[GetQuote] completed request") + + // 1. Generate a quote based on the total number of items to be shipped. + quote := createQuoteFromCount(len(items)) + + // 2. Generate a response. + return money.T{ + CurrencyCode: "USD", + Units: int64(quote.Dollars), + Nanos: int32(quote.Cents * 10000000), + }, nil +} + +// ShipOrder mocks that the requested items will be shipped. +// It supplies a tracking ID for notional lookup of shipment delivery status. +func (s *impl) ShipOrder(ctx context.Context, addr Address, items []cartservice.CartItem) (string, error) { + s.Logger().Info("[ShipOrder] received request") + defer s.Logger().Info("[ShipOrder] completed request") + + // Create a Tracking ID. + baseAddress := fmt.Sprintf("%s, %s, %s", addr.StreetAddress, addr.City, addr.State) + id := createTrackingID(baseAddress) + return id, nil +} diff --git a/shippingservice/tracker.go b/shippingservice/tracker.go new file mode 100644 index 0000000..0013ef5 --- /dev/null +++ b/shippingservice/tracker.go @@ -0,0 +1,56 @@ +// 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 shippingservice + +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(25)) +} + +// 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 +} diff --git a/shippingservice/weaver_gen.go b/shippingservice/weaver_gen.go new file mode 100644 index 0000000..0b3b431 --- /dev/null +++ b/shippingservice/weaver_gen.go @@ -0,0 +1,366 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package shippingservice + +import ( + "context" + "errors" + "fmt" + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice" + "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/shippingservice/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, getQuoteMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T", Method: "GetQuote", Remote: false}), shipOrderMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T", Method: "ShipOrder", Remote: false})} + }, + ClientStubFn: func(stub codegen.Stub, caller string) any { + return t_client_stub{stub: stub, getQuoteMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T", Method: "GetQuote", Remote: true}), shipOrderMetrics: codegen.MethodMetricsFor(codegen.MethodLabels{Caller: caller, Component: "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice/T", Method: "ShipOrder", 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 + getQuoteMetrics *codegen.MethodMetrics + shipOrderMetrics *codegen.MethodMetrics +} + +// Check that t_local_stub implements the T interface. +var _ T = (*t_local_stub)(nil) + +func (s t_local_stub) GetQuote(ctx context.Context, a0 Address, a1 []cartservice.CartItem) (r0 money.T, err error) { + // Update metrics. + begin := s.getQuoteMetrics.Begin() + defer func() { s.getQuoteMetrics.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, "shippingservice.T.GetQuote", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.GetQuote(ctx, a0, a1) +} + +func (s t_local_stub) ShipOrder(ctx context.Context, a0 Address, a1 []cartservice.CartItem) (r0 string, err error) { + // Update metrics. + begin := s.shipOrderMetrics.Begin() + defer func() { s.shipOrderMetrics.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, "shippingservice.T.ShipOrder", trace.WithSpanKind(trace.SpanKindInternal)) + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + } + + return s.impl.ShipOrder(ctx, a0, a1) +} + +// Client stub implementations. + +type t_client_stub struct { + stub codegen.Stub + getQuoteMetrics *codegen.MethodMetrics + shipOrderMetrics *codegen.MethodMetrics +} + +// Check that t_client_stub implements the T interface. +var _ T = (*t_client_stub)(nil) + +func (s t_client_stub) GetQuote(ctx context.Context, a0 Address, a1 []cartservice.CartItem) (r0 money.T, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.getQuoteMetrics.Begin() + defer func() { s.getQuoteMetrics.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, "shippingservice.T.GetQuote", 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) + serviceweaver_enc_slice_CartItem_7a7ff11c(enc, 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) ShipOrder(ctx context.Context, a0 Address, a1 []cartservice.CartItem) (r0 string, err error) { + // Update metrics. + var requestBytes, replyBytes int + begin := s.shipOrderMetrics.Begin() + defer func() { s.shipOrderMetrics.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, "shippingservice.T.ShipOrder", 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) + serviceweaver_enc_slice_CartItem_7a7ff11c(enc, a1) + 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) + r0 = dec.String() + 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 "GetQuote": + return s.getQuote + case "ShipOrder": + return s.shipOrder + default: + return nil + } +} + +func (s t_server_stub) getQuote(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 Address + (&a0).WeaverUnmarshal(dec) + var a1 []cartservice.CartItem + a1 = serviceweaver_dec_slice_CartItem_7a7ff11c(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.GetQuote(ctx, a0, a1) + + // Encode the results. + enc := codegen.NewEncoder() + (r0).WeaverMarshal(enc) + enc.Error(appErr) + return enc.Data(), nil +} + +func (s t_server_stub) shipOrder(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 Address + (&a0).WeaverUnmarshal(dec) + var a1 []cartservice.CartItem + a1 = serviceweaver_dec_slice_CartItem_7a7ff11c(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.ShipOrder(ctx, a0, a1) + + // Encode the results. + enc := codegen.NewEncoder() + enc.String(r0) + enc.Error(appErr) + return enc.Data(), nil +} + +// AutoMarshal implementations. + +var _ codegen.AutoMarshal = (*Address)(nil) + +type __is_Address[T ~struct { + weaver.AutoMarshal + StreetAddress string + City string + State string + Country string + ZipCode int32 +}] struct{} + +var _ __is_Address[Address] + +func (x *Address) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("Address.WeaverMarshal: nil receiver")) + } + enc.String(x.StreetAddress) + enc.String(x.City) + enc.String(x.State) + enc.String(x.Country) + enc.Int32(x.ZipCode) +} + +func (x *Address) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("Address.WeaverUnmarshal: nil receiver")) + } + x.StreetAddress = dec.String() + x.City = dec.String() + x.State = dec.String() + x.Country = dec.String() + x.ZipCode = dec.Int32() +} + +// Encoding/decoding implementations. + +func serviceweaver_enc_slice_CartItem_7a7ff11c(enc *codegen.Encoder, arg []cartservice.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) []cartservice.CartItem { + n := dec.Len() + if n == -1 { + return nil + } + res := make([]cartservice.CartItem, n) + for i := 0; i < n; i++ { + (&res[i]).WeaverUnmarshal(dec) + } + return res +} diff --git a/types/money/money.go b/types/money/money.go new file mode 100644 index 0000000..ee535ac --- /dev/null +++ b/types/money/money.go @@ -0,0 +1,152 @@ +// 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 money + +import ( + "errors" + + "github.com/ServiceWeaver/weaver" +) + +const ( + nanosMin = -999999999 + nanosMax = +999999999 + nanosMod = 1000000000 +) + +var ( + ErrInvalidValue = errors.New("one of the specified money values is invalid") + ErrMismatchingCurrency = errors.New("mismatching currency codes") +) + +// T represents an amount of money along with the currency type. +type T struct { + weaver.AutoMarshal + + // The 3-letter currency code defined in ISO 4217. + CurrencyCode string `json:"currencyCode"` + + // The whole units of the amount. + // For example if `currencyCode` is `"USD"`, then 1 unit is one US dollar. + Units int64 `json:"units"` + + // Number of nano (10^-9) units of the amount. + // The value must be between -999,999,999 and +999,999,999 inclusive. + // If `units` is positive, `nanos` must be positive or zero. + // If `units` is zero, `nanos` can be positive, zero, or negative. + // If `units` is negative, `nanos` must be negative or zero. + // For example $-1.75 is represented as `units`=-1 and `nanos`=-750,000,000. + Nanos int32 `json:"nanos"` +} + +// IsValid checks if specified value has a valid units/nanos signs and ranges. +func IsValid(m T) bool { + return signMatches(m) && validNanos(m.Nanos) +} + +func signMatches(m T) bool { + return m.Nanos == 0 || m.Units == 0 || (m.Nanos < 0) == (m.Units < 0) +} + +func validNanos(nanos int32) bool { return nanosMin <= nanos && nanos <= nanosMax } + +// IsZero returns true if the specified money value is equal to zero. +func IsZero(m T) bool { return m.Units == 0 && m.Nanos == 0 } + +// IsPositive returns true if the specified money value is valid and is +// positive. +func IsPositive(m T) bool { + return IsValid(m) && m.Units > 0 || (m.Units == 0 && m.Nanos > 0) +} + +// IsNegative returns true if the specified money value is valid and is +// negative. +func IsNegative(m T) bool { + return IsValid(m) && m.Units < 0 || (m.Units == 0 && m.Nanos < 0) +} + +// AreSameCurrency returns true if values l and r have a currency code and +// they are the same values. +func AreSameCurrency(l, r T) bool { + return l.CurrencyCode == r.CurrencyCode && l.CurrencyCode != "" +} + +// AreEquals returns true if values l and r are the equal, including the +// currency. This does not check validity of the provided values. +func AreEquals(l, r T) bool { + return l.CurrencyCode == r.CurrencyCode && + l.Units == r.Units && l.Nanos == r.Nanos +} + +// Negate returns the same amount with the sign negated. +func Negate(m T) T { + return T{ + Units: -m.Units, + Nanos: -m.Nanos, + CurrencyCode: m.CurrencyCode} +} + +// Must panics if the given error is not nil. This can be used with other +// functions like: "m := Must(Sum(a,b))". +func Must(v T, err error) T { + if err != nil { + panic(err) + } + return v +} + +// Sum adds two values. Returns an error if one of the values are invalid or +// currency codes are not matching (unless currency code is unspecified for +// both). +func Sum(l, r T) (T, error) { + if !IsValid(l) || !IsValid(r) { + return T{}, ErrInvalidValue + } else if l.CurrencyCode != r.CurrencyCode { + return T{}, ErrMismatchingCurrency + } + units := l.Units + r.Units + nanos := l.Nanos + r.Nanos + + if (units == 0 && nanos == 0) || (units > 0 && nanos >= 0) || (units < 0 && nanos <= 0) { + // same sign + units += int64(nanos / nanosMod) + nanos = nanos % nanosMod + } else { + // different sign. nanos guaranteed to not to go over the limit + if units > 0 { + units-- + nanos += nanosMod + } else { + units++ + nanos -= nanosMod + } + } + + return T{ + Units: units, + Nanos: nanos, + CurrencyCode: l.CurrencyCode}, nil +} + +// MultiplySlow is a slow multiplication operation done through adding the value +// to itself n-1 times. +func MultiplySlow(m T, n uint32) T { + out := m + for n > 1 { + out = Must(Sum(out, m)) + n-- + } + return out +} diff --git a/types/money/money_test.go b/types/money/money_test.go new file mode 100644 index 0000000..98ae307 --- /dev/null +++ b/types/money/money_test.go @@ -0,0 +1,245 @@ +// 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 money + +import ( + "fmt" + "reflect" + "testing" +) + +func mmc(u int64, n int32, c string) T { + return T{Units: u, Nanos: n, CurrencyCode: c} +} +func mm(u int64, n int32) T { return mmc(u, n, "") } + +func TestIsValid(t *testing.T) { + tests := []struct { + name string + in T + want bool + }{ + {"valid -/-", mm(-981273891273, -999999999), true}, + {"invalid -/+", mm(-981273891273, +999999999), false}, + {"valid +/+", mm(981273891273, 999999999), true}, + {"invalid +/-", mm(981273891273, -999999999), false}, + {"invalid +/+overflow", mm(3, 1000000000), false}, + {"invalid +/-overflow", mm(3, -1000000000), false}, + {"invalid -/+overflow", mm(-3, 1000000000), false}, + {"invalid -/-overflow", mm(-3, -1000000000), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsValid(tt.in); got != tt.want { + t.Errorf("IsValid(%v) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} + +func TestIsZero(t *testing.T) { + tests := []struct { + name string + in T + want bool + }{ + {"zero", mm(0, 0), true}, + {"not-zero (-/+)", mm(-1, +1), false}, + {"not-zero (-/-)", mm(-1, -1), false}, + {"not-zero (+/+)", mm(+1, +1), false}, + {"not-zero (+/-)", mm(+1, -1), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsZero(tt.in); got != tt.want { + t.Errorf("IsZero(%v) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} + +func TestIsPositive(t *testing.T) { + tests := []struct { + name string + in T + want bool + }{ + {"zero", mm(0, 0), false}, + {"positive (+/+)", mm(+1, +1), true}, + {"invalid (-/+)", mm(-1, +1), false}, + {"negative (-/-)", mm(-1, -1), false}, + {"invalid (+/-)", mm(+1, -1), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsPositive(tt.in); got != tt.want { + t.Errorf("IsPositive(%v) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} + +func TestIsNegative(t *testing.T) { + tests := []struct { + name string + in T + want bool + }{ + {"zero", mm(0, 0), false}, + {"positive (+/+)", mm(+1, +1), false}, + {"invalid (-/+)", mm(-1, +1), false}, + {"negative (-/-)", mm(-1, -1), true}, + {"invalid (+/-)", mm(+1, -1), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsNegative(tt.in); got != tt.want { + t.Errorf("IsNegative(%v) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} + +func TestAreSameCurrency(t *testing.T) { + type args struct { + l T + r T + } + tests := []struct { + name string + args args + want bool + }{ + {"both empty currency", args{mmc(1, 0, ""), mmc(2, 0, "")}, false}, + {"left empty currency", args{mmc(1, 0, ""), mmc(2, 0, "USD")}, false}, + {"right empty currency", args{mmc(1, 0, "USD"), mmc(2, 0, "")}, false}, + {"mismatching", args{mmc(1, 0, "USD"), mmc(2, 0, "CAD")}, false}, + {"matching", args{mmc(1, 0, "USD"), mmc(2, 0, "USD")}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := AreSameCurrency(tt.args.l, tt.args.r); got != tt.want { + t.Errorf("AreSameCurrency([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want) + } + }) + } +} + +func TestAreEquals(t *testing.T) { + type args struct { + l T + r T + } + tests := []struct { + name string + args args + want bool + }{ + {"equals", args{mmc(1, 2, "USD"), mmc(1, 2, "USD")}, true}, + {"mismatching currency", args{mmc(1, 2, "USD"), mmc(1, 2, "CAD")}, false}, + {"mismatching units", args{mmc(10, 20, "USD"), mmc(1, 20, "USD")}, false}, + {"mismatching nanos", args{mmc(1, 2, "USD"), mmc(1, 20, "USD")}, false}, + {"negated", args{mmc(1, 2, "USD"), mmc(-1, -2, "USD")}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := AreEquals(tt.args.l, tt.args.r); got != tt.want { + t.Errorf("AreEquals([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want) + } + }) + } +} + +func TestNegate(t *testing.T) { + tests := []struct { + name string + in T + want T + }{ + {"zero", mm(0, 0), mm(0, 0)}, + {"negative", mm(-1, -200), mm(1, 200)}, + {"positive", mm(1, 200), mm(-1, -200)}, + {"carries currency code", mmc(0, 0, "XXX"), mmc(0, 0, "XXX")}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Negate(tt.in); !AreEquals(got, tt.want) { + t.Errorf("Negate([%v]) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} + +func TestMust_pass(t *testing.T) { + v := Must(mm(2, 3), nil) + if !AreEquals(v, mm(2, 3)) { + t.Errorf("returned the wrong value: %v", v) + } +} + +func TestMust_panic(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Logf("panic captured: %v", r) + } + }() + Must(mm(2, 3), fmt.Errorf("some error")) + t.Fatal("this should not have executed due to the panic above") +} + +func TestSum(t *testing.T) { + type args struct { + l T + r T + } + tests := []struct { + name string + args args + want T + wantErr error + }{ + {"0+0=0", args{mm(0, 0), mm(0, 0)}, mm(0, 0), nil}, + {"Error: currency code on left", args{mmc(0, 0, "XXX"), mm(0, 0)}, mm(0, 0), ErrMismatchingCurrency}, + {"Error: currency code on right", args{mm(0, 0), mmc(0, 0, "YYY")}, mm(0, 0), ErrMismatchingCurrency}, + {"Error: currency code mismatch", args{mmc(0, 0, "AAA"), mmc(0, 0, "BBB")}, mm(0, 0), ErrMismatchingCurrency}, + {"Error: invalid +/-", args{mm(+1, -1), mm(0, 0)}, mm(0, 0), ErrInvalidValue}, + {"Error: invalid -/+", args{mm(0, 0), mm(-1, +2)}, mm(0, 0), ErrInvalidValue}, + {"Error: invalid nanos", args{mm(0, 1000000000), mm(1, 0)}, mm(0, 0), ErrInvalidValue}, + {"both positive (no carry)", args{mm(2, 200000000), mm(2, 200000000)}, mm(4, 400000000), nil}, + {"both positive (nanos=max)", args{mm(2, 111111111), mm(2, 888888888)}, mm(4, 999999999), nil}, + {"both positive (carry)", args{mm(2, 200000000), mm(2, 900000000)}, mm(5, 100000000), nil}, + {"both negative (no carry)", args{mm(-2, -200000000), mm(-2, -200000000)}, mm(-4, -400000000), nil}, + {"both negative (carry)", args{mm(-2, -200000000), mm(-2, -900000000)}, mm(-5, -100000000), nil}, + {"mixed (larger positive, just decimals)", args{mm(11, 0), mm(-2, 0)}, mm(9, 0), nil}, + {"mixed (larger negative, just decimals)", args{mm(-11, 0), mm(2, 0)}, mm(-9, 0), nil}, + {"mixed (larger positive, no borrow)", args{mm(11, 100000000), mm(-2, -100000000)}, mm(9, 0), nil}, + {"mixed (larger positive, with borrow)", args{mm(11, 100000000), mm(-2, -9000000 /*.09*/)}, mm(9, 91000000 /*.091*/), nil}, + {"mixed (larger negative, no borrow)", args{mm(-11, -100000000), mm(2, 100000000)}, mm(-9, 0), nil}, + {"mixed (larger negative, with borrow)", args{mm(-11, -100000000), mm(2, 9000000 /*.09*/)}, mm(-9, -91000000 /*.091*/), nil}, + {"0+negative", args{mm(0, 0), mm(-2, -100000000)}, mm(-2, -100000000), nil}, + {"negative+0", args{mm(-2, -100000000), mm(0, 0)}, mm(-2, -100000000), nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Sum(tt.args.l, tt.args.r) + if err != tt.wantErr { + t.Errorf("Sum([%v],[%v]): expected err=\"%v\" got=\"%v\"", tt.args.l, tt.args.r, tt.wantErr, err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Sum([%v],[%v]) = %v, want %v", tt.args.l, tt.args.r, got, tt.want) + } + }) + } +} diff --git a/types/money/weaver_gen.go b/types/money/weaver_gen.go new file mode 100644 index 0000000..d403513 --- /dev/null +++ b/types/money/weaver_gen.go @@ -0,0 +1,71 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package money + +import ( + "fmt" + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/runtime/codegen" +) + +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. + +`) + +// weaver.InstanceOf checks. + +// weaver.Router checks. + +// Local stub implementations. + +// Client stub implementations. + +// Server stub implementations. + +// AutoMarshal implementations. + +var _ codegen.AutoMarshal = (*T)(nil) + +type __is_T[T ~struct { + weaver.AutoMarshal + CurrencyCode string "json:\"currencyCode\"" + Units int64 "json:\"units\"" + Nanos int32 "json:\"nanos\"" +}] struct{} + +var _ __is_T[T] + +func (x *T) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("T.WeaverMarshal: nil receiver")) + } + enc.String(x.CurrencyCode) + enc.Int64(x.Units) + enc.Int32(x.Nanos) +} + +func (x *T) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("T.WeaverUnmarshal: nil receiver")) + } + x.CurrencyCode = dec.String() + x.Units = dec.Int64() + x.Nanos = dec.Int32() +} diff --git a/types/order.go b/types/order.go new file mode 100644 index 0000000..9d0efdb --- /dev/null +++ b/types/order.go @@ -0,0 +1,38 @@ +// 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 types + +import ( + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" +) + +// Order represents a user order. +type Order struct { + weaver.AutoMarshal + OrderID string + ShippingTrackingID string + ShippingCost money.T + ShippingAddress shippingservice.Address + Items []OrderItem +} + +type OrderItem struct { + weaver.AutoMarshal + Item cartservice.CartItem + Cost money.T +} diff --git a/types/type.go b/types/type.go new file mode 100644 index 0000000..1b0f2f4 --- /dev/null +++ b/types/type.go @@ -0,0 +1,16 @@ +// 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 types stores common types shared between services. +package types diff --git a/types/weaver_gen.go b/types/weaver_gen.go new file mode 100644 index 0000000..ec2bbea --- /dev/null +++ b/types/weaver_gen.go @@ -0,0 +1,129 @@ +// Code generated by "weaver generate". DO NOT EDIT. +//go:build !ignoreWeaverGen + +package types + +import ( + "fmt" + "github.com/ServiceWeaver/weaver" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/cartservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/shippingservice" + "github.com/ServiceWeaver/weaver/examples/onlineboutique/types/money" + "github.com/ServiceWeaver/weaver/runtime/codegen" +) + +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. + +`) + +// weaver.InstanceOf checks. + +// weaver.Router checks. + +// Local stub implementations. + +// Client stub implementations. + +// Server stub implementations. + +// AutoMarshal implementations. + +var _ codegen.AutoMarshal = (*Order)(nil) + +type __is_Order[T ~struct { + weaver.AutoMarshal + OrderID string + ShippingTrackingID string + ShippingCost money.T + ShippingAddress shippingservice.Address + Items []OrderItem +}] struct{} + +var _ __is_Order[Order] + +func (x *Order) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("Order.WeaverMarshal: nil receiver")) + } + enc.String(x.OrderID) + enc.String(x.ShippingTrackingID) + (x.ShippingCost).WeaverMarshal(enc) + (x.ShippingAddress).WeaverMarshal(enc) + serviceweaver_enc_slice_OrderItem_2b9377cb(enc, x.Items) +} + +func (x *Order) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("Order.WeaverUnmarshal: nil receiver")) + } + x.OrderID = dec.String() + x.ShippingTrackingID = dec.String() + (&x.ShippingCost).WeaverUnmarshal(dec) + (&x.ShippingAddress).WeaverUnmarshal(dec) + x.Items = serviceweaver_dec_slice_OrderItem_2b9377cb(dec) +} + +func serviceweaver_enc_slice_OrderItem_2b9377cb(enc *codegen.Encoder, arg []OrderItem) { + 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_OrderItem_2b9377cb(dec *codegen.Decoder) []OrderItem { + n := dec.Len() + if n == -1 { + return nil + } + res := make([]OrderItem, n) + for i := 0; i < n; i++ { + (&res[i]).WeaverUnmarshal(dec) + } + return res +} + +var _ codegen.AutoMarshal = (*OrderItem)(nil) + +type __is_OrderItem[T ~struct { + weaver.AutoMarshal + Item cartservice.CartItem + Cost money.T +}] struct{} + +var _ __is_OrderItem[OrderItem] + +func (x *OrderItem) WeaverMarshal(enc *codegen.Encoder) { + if x == nil { + panic(fmt.Errorf("OrderItem.WeaverMarshal: nil receiver")) + } + (x.Item).WeaverMarshal(enc) + (x.Cost).WeaverMarshal(enc) +} + +func (x *OrderItem) WeaverUnmarshal(dec *codegen.Decoder) { + if x == nil { + panic(fmt.Errorf("OrderItem.WeaverUnmarshal: nil receiver")) + } + (&x.Item).WeaverUnmarshal(dec) + (&x.Cost).WeaverUnmarshal(dec) +} diff --git a/weaver.toml b/weaver.toml new file mode 100644 index 0000000..9b631fd --- /dev/null +++ b/weaver.toml @@ -0,0 +1,13 @@ +[serviceweaver] +binary = "./onlineboutique" +rollout = "5m" + +[single] +listeners.boutique = {address = "localhost:12345"} + +[multi] +listeners.boutique = {address = "localhost:12345"} + +[gke] +regions = ["us-west1"] +listeners.boutique = {public_hostname = "onlineboutique.serviceweaver.dev"}