├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── bus
└── event_bridge.go
├── domain
├── products.go
├── products_stream.go
└── products_test.go
├── functions
├── delete-product
│ ├── event.json
│ └── main.go
├── get-product
│ ├── event.json
│ └── main.go
├── get-products
│ └── main.go
├── products-stream
│ ├── event.json
│ └── main.go
└── put-product
│ ├── event.json
│ └── main.go
├── go.mod
├── go.sum
├── handlers
├── apigateway.go
└── dynamodb.go
├── imgs
├── diagram.png
└── load-test.jpeg
├── integration-testing
└── integration_test.go
├── load-testing
├── generator.js
└── load-test.yml
├── store
├── dynamodb.go
└── memory.go
├── template.yaml
├── tools
├── go.mod
├── go.sum
└── tools.go
└── types
├── bus.go
├── mocks
└── mock_store.go
├── product.go
└── store.go
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | pull_request:
5 |
6 | jobs:
7 | tests:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | go:
12 | - "1.17"
13 | - "1.16"
14 | steps:
15 | - name: Set up Go ${{ matrix.go }}
16 | uses: actions/setup-go@v2
17 | with:
18 | go-version: ${{ matrix.go }}
19 | id: go
20 |
21 | - name: Check out code into the Go module directory
22 | uses: actions/checkout@v2
23 |
24 | - name: Cache go modules
25 | uses: actions/cache@v2
26 | with:
27 | path: |
28 | ~/.cache/go-build
29 | ~/go/pkg/mod
30 | key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('**/go.sum') }}
31 | restore-keys: |
32 | ${{ runner.os }}-go-${{ matrix.go }}
33 |
34 | - name: Run linter
35 | if: matrix.go == '1.17'
36 | run: make lint
37 |
38 | - name: Run tests
39 | run: go test -tags=unit -race -coverprofile=coverage.txt -covermode=atomic ./...
40 |
41 | - name: Upload coverage to Codecov
42 | uses: codecov/codecov-action@v2
43 | if: matrix.go == '1.17'
44 | with:
45 | files: ./coverage.txt
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .aws-sam
2 | .vscode
3 | bootstrap
4 | env-vars.json
5 | samconfig.toml
6 | bin
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
16 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | STACK_NAME ?= serverless-go-demo
2 | FUNCTIONS := get-products get-product put-product delete-product products-stream
3 | REGION := eu-central-1
4 |
5 | # To try different version of Go
6 | GO := go
7 |
8 | # Make sure to install aarch64 GCC compilers if you want to compile with GCC.
9 | CC := aarch64-linux-gnu-gcc
10 | GCCGO := aarch64-linux-gnu-gccgo-10
11 |
12 | ci: build tests-unit
13 |
14 | build:
15 | ${MAKE} ${MAKEOPTS} $(foreach function,${FUNCTIONS}, build-${function})
16 |
17 | build-%:
18 | cd functions/$* && GOOS=linux GOARCH=arm64 CGO_ENABLED=0 ${GO} build -o bootstrap
19 |
20 | build-gcc:
21 | ${MAKE} ${MAKEOPTS} $(foreach function,${FUNCTIONS}, build-gcc-${function})
22 |
23 | build-gcc-%:
24 | cd functions/$* && GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=${CC} ${GO} build -o bootstrap
25 |
26 | build-gcc-optimized:
27 | ${MAKE} ${MAKEOPTS} $(foreach function,${FUNCTIONS}, build-gcc-optimized-${function})
28 |
29 | build-gcc-optimized-%:
30 | cd functions/$* && GOOS=linux GOARCH=arm64 GCCGO=${GCCGO} ${GO} build -compiler gccgo -gccgoflags '-static -Ofast -march=armv8.2-a+fp16+rcpc+dotprod+crypto -mtune=neoverse-n1 -moutline-atomics' -o bootstrap
31 |
32 | invoke:
33 | @sam local invoke --env-vars env-vars.json GetProductsFunction
34 |
35 | invoke-put:
36 | @sam local invoke --env-vars env-vars.json --event functions/put-product/event.json PutProductFunction
37 |
38 | invoke-get:
39 | @sam local invoke --env-vars env-vars.json --event functions/get-product/event.json GetProductFunction
40 |
41 | invoke-delete:
42 | @sam local invoke --env-vars env-vars.json --event functions/delete-product/event.json DeleteProductFunction
43 |
44 | invoke-stream:
45 | @sam local invoke --env-vars env-vars.json --event functions/products-stream/event.json DDBStreamsFunction
46 |
47 | clean:
48 | @rm $(foreach function,${FUNCTIONS}, functions/${function}/bootstrap)
49 |
50 | deploy:
51 | if [ -f samconfig.toml ]; \
52 | then sam deploy --stack-name ${STACK_NAME}; \
53 | else sam deploy -g --stack-name ${STACK_NAME}; \
54 | fi
55 |
56 | tests-unit:
57 | @go test -v -tags=unit -bench=. -benchmem -cover ./...
58 |
59 | tests-integ:
60 | API_URL=$$(aws cloudformation describe-stacks --stack-name $(STACK_NAME) \
61 | --region $(REGION) \
62 | --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' \
63 | --output text) go test -v -tags=integration ./...
64 |
65 | tests-load:
66 | API_URL=$$(aws cloudformation describe-stacks --stack-name $(STACK_NAME) \
67 | --region $(REGION) \
68 | --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' \
69 | --output text) artillery run load-testing/load-test.yml
70 |
71 | export GOBIN ?= $(shell pwd)/bin
72 |
73 | STATICCHECK = $(GOBIN)/staticcheck
74 |
75 | # Many Go tools take file globs or directories as arguments instead of packages
76 | GO_FILES := $(shell \
77 | find . '(' -path '*/.*' -o -path './vendor' ')' -prune \
78 | -o -name '*.go' -print | cut -b3-)
79 | MODULE_DIRS = .
80 |
81 | .PHONY: lint
82 | lint: $(STATICCHECK)
83 | @rm -rf lint.log
84 | @echo "Checking formatting..."
85 | @gofmt -d -s $(GO_FILES) 2>&1 | tee lint.log
86 | @echo "Checking vet..."
87 | @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go vet ./... 2>&1) &&) true | tee -a lint.log
88 | @echo "Checking staticcheck..."
89 | @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(STATICCHECK) ./... 2>&1) &&) true | tee -a lint.log
90 | @echo "Checking for unresolved FIXMEs..."
91 | @git grep -i fixme | grep -v -e Makefile | tee -a lint.log
92 | @[ ! -s lint.log ]
93 | @rm lint.log
94 | @echo "Checking 'go mod tidy'..."
95 | @make tidy
96 | @if ! git diff --quiet; then \
97 | echo "'go diff tidy' resulted in chnges or working tree is dirty:"; \
98 | git --no-pager diff; \
99 | fi
100 |
101 | $(STATICCHECK):
102 | cd tools && go install honnef.co/go/tools/cmd/staticcheck
103 |
104 | .PHONY: tidy
105 | tidy:
106 | @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go mod tidy) &&) true
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Serverless Go Demo
2 |
3 | 
4 | [](https://codecov.io/gh/aws-samples/serverless-go-demo)
5 |
6 |
7 |
8 |
9 |
10 | This is a simple serverless application built in Golang. It consists of an API Gateway backed by four Lambda functions and a DynamoDB table for storage.
11 |
12 | This single project will create [five different binaries](./functions), one for each Lambda function. It uses an [hexagonal architecture pattern](https://aws.amazon.com/blogs/compute/developing-evolutionary-architecture-with-aws-lambda/) to decouple the [entry points](./handlers), from the main [domain logic](./domain), the [storage component](./store), and the [event bus component](./bus).
13 |
14 | ## 🏗️ Deployment and testing
15 |
16 | ### Requirements
17 |
18 | * [Go](https://go.dev)
19 | * The [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) for deploying to the cloud
20 | * [Artillery](https://artillery.io/) for load-testing the application
21 |
22 | ### Commands
23 |
24 | You can use the following commands at the root of this repository to test, build, and deploy this project:
25 |
26 | ```bash
27 | # Run unit tests
28 | make tests-unit
29 |
30 | # Compile and prepare Lambda functions
31 | make build
32 |
33 | # Deploy the functions on AWS
34 | make deploy
35 |
36 | # Run integration tests against the API in the cloud
37 | make tests-integ
38 | ```
39 |
40 | ## Load Test
41 |
42 | [Artillery](https://www.artillery.io/) is used to make 300 requests / second for 10 minutes to our API endpoints. You can run this
43 | with the following command:
44 |
45 | ```bash
46 | make tests-load
47 | ```
48 |
49 | ### CloudWatch Logs Insights
50 |
51 | Using this CloudWatch Logs Insights query you can analyse the latency of the requests made to the Lambda functions.
52 |
53 | The query separates cold starts from other requests and then gives you p50, p90 and p99 percentiles.
54 |
55 | The times bellow were obtained while running the sample code with 128MB of RAM and arm64 architecture.
56 |
57 | ```
58 | filter @type="REPORT"
59 | | fields greatest(@initDuration, 0) + @duration as duration, ispresent(@initDuration) as coldStart
60 | | stats count(*) as count, pct(duration, 50) as p50, pct(duration, 90) as p90, pct(duration, 99) as p99, max(duration) as max by coldStart
61 | ```
62 |
63 | 
64 |
65 | ## 👀 With other languages
66 |
67 | You can find implementations of this project in other languages here:
68 |
69 | * [⭐ Groovy](https://github.com/aws-samples/serverless-groovy-demo)
70 | * [☕ Java with GraalVM](https://github.com/aws-samples/serverless-graalvm-demo)
71 | * [🤖 Kotlin](https://github.com/aws-samples/serverless-kotlin-demo)
72 | * [🦀 Rust](https://github.com/aws-samples/serverless-rust-demo)
73 | * [🏗️ TypeScript](https://github.com/aws-samples/serverless-typescript-demo)
74 | * [🥅 .NET](https://github.com/aws-samples/serverless-dotnet-demo)
75 |
76 | ## Security
77 |
78 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
79 |
80 | ## License
81 |
82 | This library is licensed under the MIT-0 License. See the LICENSE file.
83 |
--------------------------------------------------------------------------------
/bus/event_bridge.go:
--------------------------------------------------------------------------------
1 | package bus
2 |
3 | import (
4 | "context"
5 | "log"
6 | "math"
7 |
8 | "github.com/aws-samples/serverless-go-demo/types"
9 |
10 | "github.com/aws/aws-sdk-go-v2/aws"
11 | "github.com/aws/aws-sdk-go-v2/config"
12 | "github.com/aws/aws-sdk-go-v2/service/cloudwatchevents"
13 | cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchevents/types"
14 | )
15 |
16 | type EventBridgeBus struct {
17 | client *cloudwatchevents.Client
18 | busName string
19 | }
20 |
21 | var _ types.Bus = (*EventBridgeBus)(nil)
22 |
23 | func NewEventBridgeBus(ctx context.Context, busName string) *EventBridgeBus {
24 | cfg, err := config.LoadDefaultConfig(ctx)
25 | if err != nil {
26 | log.Fatalf("unable to load SDK config, %v", err)
27 | }
28 |
29 | client := cloudwatchevents.NewFromConfig(cfg)
30 |
31 | return &EventBridgeBus{
32 | client: client,
33 | busName: busName,
34 | }
35 | }
36 |
37 | func (e *EventBridgeBus) Put(ctx context.Context, events []types.Event) ([]types.FailedEvent, error) {
38 | failedBatchEvents, err :=
39 | batchEvents(events, 10, func(batchEvents []types.Event) ([]types.FailedEvent, error) {
40 | eventBridgeEvents := make([]cloudwatchtypes.PutEventsRequestEntry, len(batchEvents))
41 |
42 | for i, event := range batchEvents {
43 | eventBridgeEvent := cloudwatchtypes.PutEventsRequestEntry{
44 | EventBusName: &e.busName,
45 | Source: aws.String(event.Source),
46 | Detail: aws.String(event.Detail),
47 | DetailType: aws.String(event.DetailType),
48 | Resources: event.Resources,
49 | }
50 |
51 | eventBridgeEvents[i] = eventBridgeEvent
52 | }
53 |
54 | result, err := e.client.PutEvents(ctx, &cloudwatchevents.PutEventsInput{
55 | Entries: eventBridgeEvents,
56 | })
57 |
58 | failedEvents := []types.FailedEvent{}
59 | if err != nil {
60 | return failedEvents, err
61 | }
62 |
63 | if result.FailedEntryCount > 0 {
64 | for i, entry := range result.Entries {
65 | if entry.EventId != nil {
66 | continue
67 | }
68 |
69 | failedEvent := types.FailedEvent{
70 | Event: batchEvents[i],
71 | FailureCode: *entry.ErrorCode,
72 | FailureMessage: *entry.ErrorMessage,
73 | }
74 |
75 | failedEvents = append(failedEvents, failedEvent)
76 | }
77 | }
78 |
79 | return failedEvents, nil
80 | })
81 |
82 | return failedBatchEvents, err
83 | }
84 |
85 | func batchEvents(events []types.Event, maxBatchSize uint, batchFn func([]types.Event) ([]types.FailedEvent, error)) ([]types.FailedEvent, error) {
86 | skip := 0
87 | recordsAmount := len(events)
88 | batchAmount := int(math.Ceil(float64(recordsAmount) / float64(maxBatchSize)))
89 |
90 | batchFailedEvents := []types.FailedEvent{}
91 |
92 | for i := 0; i < batchAmount; i++ {
93 | lowerBound := skip
94 | upperBound := skip + int(maxBatchSize)
95 |
96 | if upperBound > recordsAmount {
97 | upperBound = recordsAmount
98 | }
99 |
100 | batchEvents := events[lowerBound:upperBound]
101 | skip += int(maxBatchSize)
102 |
103 | failedEvents, err := batchFn(batchEvents)
104 | if err != nil {
105 | return batchFailedEvents, err
106 | }
107 |
108 | if len(failedEvents) > 0 {
109 | batchFailedEvents = append(batchFailedEvents, failedEvents...)
110 | }
111 | }
112 |
113 | return batchFailedEvents, nil
114 | }
115 |
--------------------------------------------------------------------------------
/domain/products.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "strings"
9 |
10 | "github.com/aws-samples/serverless-go-demo/types"
11 | )
12 |
13 | var (
14 | ErrJsonUnmarshal = errors.New("failed to parse product from request body")
15 | ErrProductIdMismatch = errors.New("product ID in path does not match product ID in body")
16 | )
17 |
18 | type Products struct {
19 | store types.Store
20 | }
21 |
22 | func NewProductsDomain(s types.Store) *Products {
23 | return &Products{
24 | store: s,
25 | }
26 | }
27 |
28 | func (d *Products) GetProduct(ctx context.Context, id string) (*types.Product, error) {
29 | product, err := d.store.Get(ctx, id)
30 | if err != nil {
31 | return nil, fmt.Errorf("%w", err)
32 | }
33 |
34 | return product, nil
35 | }
36 |
37 | func (d *Products) AllProducts(ctx context.Context, next *string) (types.ProductRange, error) {
38 | if next != nil && strings.TrimSpace(*next) == "" {
39 | next = nil
40 | }
41 |
42 | productRange, err := d.store.All(ctx, next)
43 | if err != nil {
44 | return productRange, fmt.Errorf("%w", err)
45 | }
46 |
47 | return productRange, nil
48 | }
49 |
50 | func (d *Products) PutProduct(ctx context.Context, id string, body []byte) (*types.Product, error) {
51 | product := types.Product{}
52 | if err := json.Unmarshal(body, &product); err != nil {
53 | return nil, fmt.Errorf("%w", ErrJsonUnmarshal)
54 | }
55 |
56 | if product.Id != id {
57 | return nil, fmt.Errorf("%w", ErrProductIdMismatch)
58 | }
59 |
60 | err := d.store.Put(ctx, product)
61 | if err != nil {
62 | return nil, fmt.Errorf("%w", err)
63 | }
64 |
65 | return &product, nil
66 | }
67 |
68 | func (d *Products) DeleteProduct(ctx context.Context, id string) error {
69 | err := d.store.Delete(ctx, id)
70 | if err != nil {
71 | return fmt.Errorf("%w", err)
72 | }
73 |
74 | return nil
75 | }
76 |
--------------------------------------------------------------------------------
/domain/products_stream.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/aws-samples/serverless-go-demo/types"
8 | )
9 |
10 | type ProductsStream struct {
11 | bus types.Bus
12 | }
13 |
14 | func NewProductsStream(b types.Bus) *ProductsStream {
15 | return &ProductsStream{
16 | bus: b,
17 | }
18 | }
19 |
20 | func (p *ProductsStream) Publish(ctx context.Context, events []types.Event) ([]types.FailedEvent, error) {
21 | failedEvents, err := p.bus.Put(ctx, events)
22 | if err != nil {
23 | return failedEvents, fmt.Errorf("%w", err)
24 | }
25 |
26 | return failedEvents, nil
27 | }
28 |
--------------------------------------------------------------------------------
/domain/products_test.go:
--------------------------------------------------------------------------------
1 | //go:build unit
2 | // +build unit
3 |
4 | package domain
5 |
6 | import (
7 | "context"
8 | "errors"
9 | "testing"
10 |
11 | "github.com/aws-samples/serverless-go-demo/store"
12 | "github.com/aws-samples/serverless-go-demo/types"
13 | "github.com/aws-samples/serverless-go-demo/types/mocks"
14 | "github.com/golang/mock/gomock"
15 | )
16 |
17 | func TestGetProductNotFound(t *testing.T) {
18 | memoryStore := store.NewMemoryStore()
19 | domain := NewProductsDomain(memoryStore)
20 |
21 | product, err := domain.GetProduct(context.Background(), "1")
22 | if err != nil {
23 | t.Errorf("GetProduct returned an error: %w", err)
24 | }
25 |
26 | if product != nil {
27 | t.Error("GetProduct returned unexpected product")
28 | }
29 | }
30 |
31 | func TestGetExistingProduct(t *testing.T) {
32 | ctx := context.Background()
33 |
34 | memoryStore := store.NewMemoryStore()
35 | memoryStore.Put(ctx, types.Product{
36 | Id: "iXR",
37 | Name: "iPhone XML",
38 | Price: 0.123,
39 | })
40 |
41 | domain := NewProductsDomain(memoryStore)
42 |
43 | product, err := domain.GetProduct(context.Background(), "iXR")
44 | if err != nil {
45 | t.Errorf("GetProduct returned an error: %w", err)
46 | }
47 |
48 | if product == nil {
49 | t.Errorf("GetProduct returned nil object")
50 | return
51 | }
52 |
53 | if product.Name != "iPhone XML" {
54 | t.Errorf("GetProduct returned wrong product name")
55 | }
56 |
57 | if product.Price != 0.123 {
58 | t.Errorf("GetProduct returned wrong price")
59 | }
60 | }
61 |
62 | func TestGetInternalStoreError(t *testing.T) {
63 | ctrl := gomock.NewController(t)
64 | ctx := context.Background()
65 |
66 | store := mocks.NewMockStore(ctrl)
67 | store.EXPECT().
68 | Get(ctx, gomock.Eq("1")).
69 | Return(nil, errors.New("internal error"))
70 |
71 | domain := NewProductsDomain(store)
72 |
73 | product, err := domain.GetProduct(ctx, "1")
74 | if product != nil {
75 | t.Error("Got unexpected product")
76 | }
77 |
78 | if err == nil {
79 | t.Error("Expecting an error to be returned")
80 | return
81 | }
82 |
83 | if err.Error() != "internal error" {
84 | t.Errorf("Got unexpected error: %s", err)
85 | }
86 | }
87 |
88 | func TestAllProductsWithInvalidNext(t *testing.T) {
89 | ctrl := gomock.NewController(t)
90 | ctx := context.Background()
91 |
92 | store := mocks.NewMockStore(ctrl)
93 | store.EXPECT().
94 | All(ctx, gomock.Nil()).
95 | AnyTimes()
96 |
97 | domain := NewProductsDomain(store)
98 |
99 | t.Parallel()
100 |
101 | t.Run("with nil 'next'", func(t *testing.T) {
102 | domain.AllProducts(ctx, nil)
103 | })
104 |
105 | t.Run("with empty 'next'", func(t *testing.T) {
106 | next := ""
107 | domain.AllProducts(ctx, &next)
108 | })
109 |
110 | t.Run("with empty spaces 'next'", func(t *testing.T) {
111 | next := " "
112 | domain.AllProducts(ctx, &next)
113 | })
114 | }
115 |
116 | func TestAllProductsInternalStoreError(t *testing.T) {
117 | ctrl := gomock.NewController(t)
118 | ctx := context.Background()
119 |
120 | store := mocks.NewMockStore(ctrl)
121 | store.EXPECT().
122 | All(ctx, gomock.All()).
123 | Return(types.ProductRange{}, errors.New("internal error"))
124 |
125 | domain := NewProductsDomain(store)
126 |
127 | _, err := domain.AllProducts(ctx, nil)
128 | if err == nil {
129 | t.Error("Expecting an error to be returned")
130 | return
131 | }
132 |
133 | if err.Error() != "internal error" {
134 | t.Errorf("Got unexpected error: %s", err)
135 | }
136 | }
137 |
138 | func TestAllProducts(t *testing.T) {
139 | memoryStore := store.NewMemoryStore()
140 | domain := NewProductsDomain(memoryStore)
141 | ctx := context.Background()
142 |
143 | t.Run("with an empty store", func(t *testing.T) {
144 | productRange, err := domain.AllProducts(ctx, nil)
145 | if err != nil {
146 | t.Errorf("Got unexpected error: %w", err)
147 | }
148 |
149 | if len(productRange.Products) != 0 {
150 | t.Errorf("Got unexpected products")
151 | }
152 | })
153 |
154 | t.Run("with products on the store", func(t *testing.T) {
155 | memoryStore.Put(ctx, types.Product{
156 | Id: "iXR",
157 | Name: "iPhone XML",
158 | Price: 0.123,
159 | })
160 |
161 | productRange, err := domain.AllProducts(ctx, nil)
162 | if err != nil {
163 | t.Errorf("Got unexpected error: %w", err)
164 | }
165 |
166 | if len(productRange.Products) != 1 {
167 | t.Errorf("Got unexpected products")
168 | }
169 | })
170 | }
171 |
--------------------------------------------------------------------------------
/functions/delete-product/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "",
3 | "resource": "/{id}",
4 | "path": "/1",
5 | "httpMethod": "DELETE",
6 | "isBase64Encoded": true,
7 | "queryStringParameters": {
8 | "foo": "bar"
9 | },
10 | "multiValueQueryStringParameters": {
11 | "foo": [
12 | "bar"
13 | ]
14 | },
15 | "pathParameters": {
16 | "id": "1"
17 | },
18 | "stageVariables": {
19 | "baz": "qux"
20 | },
21 | "headers": {
22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
23 | "Accept-Encoding": "gzip, deflate, sdch",
24 | "Accept-Language": "en-US,en;q=0.8",
25 | "Cache-Control": "max-age=0",
26 | "CloudFront-Forwarded-Proto": "https",
27 | "CloudFront-Is-Desktop-Viewer": "true",
28 | "CloudFront-Is-Mobile-Viewer": "false",
29 | "CloudFront-Is-SmartTV-Viewer": "false",
30 | "CloudFront-Is-Tablet-Viewer": "false",
31 | "CloudFront-Viewer-Country": "US",
32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
33 | "Upgrade-Insecure-Requests": "1",
34 | "User-Agent": "Custom User Agent String",
35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
38 | "X-Forwarded-Port": "443",
39 | "X-Forwarded-Proto": "https"
40 | },
41 | "multiValueHeaders": {
42 | "Accept": [
43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
44 | ],
45 | "Accept-Encoding": [
46 | "gzip, deflate, sdch"
47 | ],
48 | "Accept-Language": [
49 | "en-US,en;q=0.8"
50 | ],
51 | "Cache-Control": [
52 | "max-age=0"
53 | ],
54 | "CloudFront-Forwarded-Proto": [
55 | "https"
56 | ],
57 | "CloudFront-Is-Desktop-Viewer": [
58 | "true"
59 | ],
60 | "CloudFront-Is-Mobile-Viewer": [
61 | "false"
62 | ],
63 | "CloudFront-Is-SmartTV-Viewer": [
64 | "false"
65 | ],
66 | "CloudFront-Is-Tablet-Viewer": [
67 | "false"
68 | ],
69 | "CloudFront-Viewer-Country": [
70 | "US"
71 | ],
72 | "Host": [
73 | "0123456789.execute-api.us-east-1.amazonaws.com"
74 | ],
75 | "Upgrade-Insecure-Requests": [
76 | "1"
77 | ],
78 | "User-Agent": [
79 | "Custom User Agent String"
80 | ],
81 | "Via": [
82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
83 | ],
84 | "X-Amz-Cf-Id": [
85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
86 | ],
87 | "X-Forwarded-For": [
88 | "127.0.0.1, 127.0.0.2"
89 | ],
90 | "X-Forwarded-Port": [
91 | "443"
92 | ],
93 | "X-Forwarded-Proto": [
94 | "https"
95 | ]
96 | },
97 | "requestContext": {
98 | "accountId": "123456789012",
99 | "resourceId": "123456",
100 | "stage": "prod",
101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
102 | "requestTime": "09/Apr/2015:12:34:56 +0000",
103 | "requestTimeEpoch": 1428582896000,
104 | "identity": {
105 | "cognitoIdentityPoolId": null,
106 | "accountId": null,
107 | "cognitoIdentityId": null,
108 | "caller": null,
109 | "accessKey": null,
110 | "sourceIp": "127.0.0.1",
111 | "cognitoAuthenticationType": null,
112 | "cognitoAuthenticationProvider": null,
113 | "userArn": null,
114 | "userAgent": "Custom User Agent String",
115 | "user": null
116 | },
117 | "path": "/prod/1",
118 | "resourcePath": "/{id}",
119 | "httpMethod": "GET",
120 | "apiId": "1234567890",
121 | "protocol": "HTTP/1.1"
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/functions/delete-product/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/aws-samples/serverless-go-demo/domain"
8 | "github.com/aws-samples/serverless-go-demo/handlers"
9 | "github.com/aws-samples/serverless-go-demo/store"
10 |
11 | "github.com/aws/aws-lambda-go/lambda"
12 | )
13 |
14 | func main() {
15 | tableName, ok := os.LookupEnv("TABLE")
16 | if !ok {
17 | panic("Need TABLE environment variable")
18 | }
19 |
20 | dynamodb := store.NewDynamoDBStore(context.TODO(), tableName)
21 | domain := domain.NewProductsDomain(dynamodb)
22 | handler := handlers.NewAPIGatewayV2Handler(domain)
23 | lambda.Start(handler.DeleteHandler)
24 | }
25 |
--------------------------------------------------------------------------------
/functions/get-product/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "eyJ0ZXN0IjoiYm9keSJ9",
3 | "resource": "/{id}",
4 | "path": "/1",
5 | "httpMethod": "GET",
6 | "isBase64Encoded": true,
7 | "queryStringParameters": {
8 | "foo": "bar"
9 | },
10 | "multiValueQueryStringParameters": {
11 | "foo": [
12 | "bar"
13 | ]
14 | },
15 | "pathParameters": {
16 | "id": "1"
17 | },
18 | "stageVariables": {
19 | "baz": "qux"
20 | },
21 | "headers": {
22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
23 | "Accept-Encoding": "gzip, deflate, sdch",
24 | "Accept-Language": "en-US,en;q=0.8",
25 | "Cache-Control": "max-age=0",
26 | "CloudFront-Forwarded-Proto": "https",
27 | "CloudFront-Is-Desktop-Viewer": "true",
28 | "CloudFront-Is-Mobile-Viewer": "false",
29 | "CloudFront-Is-SmartTV-Viewer": "false",
30 | "CloudFront-Is-Tablet-Viewer": "false",
31 | "CloudFront-Viewer-Country": "US",
32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
33 | "Upgrade-Insecure-Requests": "1",
34 | "User-Agent": "Custom User Agent String",
35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
38 | "X-Forwarded-Port": "443",
39 | "X-Forwarded-Proto": "https"
40 | },
41 | "multiValueHeaders": {
42 | "Accept": [
43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
44 | ],
45 | "Accept-Encoding": [
46 | "gzip, deflate, sdch"
47 | ],
48 | "Accept-Language": [
49 | "en-US,en;q=0.8"
50 | ],
51 | "Cache-Control": [
52 | "max-age=0"
53 | ],
54 | "CloudFront-Forwarded-Proto": [
55 | "https"
56 | ],
57 | "CloudFront-Is-Desktop-Viewer": [
58 | "true"
59 | ],
60 | "CloudFront-Is-Mobile-Viewer": [
61 | "false"
62 | ],
63 | "CloudFront-Is-SmartTV-Viewer": [
64 | "false"
65 | ],
66 | "CloudFront-Is-Tablet-Viewer": [
67 | "false"
68 | ],
69 | "CloudFront-Viewer-Country": [
70 | "US"
71 | ],
72 | "Host": [
73 | "0123456789.execute-api.us-east-1.amazonaws.com"
74 | ],
75 | "Upgrade-Insecure-Requests": [
76 | "1"
77 | ],
78 | "User-Agent": [
79 | "Custom User Agent String"
80 | ],
81 | "Via": [
82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
83 | ],
84 | "X-Amz-Cf-Id": [
85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
86 | ],
87 | "X-Forwarded-For": [
88 | "127.0.0.1, 127.0.0.2"
89 | ],
90 | "X-Forwarded-Port": [
91 | "443"
92 | ],
93 | "X-Forwarded-Proto": [
94 | "https"
95 | ]
96 | },
97 | "requestContext": {
98 | "accountId": "123456789012",
99 | "resourceId": "123456",
100 | "stage": "prod",
101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
102 | "requestTime": "09/Apr/2015:12:34:56 +0000",
103 | "requestTimeEpoch": 1428582896000,
104 | "identity": {
105 | "cognitoIdentityPoolId": null,
106 | "accountId": null,
107 | "cognitoIdentityId": null,
108 | "caller": null,
109 | "accessKey": null,
110 | "sourceIp": "127.0.0.1",
111 | "cognitoAuthenticationType": null,
112 | "cognitoAuthenticationProvider": null,
113 | "userArn": null,
114 | "userAgent": "Custom User Agent String",
115 | "user": null
116 | },
117 | "path": "/prod/1",
118 | "resourcePath": "/{id}",
119 | "httpMethod": "GET",
120 | "apiId": "1234567890",
121 | "protocol": "HTTP/1.1"
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/functions/get-product/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/aws-samples/serverless-go-demo/domain"
8 | "github.com/aws-samples/serverless-go-demo/handlers"
9 | "github.com/aws-samples/serverless-go-demo/store"
10 |
11 | "github.com/aws/aws-lambda-go/lambda"
12 | )
13 |
14 | func main() {
15 | tableName, ok := os.LookupEnv("TABLE")
16 | if !ok {
17 | panic("Need TABLE environment variable")
18 | }
19 |
20 | dynamodb := store.NewDynamoDBStore(context.TODO(), tableName)
21 | domain := domain.NewProductsDomain(dynamodb)
22 | handler := handlers.NewAPIGatewayV2Handler(domain)
23 | lambda.Start(handler.GetHandler)
24 | }
25 |
--------------------------------------------------------------------------------
/functions/get-products/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/aws-samples/serverless-go-demo/domain"
8 | "github.com/aws-samples/serverless-go-demo/handlers"
9 | "github.com/aws-samples/serverless-go-demo/store"
10 |
11 | "github.com/aws/aws-lambda-go/lambda"
12 | )
13 |
14 | func main() {
15 | tableName, ok := os.LookupEnv("TABLE")
16 | if !ok {
17 | panic("Need TABLE environment variable")
18 | }
19 |
20 | dynamodb := store.NewDynamoDBStore(context.TODO(), tableName)
21 | domain := domain.NewProductsDomain(dynamodb)
22 | handler := handlers.NewAPIGatewayV2Handler(domain)
23 | lambda.Start(handler.AllHandler)
24 | }
25 |
--------------------------------------------------------------------------------
/functions/products-stream/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [
3 | {
4 | "eventID": "1",
5 | "eventVersion": "1.0",
6 | "dynamodb": {
7 | "Keys": {
8 | "Id": {
9 | "N": "101"
10 | }
11 | },
12 | "NewImage": {
13 | "Message": {
14 | "S": "New item!"
15 | },
16 | "Id": {
17 | "N": "101"
18 | }
19 | },
20 | "StreamViewType": "NEW_AND_OLD_IMAGES",
21 | "SequenceNumber": "111",
22 | "SizeBytes": 26
23 | },
24 | "awsRegion": "us-west-2",
25 | "eventName": "INSERT",
26 | "eventSourceARN": "eventsourceArn",
27 | "eventSource": "aws:dynamodb"
28 | },
29 | {
30 | "eventID": "2",
31 | "eventVersion": "1.0",
32 | "dynamodb": {
33 | "OldImage": {
34 | "Message": {
35 | "S": "New item!"
36 | },
37 | "Id": {
38 | "N": "101"
39 | }
40 | },
41 | "SequenceNumber": "222",
42 | "Keys": {
43 | "Id": {
44 | "N": "101"
45 | }
46 | },
47 | "SizeBytes": 59,
48 | "NewImage": {
49 | "Message": {
50 | "S": "This item has changed"
51 | },
52 | "Id": {
53 | "N": "101"
54 | }
55 | },
56 | "StreamViewType": "NEW_AND_OLD_IMAGES"
57 | },
58 | "awsRegion": "us-west-2",
59 | "eventName": "MODIFY",
60 | "eventSourceARN": "sourceArn",
61 | "eventSource": "aws:dynamodb"
62 | }
63 | ]
64 | }
--------------------------------------------------------------------------------
/functions/products-stream/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/aws/aws-lambda-go/lambda"
8 |
9 | "github.com/aws-samples/serverless-go-demo/bus"
10 | "github.com/aws-samples/serverless-go-demo/domain"
11 | "github.com/aws-samples/serverless-go-demo/handlers"
12 | )
13 |
14 | func main() {
15 | eventBusName, ok := os.LookupEnv("EVENT_BUS_NAME")
16 | if !ok {
17 | panic("Need EVENT_BUS_NAME environment variable")
18 | }
19 |
20 | store := bus.NewEventBridgeBus(context.TODO(), eventBusName)
21 | domain := domain.NewProductsStream(store)
22 | handler := handlers.NewDynamoDBEventHandler(domain)
23 | lambda.Start(handler.StreamHandler)
24 | }
25 |
--------------------------------------------------------------------------------
/functions/put-product/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "{\"id\":\"1\", \"name\":\"product\", \"price\": 0.123}",
3 | "resource": "/{id}",
4 | "path": "/1",
5 | "httpMethod": "PUT",
6 | "isBase64Encoded": true,
7 | "queryStringParameters": {
8 | "foo": "bar"
9 | },
10 | "multiValueQueryStringParameters": {
11 | "foo": [
12 | "bar"
13 | ]
14 | },
15 | "pathParameters": {
16 | "id": "1"
17 | },
18 | "stageVariables": {
19 | "baz": "qux"
20 | },
21 | "headers": {
22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
23 | "Accept-Encoding": "gzip, deflate, sdch",
24 | "Accept-Language": "en-US,en;q=0.8",
25 | "Cache-Control": "max-age=0",
26 | "CloudFront-Forwarded-Proto": "https",
27 | "CloudFront-Is-Desktop-Viewer": "true",
28 | "CloudFront-Is-Mobile-Viewer": "false",
29 | "CloudFront-Is-SmartTV-Viewer": "false",
30 | "CloudFront-Is-Tablet-Viewer": "false",
31 | "CloudFront-Viewer-Country": "US",
32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
33 | "Upgrade-Insecure-Requests": "1",
34 | "User-Agent": "Custom User Agent String",
35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
38 | "X-Forwarded-Port": "443",
39 | "X-Forwarded-Proto": "https"
40 | },
41 | "multiValueHeaders": {
42 | "Accept": [
43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
44 | ],
45 | "Accept-Encoding": [
46 | "gzip, deflate, sdch"
47 | ],
48 | "Accept-Language": [
49 | "en-US,en;q=0.8"
50 | ],
51 | "Cache-Control": [
52 | "max-age=0"
53 | ],
54 | "CloudFront-Forwarded-Proto": [
55 | "https"
56 | ],
57 | "CloudFront-Is-Desktop-Viewer": [
58 | "true"
59 | ],
60 | "CloudFront-Is-Mobile-Viewer": [
61 | "false"
62 | ],
63 | "CloudFront-Is-SmartTV-Viewer": [
64 | "false"
65 | ],
66 | "CloudFront-Is-Tablet-Viewer": [
67 | "false"
68 | ],
69 | "CloudFront-Viewer-Country": [
70 | "US"
71 | ],
72 | "Host": [
73 | "0123456789.execute-api.us-east-1.amazonaws.com"
74 | ],
75 | "Upgrade-Insecure-Requests": [
76 | "1"
77 | ],
78 | "User-Agent": [
79 | "Custom User Agent String"
80 | ],
81 | "Via": [
82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
83 | ],
84 | "X-Amz-Cf-Id": [
85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
86 | ],
87 | "X-Forwarded-For": [
88 | "127.0.0.1, 127.0.0.2"
89 | ],
90 | "X-Forwarded-Port": [
91 | "443"
92 | ],
93 | "X-Forwarded-Proto": [
94 | "https"
95 | ]
96 | },
97 | "requestContext": {
98 | "accountId": "123456789012",
99 | "resourceId": "123456",
100 | "stage": "prod",
101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
102 | "requestTime": "09/Apr/2015:12:34:56 +0000",
103 | "requestTimeEpoch": 1428582896000,
104 | "identity": {
105 | "cognitoIdentityPoolId": null,
106 | "accountId": null,
107 | "cognitoIdentityId": null,
108 | "caller": null,
109 | "accessKey": null,
110 | "sourceIp": "127.0.0.1",
111 | "cognitoAuthenticationType": null,
112 | "cognitoAuthenticationProvider": null,
113 | "userArn": null,
114 | "userAgent": "Custom User Agent String",
115 | "user": null
116 | },
117 | "path": "/prod/1",
118 | "resourcePath": "/{id}",
119 | "httpMethod": "GET",
120 | "apiId": "1234567890",
121 | "protocol": "HTTP/1.1"
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/functions/put-product/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/aws-samples/serverless-go-demo/domain"
8 | "github.com/aws-samples/serverless-go-demo/handlers"
9 | "github.com/aws-samples/serverless-go-demo/store"
10 |
11 | "github.com/aws/aws-lambda-go/lambda"
12 | )
13 |
14 | func main() {
15 | tableName, ok := os.LookupEnv("TABLE")
16 | if !ok {
17 | panic("Need TABLE environment variable")
18 | }
19 |
20 | dynamodb := store.NewDynamoDBStore(context.TODO(), tableName)
21 | domain := domain.NewProductsDomain(dynamodb)
22 | handler := handlers.NewAPIGatewayV2Handler(domain)
23 | lambda.Start(handler.PutHandler)
24 | }
25 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/aws-samples/serverless-go-demo
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/aws/aws-lambda-go v1.27.0
7 | github.com/aws/aws-sdk-go-v2 v1.11.2
8 | github.com/aws/aws-sdk-go-v2/config v1.11.0
9 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.4.4
10 | github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.9.2
11 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.10.0
12 | github.com/golang/mock v1.6.0
13 | )
14 |
15 | require (
16 | github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect
17 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
18 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirect
19 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirect
20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
21 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.8.1 // indirect
22 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirect
23 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.3.3 // indirect
24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
25 | github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
26 | github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
27 | github.com/aws/smithy-go v1.9.0 // indirect
28 | github.com/jmespath/go-jmespath v0.4.0 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2 | github.com/aws/aws-lambda-go v1.27.0 h1:aLzrJwdyHoF1A18YeVdJjX8Ixkd+bpogdxVInvHcWjM=
3 | github.com/aws/aws-lambda-go v1.27.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU=
4 | github.com/aws/aws-sdk-go-v2 v1.11.2 h1:SDiCYqxdIYi6HgQfAWRhgdZrdnOuGyLDJVRSWLeHWvs=
5 | github.com/aws/aws-sdk-go-v2 v1.11.2/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
6 | github.com/aws/aws-sdk-go-v2/config v1.11.0 h1:Czlld5zBB61A3/aoegA9/buZulwL9mHHfizh/Oq+Kqs=
7 | github.com/aws/aws-sdk-go-v2/config v1.11.0/go.mod h1:VrQDJGFBM5yZe+IOeenNZ/DWoErdny+k2MHEIpwDsEY=
8 | github.com/aws/aws-sdk-go-v2/credentials v1.6.4 h1:2hvbUoHufns0lDIsaK8FVCMukT1WngtZPavN+W2FkSw=
9 | github.com/aws/aws-sdk-go-v2/credentials v1.6.4/go.mod h1:tTrhvBPHyPde4pdIPSba4Nv7RYr4wP9jxXEDa1bKn/8=
10 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.4.4 h1:9WteVf5jmManG9HlxTFsk1+MT1IZ8S/8rvR+3A3OKng=
11 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.4.4/go.mod h1:MWyvQ5I9fEsoV+Im6IgpILXlAaypjlRqUkyS5GP5pIo=
12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 h1:KiN5TPOLrEjbGCvdTQR4t0U4T87vVwALZ5Bg3jpMqPY=
13 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2/go.mod h1:dF2F6tXEOgmW5X1ZFO/EPtWrcm7XkW07KNcJUGNtt4s=
14 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 h1:XJLnluKuUxQG255zPNe+04izXl7GSyUVafIsgfv9aw4=
15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2/go.mod h1:SgKKNBIoDC/E1ZCDhhMW3yalWjwuLjMcpLzsM/QQnWo=
16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 h1:EauRoYZVNPlidZSZJDscjJBQ22JhVF2+tdteatax2Ak=
17 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2/go.mod h1:xT4XX6w5Sa3dhg50JrYyy3e4WPYo/+WjY/BXtqXVunU=
18 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 h1:IQup8Q6lorXeiA/rK72PeToWoWK8h7VAPgHNWdSrtgE=
19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2/go.mod h1:VITe/MdW6EMXPb0o0txu/fsonXbMHUU2OC2Qp7ivU4o=
20 | github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.9.2 h1:3KMmqYzUxNPLuOYo6+n6BbqmIV3T5RfeK2NOpgTtkFU=
21 | github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.9.2/go.mod h1:FDL3tQ4lF3/k1xsp4WVNmXgWS/o/v/1uwn+lrAOgolE=
22 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.10.0 h1:jzvWaPf99rIjqEBxh9uGKxtnIykU/SOXY/nfvThhJvI=
23 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.10.0/go.mod h1:ELltfl9ri0n4sZ/VjPZBgemNMd9mYIpCAuZhc7NP7l4=
24 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.8.1 h1:AQurjazY9KPUxvq4EBN9Q3iWGaDrcqfpfSWtkP0Qy+g=
25 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.8.1/go.mod h1:RiesWyLiePOOwyT5ySDupQosvbG+OTMv9pws/EhDu4U=
26 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 h1:lPLbw4Gn59uoKqvOfSnkJr54XWk5Ak1NK20ZEiSWb3U=
27 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0/go.mod h1:80NaCIH9YU3rzTTs/J/ECATjXuRqzo/wB6ukO6MZ0XY=
28 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.3.3 h1:ru9+IpkVIuDvIkm9Q0DEjtWHnh6ITDoZo8fH2dIjlqQ=
29 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.3.3/go.mod h1:zOyLMYyg60yyZpOCniAUuibWVqTU4TuLmMa/Wh4P+HA=
30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 h1:CKdUNKmuilw/KNmO2Q53Av8u+ZyXMC2M9aX8Z+c/gzg=
31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2/go.mod h1:FgR1tCsn8C6+Hf+N5qkfrE4IXvUL1RgW87sunJ+5J4I=
32 | github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 h1:2IDmvSb86KT44lSg1uU4ONpzgWLOuApRl6Tg54mZ6Dk=
33 | github.com/aws/aws-sdk-go-v2/service/sso v1.6.2/go.mod h1:KnIpszaIdwI33tmc/W/GGXyn22c1USYxA/2KyvoeDY0=
34 | github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 h1:QKR7wy5e650q70PFKMfGF9sTo0rZgUevSSJ4wxmyWXk=
35 | github.com/aws/aws-sdk-go-v2/service/sts v1.11.1/go.mod h1:UV2N5HaPfdbDpkgkz4sRzWCvQswZjdO1FfqCWl0t7RA=
36 | github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58=
37 | github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
38 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
39 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
43 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
44 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
45 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
46 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
47 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
48 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
49 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
50 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
51 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
54 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
55 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
57 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
58 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
59 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
60 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
61 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
62 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
63 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
66 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
67 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
68 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
69 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
70 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
71 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
72 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
73 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
74 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
75 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
76 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
78 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
79 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
80 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
81 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
83 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
84 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
86 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
87 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
88 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
89 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
90 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
91 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
92 |
--------------------------------------------------------------------------------
/handlers/apigateway.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "net/http"
8 | "strings"
9 |
10 | "github.com/aws-samples/serverless-go-demo/domain"
11 |
12 | "github.com/aws/aws-lambda-go/events"
13 | )
14 |
15 | type APIGatewayV2Handler struct {
16 | products *domain.Products
17 | }
18 |
19 | func NewAPIGatewayV2Handler(d *domain.Products) *APIGatewayV2Handler {
20 | return &APIGatewayV2Handler{
21 | products: d,
22 | }
23 | }
24 |
25 | func (l *APIGatewayV2Handler) AllHandler(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
26 | next := event.QueryStringParameters["next"]
27 |
28 | productRange, err := l.products.AllProducts(ctx, &next)
29 | if err != nil {
30 | return errResponse(http.StatusInternalServerError, err.Error()), nil
31 | }
32 |
33 | return response(http.StatusOK, productRange), nil
34 | }
35 |
36 | func (l *APIGatewayV2Handler) GetHandler(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
37 | id, ok := event.PathParameters["id"]
38 | if !ok {
39 | return errResponse(http.StatusBadRequest, "missing 'id' parameter in path"), nil
40 | }
41 |
42 | product, err := l.products.GetProduct(ctx, id)
43 |
44 | if err != nil {
45 | return errResponse(http.StatusInternalServerError, err.Error()), nil
46 | }
47 | if product == nil {
48 | return errResponse(http.StatusNotFound, "product not found"), nil
49 | } else {
50 | return response(http.StatusOK, product), nil
51 | }
52 | }
53 |
54 | func (l *APIGatewayV2Handler) PutHandler(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
55 | id, ok := event.PathParameters["id"]
56 | if !ok {
57 | return errResponse(http.StatusBadRequest, "missing 'id' parameter in path"), nil
58 | }
59 |
60 | if strings.TrimSpace(event.Body) == "" {
61 | return errResponse(http.StatusBadRequest, "empty request body"), nil
62 | }
63 |
64 | product, err := l.products.PutProduct(ctx, id, []byte(event.Body))
65 | if err != nil {
66 | if errors.Is(err, domain.ErrJsonUnmarshal) || errors.Is(err, domain.ErrProductIdMismatch) {
67 | return errResponse(http.StatusBadRequest, err.Error()), nil
68 | } else {
69 | return errResponse(http.StatusInternalServerError, err.Error()), nil
70 | }
71 | }
72 |
73 | return response(http.StatusCreated, product), nil
74 | }
75 |
76 | func (l *APIGatewayV2Handler) DeleteHandler(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
77 | id, ok := event.PathParameters["id"]
78 | if !ok {
79 | return errResponse(http.StatusBadRequest, "missing 'id' parameter in path"), nil
80 | }
81 |
82 | err := l.products.DeleteProduct(ctx, id)
83 | if err != nil {
84 | return errResponse(http.StatusInternalServerError, err.Error()), nil
85 | }
86 |
87 | return response(http.StatusOK, nil), nil
88 | }
89 |
90 | func response(code int, object interface{}) events.APIGatewayV2HTTPResponse {
91 | marshalled, err := json.Marshal(object)
92 | if err != nil {
93 | return errResponse(http.StatusInternalServerError, err.Error())
94 | }
95 |
96 | return events.APIGatewayV2HTTPResponse{
97 | StatusCode: code,
98 | Headers: map[string]string{
99 | "Content-Type": "application/json",
100 | },
101 | Body: string(marshalled),
102 | IsBase64Encoded: false,
103 | }
104 | }
105 |
106 | func errResponse(status int, body string) events.APIGatewayV2HTTPResponse {
107 | message := map[string]string{
108 | "message": body,
109 | }
110 |
111 | messageBytes, _ := json.Marshal(&message)
112 |
113 | return events.APIGatewayV2HTTPResponse{
114 | StatusCode: status,
115 | Headers: map[string]string{
116 | "Content-Type": "application/json",
117 | },
118 | Body: string(messageBytes),
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/handlers/dynamodb.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "log"
7 |
8 | "github.com/aws-samples/serverless-go-demo/domain"
9 | "github.com/aws-samples/serverless-go-demo/types"
10 | "github.com/aws/aws-lambda-go/events"
11 | )
12 |
13 | type DynamoDBEventHandler struct {
14 | productStream *domain.ProductsStream
15 | }
16 |
17 | // Can be deleted when this is merged: https://github.com/aws/aws-lambda-go/pull/410/files
18 |
19 | type StreamsEventResponse struct {
20 | BatchItemFailures []BatchItemFailure `json:"batchItemFailures"`
21 | }
22 |
23 | type BatchItemFailure struct {
24 | ItemIdentifier string `json:"itemIdentifier"`
25 | }
26 |
27 | func NewDynamoDBEventHandler(p *domain.ProductsStream) *DynamoDBEventHandler {
28 | return &DynamoDBEventHandler{
29 | productStream: p,
30 | }
31 | }
32 |
33 | func (d *DynamoDBEventHandler) StreamHandler(ctx context.Context, event events.DynamoDBEvent) (StreamsEventResponse, error) {
34 | internalEvents := make([]types.Event, len(event.Records))
35 | for i, ddbEvent := range event.Records {
36 | internalEvents[i] = eventFromDynamoDBRecord(ddbEvent)
37 | }
38 |
39 | failedEvents, err := d.productStream.Publish(ctx, internalEvents)
40 | if err != nil {
41 | log.Fatalf("totally failed to publish: %v", err)
42 | return StreamsEventResponse{}, err
43 | }
44 |
45 | if len(failedEvents) > 0 {
46 | itemFailures := make([]BatchItemFailure, len(failedEvents))
47 | for i, failedItem := range failedEvents {
48 | itemFailures[i] = BatchItemFailure{ItemIdentifier: failedItem.Resources[0]}
49 | }
50 |
51 | return StreamsEventResponse{BatchItemFailures: itemFailures}, nil
52 | }
53 |
54 | return StreamsEventResponse{}, nil
55 | }
56 |
57 | func eventFromDynamoDBRecord(record events.DynamoDBEventRecord) types.Event {
58 | change, err := json.Marshal(record.Change)
59 | if err != nil {
60 | log.Fatalf("cannot unmarshal dynamodb record change: %s", err)
61 | }
62 |
63 | detailType := ""
64 | switch record.EventName {
65 | case string(events.DynamoDBOperationTypeInsert):
66 | detailType = "ProductCreated"
67 | case string(events.DynamoDBOperationTypeModify):
68 | detailType = "ProductUpdated"
69 | case string(events.DynamoDBOperationTypeRemove):
70 | detailType = "ProductDelected"
71 | }
72 |
73 | return types.Event{
74 | Source: "serverless-go-demo",
75 | Detail: string(change),
76 | DetailType: detailType,
77 | Resources: []string{record.EventID},
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/imgs/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/serverless-go-demo/a07a55b1ac373c5293c0238dc771169bdd6e8e81/imgs/diagram.png
--------------------------------------------------------------------------------
/imgs/load-test.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/serverless-go-demo/a07a55b1ac373c5293c0238dc771169bdd6e8e81/imgs/load-test.jpeg
--------------------------------------------------------------------------------
/integration-testing/integration_test.go:
--------------------------------------------------------------------------------
1 | //go:build integration
2 | // +build integration
3 |
4 | package main
5 |
6 | import (
7 | "bytes"
8 | "encoding/json"
9 | "fmt"
10 | "io"
11 | "log"
12 | "math/rand"
13 | "net/http"
14 | "os"
15 | "strings"
16 | "testing"
17 | "time"
18 |
19 | "github.com/aws-samples/serverless-go-demo/types"
20 | )
21 |
22 | func randomString(length int) string {
23 | rand.Seed(time.Now().UnixNano())
24 | b := make([]byte, length)
25 | rand.Read(b)
26 | return fmt.Sprintf("%x", b)[:length]
27 | }
28 |
29 | func getRandomProduct() types.Product {
30 | return types.Product{
31 | Id: randomString(3),
32 | Name: randomString(10),
33 | Price: rand.Float64(),
34 | }
35 | }
36 |
37 | var apiUrl string
38 |
39 | func init() {
40 | _apiUrl, ok := os.LookupEnv("API_URL")
41 | if !ok {
42 | panic("Can't find API_URL environment variable")
43 | }
44 |
45 | apiUrl = _apiUrl
46 | }
47 |
48 | func TestFlow(t *testing.T) {
49 | client := &http.Client{}
50 | product := getRandomProduct()
51 |
52 | // Put new product
53 | log.Println("PUT new product")
54 | payload, err := json.Marshal(product)
55 | if err != nil {
56 | panic(err)
57 | }
58 |
59 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", apiUrl, product.Id), bytes.NewBuffer(payload))
60 | if err != nil {
61 | panic(err)
62 | }
63 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
64 | resp, err := client.Do(req)
65 | if err != nil {
66 | panic(err)
67 | }
68 | defer resp.Body.Close()
69 |
70 | if resp.StatusCode != http.StatusCreated {
71 | t.Fatalf("Failed to create product. Got response code %d", resp.StatusCode)
72 | }
73 |
74 | // Get product
75 | log.Println("GET product")
76 | req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", apiUrl, product.Id), nil)
77 | if err != nil {
78 | panic(err)
79 | }
80 | resp, err = client.Do(req)
81 | if err != nil {
82 | panic(err)
83 | }
84 | defer resp.Body.Close()
85 |
86 | if resp.StatusCode != http.StatusOK {
87 | t.Fatalf("Failed to get product. Got response code %d", resp.StatusCode)
88 | }
89 |
90 | apiProduct := types.Product{}
91 | body, _ := io.ReadAll(resp.Body)
92 | json.Unmarshal(body, &apiProduct)
93 |
94 | if apiProduct.Id != product.Id {
95 | t.Fatalf("API product ID is different from our own")
96 | }
97 |
98 | if apiProduct.Name != product.Name {
99 | t.Fatalf("API product Name is different from our own")
100 | }
101 |
102 | if apiProduct.Price != product.Price {
103 | t.Fatalf("API product Price is different from our own")
104 | }
105 |
106 | // Get all products
107 | log.Println("GET all products")
108 |
109 | req, err = http.NewRequest(http.MethodGet, apiUrl, nil)
110 | if err != nil {
111 | panic(err)
112 | }
113 | resp, err = client.Do(req)
114 | if err != nil {
115 | panic(err)
116 | }
117 | defer resp.Body.Close()
118 |
119 | if resp.StatusCode != http.StatusOK {
120 | t.Fatalf("Failed to get all products. Got response code %d", resp.StatusCode)
121 | }
122 |
123 | products := types.ProductRange{}
124 | body, _ = io.ReadAll(resp.Body)
125 | json.Unmarshal(body, &products)
126 |
127 | if len(products.Products) < 1 {
128 | t.Fatalf("Failed to get all products. Only got %d", len(products.Products))
129 | }
130 |
131 | // Delete product
132 | log.Println("DELETE product")
133 |
134 | req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s", apiUrl, product.Id), nil)
135 | if err != nil {
136 | panic(err)
137 | }
138 | resp, err = client.Do(req)
139 | if err != nil {
140 | panic(err)
141 | }
142 | defer resp.Body.Close()
143 |
144 | if resp.StatusCode != http.StatusOK {
145 | t.Fatalf("Failed to delete product. Got response code %d", resp.StatusCode)
146 | }
147 |
148 | // Get product again
149 | log.Println("GET product again")
150 | req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", apiUrl, product.Id), nil)
151 | if err != nil {
152 | panic(err)
153 | }
154 | resp, err = client.Do(req)
155 | if err != nil {
156 | panic(err)
157 | }
158 | defer resp.Body.Close()
159 |
160 | if resp.StatusCode != http.StatusNotFound {
161 | t.Fatal("Got unexpected product")
162 | }
163 | }
164 |
165 | func TestPutProductWithInvalidId(t *testing.T) {
166 | client := &http.Client{}
167 |
168 | product := getRandomProduct()
169 | product.Id = "invalid id"
170 |
171 | log.Println("PUT new product")
172 | payload, err := json.Marshal(product)
173 | if err != nil {
174 | panic(err)
175 | }
176 |
177 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", apiUrl, "not-the-same-id"), bytes.NewBuffer(payload))
178 | if err != nil {
179 | panic(err)
180 | }
181 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
182 | resp, err := client.Do(req)
183 | if err != nil {
184 | panic(err)
185 | }
186 | defer resp.Body.Close()
187 |
188 | if resp.StatusCode != http.StatusBadRequest {
189 | t.Fatalf("Should have not created product. Got response code %d", resp.StatusCode)
190 | }
191 |
192 | body, _ := io.ReadAll(resp.Body)
193 | if !strings.Contains(string(body), "product ID in path does not match product ID in body") {
194 | t.Fatalf("Wrong body content: %s", string(body))
195 | }
196 | }
197 |
198 | func TestProductEmpty(t *testing.T) {
199 | client := &http.Client{}
200 |
201 | log.Println("PUT new product")
202 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", apiUrl, "empty-id"), nil)
203 | if err != nil {
204 | panic(err)
205 | }
206 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
207 | resp, err := client.Do(req)
208 | if err != nil {
209 | panic(err)
210 | }
211 | defer resp.Body.Close()
212 |
213 | if resp.StatusCode != http.StatusBadRequest {
214 | t.Fatalf("Should have not created product. Got response code %d", resp.StatusCode)
215 | }
216 |
217 | body, _ := io.ReadAll(resp.Body)
218 | if !strings.Contains(string(body), "empty request body") {
219 | t.Fatalf("Wrong body content: %s", string(body))
220 | }
221 | }
222 |
223 | func TestPutProductInvalidBody(t *testing.T) {
224 | client := &http.Client{}
225 |
226 | product := getRandomProduct()
227 |
228 | log.Println("PUT new product")
229 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", apiUrl, product.Id), bytes.NewReader([]byte("invalid body")))
230 | if err != nil {
231 | panic(err)
232 | }
233 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
234 | resp, err := client.Do(req)
235 | if err != nil {
236 | panic(err)
237 | }
238 | defer resp.Body.Close()
239 |
240 | if resp.StatusCode != http.StatusBadRequest {
241 | t.Fatalf("Should have not created product. Got response code %d", resp.StatusCode)
242 | }
243 |
244 | body, _ := io.ReadAll(resp.Body)
245 | if !strings.Contains(string(body), "failed to parse product from request body") {
246 | t.Fatalf("Wrong body content: %s", string(body))
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/load-testing/generator.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 |
3 | const COLORS = [
4 | "Red", "Green", "Blue", "Yellow", "Orange", "Purple", "Pink", "Brown",
5 | "Black", "White", "Gray", "Silver", "Gold", "Cyan", "Magenta", "Maroon",
6 | "Navy", "Olive", "Teal", "Aqua", "Lime", "Coral", "Aquamarine",
7 | "Turquoise", "Violet", "Indigo", "Plum", "Crimson", "Salmon", "Coral",
8 | "Khaki", "Beige",
9 | ];
10 |
11 | const PRODUCTS = [
12 | "Shoes", "Sweatshirts", "Hats", "Pants", "Shirts", "T-Shirts", "Trousers",
13 | "Jackets", "Shorts", "Skirts", "Dresses", "Coats", "Jeans", "Blazers",
14 | "Socks", "Gloves", "Belts", "Bags", "Shoes", "Sunglasses", "Watches",
15 | "Jewelry", "Ties", "Hair Accessories", "Makeup", "Accessories",
16 | ];
17 |
18 | module.exports = {
19 | generateProduct: function(context, events, done) {
20 | const color = COLORS[Math.floor(Math.random() * COLORS.length)];
21 | const name = PRODUCTS[Math.floor(Math.random() * PRODUCTS.length)];
22 |
23 | context.vars.id = crypto.randomUUID();
24 | context.vars.name = `${color} ${name}`;
25 | context.vars.price = Math.round(Math.random() * 10000) / 100;
26 |
27 | return done();
28 | },
29 | };
--------------------------------------------------------------------------------
/load-testing/load-test.yml:
--------------------------------------------------------------------------------
1 | config:
2 | target: "{{ $processEnvironment.API_URL }}"
3 | processor: generator.js
4 | phases:
5 | - duration: 600
6 | arrivalRate: 300
7 |
8 | scenarios:
9 | - name: "Generate products"
10 | weight: 8
11 | flow:
12 | - function: generateProduct
13 | - put:
14 | url: "/{{ id }}"
15 | headers:
16 | Content-Type: "application/json"
17 | json:
18 | id: "{{ id }}"
19 | name: "{{ name }}"
20 | price: "{{ price }}"
21 | - get:
22 | url: "/{{ id }}"
23 | - think: 3
24 | - delete:
25 | url: "/{{ id }}"
26 | - name: "Get products"
27 | weight: 2
28 | flow:
29 | - get:
30 | url: "/"
31 |
--------------------------------------------------------------------------------
/store/dynamodb.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/aws/aws-sdk-go-v2/aws"
9 | "github.com/aws/aws-sdk-go-v2/config"
10 | "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
11 | "github.com/aws/aws-sdk-go-v2/service/dynamodb"
12 | ddbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
13 |
14 | "github.com/aws-samples/serverless-go-demo/types"
15 | )
16 |
17 | type DynamoDBStore struct {
18 | client *dynamodb.Client
19 | tableName string
20 | }
21 |
22 | var _ types.Store = (*DynamoDBStore)(nil)
23 |
24 | func NewDynamoDBStore(ctx context.Context, tableName string) *DynamoDBStore {
25 | cfg, err := config.LoadDefaultConfig(ctx)
26 | if err != nil {
27 | log.Fatalf("unable to load SDK config, %v", err)
28 | }
29 |
30 | client := dynamodb.NewFromConfig(cfg)
31 |
32 | return &DynamoDBStore{
33 | client: client,
34 | tableName: tableName,
35 | }
36 | }
37 |
38 | func (d *DynamoDBStore) All(ctx context.Context, next *string) (types.ProductRange, error) {
39 | productRange := types.ProductRange{
40 | Products: []types.Product{},
41 | }
42 |
43 | input := &dynamodb.ScanInput{
44 | TableName: &d.tableName,
45 | Limit: aws.Int32(20),
46 | }
47 |
48 | if next != nil {
49 | input.ExclusiveStartKey = map[string]ddbtypes.AttributeValue{
50 | "id": &ddbtypes.AttributeValueMemberS{Value: *next},
51 | }
52 | }
53 |
54 | result, err := d.client.Scan(ctx, input)
55 |
56 | if err != nil {
57 | return productRange, fmt.Errorf("failed to get items from DynamoDB: %w", err)
58 | }
59 |
60 | err = attributevalue.UnmarshalListOfMaps(result.Items, &productRange.Products)
61 | if err != nil {
62 | return productRange, fmt.Errorf("failed to unmarshal data from DynamoDB: %w", err)
63 | }
64 |
65 | if len(result.LastEvaluatedKey) > 0 {
66 | if key, ok := result.LastEvaluatedKey["id"]; ok {
67 | nextKey := key.(*ddbtypes.AttributeValueMemberS).Value
68 | productRange.Next = &nextKey
69 | }
70 | }
71 |
72 | return productRange, nil
73 | }
74 |
75 | func (d *DynamoDBStore) Get(ctx context.Context, id string) (*types.Product, error) {
76 | response, err := d.client.GetItem(ctx, &dynamodb.GetItemInput{
77 | TableName: &d.tableName,
78 | Key: map[string]ddbtypes.AttributeValue{
79 | "id": &ddbtypes.AttributeValueMemberS{Value: id},
80 | },
81 | })
82 |
83 | if err != nil {
84 | return nil, fmt.Errorf("failed to get item from DynamoDB: %w", err)
85 | }
86 |
87 | if len(response.Item) == 0 {
88 | return nil, nil
89 | }
90 |
91 | product := types.Product{}
92 | err = attributevalue.UnmarshalMap(response.Item, &product)
93 |
94 | if err != nil {
95 | return nil, fmt.Errorf("error getting item %w", err)
96 | }
97 |
98 | return &product, nil
99 | }
100 |
101 | func (d *DynamoDBStore) Put(ctx context.Context, product types.Product) error {
102 | item, err := attributevalue.MarshalMap(&product)
103 | if err != nil {
104 | return fmt.Errorf("unable to marshal product: %w", err)
105 | }
106 |
107 | _, err = d.client.PutItem(ctx, &dynamodb.PutItemInput{
108 | TableName: &d.tableName,
109 | Item: item,
110 | })
111 |
112 | if err != nil {
113 | return fmt.Errorf("cannot put item: %w", err)
114 | }
115 |
116 | return nil
117 | }
118 |
119 | func (d *DynamoDBStore) Delete(ctx context.Context, id string) error {
120 | _, err := d.client.DeleteItem(ctx, &dynamodb.DeleteItemInput{
121 | TableName: &d.tableName,
122 | Key: map[string]ddbtypes.AttributeValue{
123 | "id": &ddbtypes.AttributeValueMemberS{Value: id},
124 | },
125 | })
126 |
127 | if err != nil {
128 | return fmt.Errorf("can't delete item: %w", err)
129 | }
130 |
131 | return nil
132 | }
133 |
--------------------------------------------------------------------------------
/store/memory.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/aws/aws-sdk-go-v2/aws"
7 |
8 | "github.com/aws-samples/serverless-go-demo/types"
9 | )
10 |
11 | type MemoryStore struct {
12 | storage map[string]types.Product
13 | }
14 |
15 | // Just to make sure MemoryStore implements the Store interface
16 | var _ types.Store = (*MemoryStore)(nil)
17 |
18 | func NewMemoryStore() *MemoryStore {
19 | return &MemoryStore{
20 | storage: make(map[string]types.Product),
21 | }
22 | }
23 |
24 | func (m *MemoryStore) All(ctx context.Context, next *string) (types.ProductRange, error) {
25 | productRange := types.ProductRange{
26 | Products: []types.Product{},
27 | Next: aws.String("random next string"),
28 | }
29 |
30 | for _, v := range m.storage {
31 | productRange.Products = append(productRange.Products, v)
32 | }
33 |
34 | return productRange, nil
35 | }
36 |
37 | func (m *MemoryStore) Get(ctx context.Context, id string) (*types.Product, error) {
38 | p, ok := m.storage[id]
39 | if !ok {
40 | return nil, nil
41 | }
42 |
43 | return &p, nil
44 | }
45 |
46 | func (m *MemoryStore) Put(ctx context.Context, p types.Product) error {
47 | m.storage[p.Id] = p
48 |
49 | return nil
50 | }
51 |
52 | func (m *MemoryStore) Delete(ctx context.Context, id string) error {
53 | delete(m.storage, id)
54 |
55 | return nil
56 | }
57 |
--------------------------------------------------------------------------------
/template.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: "2010-09-09"
2 | Transform: AWS::Serverless-2016-10-31
3 |
4 | Globals:
5 | Function:
6 | MemorySize: 128
7 | Architectures: ["arm64"]
8 | Handler: bootstrap
9 | Runtime: provided.al2
10 | Timeout: 5
11 | Tracing: Active
12 | Environment:
13 | Variables:
14 | TABLE: !Ref Table
15 |
16 | Resources:
17 | GetProductsFunction:
18 | Type: AWS::Serverless::Function
19 | Properties:
20 | CodeUri: functions/get-products/
21 | Events:
22 | Api:
23 | Type: HttpApi
24 | Properties:
25 | Path: /
26 | Method: GET
27 | Policies:
28 | - Version: "2012-10-17"
29 | Statement:
30 | - Effect: Allow
31 | Action: dynamodb:Scan
32 | Resource: !GetAtt Table.Arn
33 | Metadata:
34 | BuildMethod: makefile
35 |
36 | GetProductFunction:
37 | Type: AWS::Serverless::Function
38 | Properties:
39 | CodeUri: functions/get-product/
40 | Events:
41 | Api:
42 | Type: HttpApi
43 | Properties:
44 | Path: /{id}
45 | Method: GET
46 | Policies:
47 | - Version: "2012-10-17"
48 | Statement:
49 | - Effect: Allow
50 | Action: dynamodb:GetItem
51 | Resource: !GetAtt Table.Arn
52 | Metadata:
53 | BuildMethod: makefile
54 |
55 | DeleteProductFunction:
56 | Type: AWS::Serverless::Function
57 | Properties:
58 | CodeUri: functions/delete-product/
59 | Events:
60 | Api:
61 | Type: HttpApi
62 | Properties:
63 | Path: /{id}
64 | Method: DELETE
65 | Policies:
66 | - Version: "2012-10-17"
67 | Statement:
68 | - Effect: Allow
69 | Action: dynamodb:DeleteItem
70 | Resource: !GetAtt Table.Arn
71 | Metadata:
72 | BuildMethod: makefile
73 |
74 | PutProductFunction:
75 | Type: AWS::Serverless::Function
76 | Properties:
77 | CodeUri: functions/put-product/
78 | Events:
79 | Api:
80 | Type: HttpApi
81 | Properties:
82 | Path: /{id}
83 | Method: PUT
84 | Policies:
85 | - Version: "2012-10-17"
86 | Statement:
87 | - Effect: Allow
88 | Action: dynamodb:PutItem
89 | Resource: !GetAtt Table.Arn
90 | Metadata:
91 | BuildMethod: makefile
92 |
93 | DDBStreamsFunction:
94 | Type: AWS::Serverless::Function
95 | Properties:
96 | CodeUri: functions/products-stream/
97 | Timeout: 10
98 | Events:
99 | TableStream:
100 | Type: DynamoDB
101 | Properties:
102 | BatchSize: 1000
103 | FunctionResponseTypes:
104 | - ReportBatchItemFailures
105 | MaximumBatchingWindowInSeconds: 10
106 | StartingPosition: TRIM_HORIZON
107 | Stream: !GetAtt Table.StreamArn
108 | Environment:
109 | Variables:
110 | EVENT_BUS_NAME: !Ref EventBus
111 | MemorySize: 128
112 | Policies:
113 | - Version: "2012-10-17"
114 | Statement:
115 | - Effect: Allow
116 | Action: events:PutEvents
117 | Resource: !GetAtt EventBus.Arn
118 |
119 | Table:
120 | Type: AWS::DynamoDB::Table
121 | Properties:
122 | AttributeDefinitions:
123 | - AttributeName: id
124 | AttributeType: S
125 | BillingMode: PAY_PER_REQUEST
126 | KeySchema:
127 | - AttributeName: id
128 | KeyType: HASH
129 | StreamSpecification:
130 | StreamViewType: NEW_AND_OLD_IMAGES
131 |
132 | EventBus:
133 | Type: AWS::Events::EventBus
134 | Properties:
135 | Name: !Ref AWS::StackName
136 |
137 | Outputs:
138 | ApiUrl:
139 | Description: "API Gateway endpoint URL"
140 | Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/"
141 |
--------------------------------------------------------------------------------
/tools/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/aws-samples/serverless-go-demo/tools
2 |
3 | go 1.17
4 |
5 | require honnef.co/go/tools v0.2.2
6 |
7 | require (
8 | github.com/BurntSushi/toml v0.3.1 // indirect
9 | golang.org/x/mod v0.3.0 // indirect
10 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect
11 | golang.org/x/tools v0.1.0 // indirect
12 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/tools/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
5 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
6 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
7 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
8 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
9 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
11 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
12 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
13 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
15 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
16 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
17 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
18 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
19 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
20 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
21 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
22 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
23 | golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
24 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
25 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
26 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
27 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
28 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
29 | honnef.co/go/tools v0.2.2 h1:MNh1AVMyVX23VUHE2O27jm6lNj3vjO5DexS4A1xvnzk=
30 | honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
31 |
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | //go:build tools
2 | // +build tools
3 |
4 | package tools
5 |
6 | import (
7 | // Tools we use during development.
8 | _ "honnef.co/go/tools/cmd/staticcheck"
9 | )
10 |
--------------------------------------------------------------------------------
/types/bus.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "context"
4 |
5 | type Event struct {
6 | Source string
7 | Detail string
8 | DetailType string
9 | Resources []string
10 | }
11 |
12 | type FailedEvent struct {
13 | Event
14 | FailureCode string
15 | FailureMessage string
16 | }
17 |
18 | type Bus interface {
19 | Put(context.Context, []Event) ([]FailedEvent, error)
20 | }
21 |
--------------------------------------------------------------------------------
/types/mocks/mock_store.go:
--------------------------------------------------------------------------------
1 | // Code generated by MockGen. DO NOT EDIT.
2 | // Source: github.com/aws-samples/serverless-go-demo/types (interfaces: Store)
3 |
4 | // Package mocks is a generated GoMock package.
5 | package mocks
6 |
7 | import (
8 | context "context"
9 | reflect "reflect"
10 |
11 | types "github.com/aws-samples/serverless-go-demo/types"
12 | gomock "github.com/golang/mock/gomock"
13 | )
14 |
15 | // MockStore is a mock of Store interface.
16 | type MockStore struct {
17 | ctrl *gomock.Controller
18 | recorder *MockStoreMockRecorder
19 | }
20 |
21 | // MockStoreMockRecorder is the mock recorder for MockStore.
22 | type MockStoreMockRecorder struct {
23 | mock *MockStore
24 | }
25 |
26 | // NewMockStore creates a new mock instance.
27 | func NewMockStore(ctrl *gomock.Controller) *MockStore {
28 | mock := &MockStore{ctrl: ctrl}
29 | mock.recorder = &MockStoreMockRecorder{mock}
30 | return mock
31 | }
32 |
33 | // EXPECT returns an object that allows the caller to indicate expected use.
34 | func (m *MockStore) EXPECT() *MockStoreMockRecorder {
35 | return m.recorder
36 | }
37 |
38 | // All mocks base method.
39 | func (m *MockStore) All(arg0 context.Context, arg1 *string) (types.ProductRange, error) {
40 | m.ctrl.T.Helper()
41 | ret := m.ctrl.Call(m, "All", arg0, arg1)
42 | ret0, _ := ret[0].(types.ProductRange)
43 | ret1, _ := ret[1].(error)
44 | return ret0, ret1
45 | }
46 |
47 | // All indicates an expected call of All.
48 | func (mr *MockStoreMockRecorder) All(arg0, arg1 interface{}) *gomock.Call {
49 | mr.mock.ctrl.T.Helper()
50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockStore)(nil).All), arg0, arg1)
51 | }
52 |
53 | // Delete mocks base method.
54 | func (m *MockStore) Delete(arg0 context.Context, arg1 string) error {
55 | m.ctrl.T.Helper()
56 | ret := m.ctrl.Call(m, "Delete", arg0, arg1)
57 | ret0, _ := ret[0].(error)
58 | return ret0
59 | }
60 |
61 | // Delete indicates an expected call of Delete.
62 | func (mr *MockStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
63 | mr.mock.ctrl.T.Helper()
64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStore)(nil).Delete), arg0, arg1)
65 | }
66 |
67 | // Get mocks base method.
68 | func (m *MockStore) Get(arg0 context.Context, arg1 string) (*types.Product, error) {
69 | m.ctrl.T.Helper()
70 | ret := m.ctrl.Call(m, "Get", arg0, arg1)
71 | ret0, _ := ret[0].(*types.Product)
72 | ret1, _ := ret[1].(error)
73 | return ret0, ret1
74 | }
75 |
76 | // Get indicates an expected call of Get.
77 | func (mr *MockStoreMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
78 | mr.mock.ctrl.T.Helper()
79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStore)(nil).Get), arg0, arg1)
80 | }
81 |
82 | // Put mocks base method.
83 | func (m *MockStore) Put(arg0 context.Context, arg1 types.Product) error {
84 | m.ctrl.T.Helper()
85 | ret := m.ctrl.Call(m, "Put", arg0, arg1)
86 | ret0, _ := ret[0].(error)
87 | return ret0
88 | }
89 |
90 | // Put indicates an expected call of Put.
91 | func (mr *MockStoreMockRecorder) Put(arg0, arg1 interface{}) *gomock.Call {
92 | mr.mock.ctrl.T.Helper()
93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockStore)(nil).Put), arg0, arg1)
94 | }
95 |
--------------------------------------------------------------------------------
/types/product.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type Product struct {
4 | Id string `dynamodbav:"id" json:"id"`
5 | Name string `dynamodbav:"name" json:"name"`
6 | Price float64 `dynamodbav:"price" json:"price"`
7 | }
8 |
9 | type ProductRange struct {
10 | Products []Product `json:"products"`
11 | Next *string `json:"next,omitempty"`
12 | }
13 |
--------------------------------------------------------------------------------
/types/store.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | //go:generate mockgen -destination=./mocks/mock_store.go -package=mocks github.com/aws-samples/serverless-go-demo/types Store
4 |
5 | import (
6 | "context"
7 | )
8 |
9 | type Store interface {
10 | All(context.Context, *string) (ProductRange, error)
11 | Get(context.Context, string) (*Product, error)
12 | Put(context.Context, Product) error
13 | Delete(context.Context, string) error
14 | }
15 |
--------------------------------------------------------------------------------