├── .gitignore
├── Makefile
├── release.sh
├── examples
├── template.yaml
└── example.go
├── CHANGELOG.md
├── LICENSE
├── .github
└── workflows
│ └── test.yml
├── go.mod
├── lambdamux.go
├── README.md
├── lambdamux_test.go
├── internal
└── radix
│ ├── radix.go
│ └── radix_test.go
├── go.sum
└── lambdamux_benchmark_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | lambdamux
2 | examples/bootstrap
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build test benchmark
2 |
3 | BINARY_NAME=lambdamux
4 |
5 | build:
6 | go build -v -o $(BINARY_NAME) .
7 |
8 | test:
9 | go test -v ./...
10 |
11 | benchmark:
12 | go test -bench=. -benchmem ./...
13 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Ensure we're on the main branch
4 | git checkout main
5 | git pull origin main
6 |
7 | # Get the latest tag
8 | LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
9 | echo "Latest tag: $LATEST_TAG"
10 |
11 | # Prompt for new version
12 | read -p "Enter new version number (e.g., 1.0.1): " VERSION
13 |
14 | # Create and push new tag
15 | git tag -a v$VERSION -m "Release version $VERSION"
16 | git push origin v$VERSION
17 |
18 | # Build the project
19 | GOOS=linux GOARCH=amd64 go build -v -o lambdamux .
20 |
21 | # Create GitHub release with only the binary
22 | gh release create v$VERSION ./lambdamux -t "v$VERSION" -n "Release notes for version $VERSION"
23 |
24 | echo "Release v$VERSION created and published!"
25 |
--------------------------------------------------------------------------------
/examples/template.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Transform: AWS::Serverless-2016-10-31
3 | Description: Example Lambda function using lambdamux router
4 |
5 | Globals:
6 | Function:
7 | Timeout: 5
8 | MemorySize: 128
9 |
10 | Resources:
11 | LambdamuxFunction:
12 | Type: AWS::Serverless::Function
13 | Properties:
14 | CodeUri: .
15 | Handler: bootstrap
16 | Runtime: provided.al2
17 | Architectures:
18 | - x86_64
19 | Events:
20 | CatchAll:
21 | Type: Api
22 | Properties:
23 | Path: /{proxy+}
24 | Method: ANY
25 |
26 | Outputs:
27 | LambdamuxAPI:
28 | Description: "API Gateway endpoint URL for Prod stage"
29 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
30 | LambdamuxFunction:
31 | Description: "Lambdamux Lambda Function ARN"
32 | Value: !GetAtt LambdamuxFunction.Arn
33 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ### Added
11 |
12 | ### Changed
13 |
14 | ### Deprecated
15 |
16 | ### Removed
17 |
18 | ### Fixed
19 |
20 | ### Security
21 |
22 | ## [0.0.3] - 2024-10-19
23 | ### Added
24 | - Benchmark tests
25 |
26 | ### Changed
27 | - Improved performance by 14%
28 | - README updates
29 |
30 | ## [0.0.2] - 2024-10-19
31 |
32 | ### Added
33 | - GoDoc documentation for better code understanding and usage
34 | - Readme updates
35 | - Add test
36 |
37 | ## [0.0.1] - 2024-10-19
38 |
39 | ### Added
40 | - Initial release of lambdamux
41 |
42 | [Unreleased]: https://github.com/D-Andreev/lambdamux/compare/v0.0.3...HEAD
43 | [0.0.3]: https://github.com/D-Andreev/lambdamux/compare/v0.0.2...v0.0.3
44 | [0.0.2]: https://github.com/D-Andreev/lambdamux/compare/v0.0.1...v0.0.2
45 | [0.0.1]: https://github.com/D-Andreev/lambdamux/releases/tag/v0.0.1
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 [Dimitar Andreev]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tets
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 |
11 | build:
12 | name: Build and Test
13 | runs-on: ubuntu-latest
14 | steps:
15 |
16 | - name: Set up Go 1.x
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: ^1.19
20 | id: go
21 |
22 | - name: Check out code into the Go module directory
23 | uses: actions/checkout@v2
24 |
25 | - name: Get dependencies
26 | run: |
27 | go get -v -t -d ./...
28 | if [ -f Gopkg.toml ]; then
29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
30 | dep ensure
31 | fi
32 |
33 | - name: Build
34 | run: go build -v ./...
35 |
36 | - name: Test
37 | run: go test -v ./...
38 |
39 | - name: Build Example
40 | run: |
41 | cd examples
42 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bootstrap example.go
43 |
44 | - name: Benchmark
45 | run: go test -bench=. -benchmem ./... | tee bench.txt
46 |
47 | - name: Upload benchmark results
48 | uses: actions/upload-artifact@v3
49 | with:
50 | name: benchmark-results
51 | path: bench.txt
52 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/D-Andreev/lambdamux
2 |
3 | go 1.22.0
4 |
5 | require (
6 | github.com/aquasecurity/lmdrouter v0.4.4
7 | github.com/aws/aws-lambda-go v1.47.0
8 | github.com/awslabs/aws-lambda-go-api-proxy v0.16.2
9 | github.com/gin-gonic/gin v1.10.0
10 | github.com/go-chi/chi/v5 v5.0.8
11 | github.com/gofiber/fiber/v2 v2.52.1
12 | github.com/google/uuid v1.6.0
13 | github.com/stretchr/testify v1.9.0
14 | )
15 |
16 | require (
17 | github.com/andybalholm/brotli v1.1.0 // indirect
18 | github.com/bytedance/sonic v1.11.6 // indirect
19 | github.com/bytedance/sonic/loader v0.1.1 // indirect
20 | github.com/cloudwego/base64x v0.1.4 // indirect
21 | github.com/cloudwego/iasm v0.2.0 // indirect
22 | github.com/davecgh/go-spew v1.1.1 // indirect
23 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
24 | github.com/gin-contrib/sse v0.1.0 // indirect
25 | github.com/go-playground/locales v0.14.1 // indirect
26 | github.com/go-playground/universal-translator v0.18.1 // indirect
27 | github.com/go-playground/validator/v10 v10.20.0 // indirect
28 | github.com/goccy/go-json v0.10.2 // indirect
29 | github.com/json-iterator/go v1.1.12 // indirect
30 | github.com/klauspost/compress v1.17.4 // indirect
31 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect
32 | github.com/leodido/go-urn v1.4.0 // indirect
33 | github.com/mattn/go-colorable v0.1.13 // indirect
34 | github.com/mattn/go-isatty v0.0.20 // indirect
35 | github.com/mattn/go-runewidth v0.0.15 // indirect
36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
37 | github.com/modern-go/reflect2 v1.0.2 // indirect
38 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
39 | github.com/pmezard/go-difflib v1.0.0 // indirect
40 | github.com/rivo/uniseg v0.2.0 // indirect
41 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
42 | github.com/ugorji/go/codec v1.2.12 // indirect
43 | github.com/valyala/bytebufferpool v1.0.0 // indirect
44 | github.com/valyala/fasthttp v1.51.0 // indirect
45 | github.com/valyala/tcplisten v1.0.0 // indirect
46 | golang.org/x/arch v0.8.0 // indirect
47 | golang.org/x/crypto v0.23.0 // indirect
48 | golang.org/x/net v0.25.0 // indirect
49 | golang.org/x/sys v0.20.0 // indirect
50 | golang.org/x/text v0.15.0 // indirect
51 | google.golang.org/protobuf v1.34.1 // indirect
52 | gopkg.in/yaml.v3 v3.0.1 // indirect
53 | )
54 |
--------------------------------------------------------------------------------
/lambdamux.go:
--------------------------------------------------------------------------------
1 | // Package lambdamux provides a lightweight HTTP router for AWS Lambda functions.
2 | package lambdamux
3 |
4 | import (
5 | "context"
6 | "net/http"
7 |
8 | "github.com/D-Andreev/lambdamux/internal/radix"
9 | "github.com/aws/aws-lambda-go/events"
10 | )
11 |
12 | // HandlerFunc defines the function signature for request handlers
13 | type HandlerFunc func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
14 |
15 | // LambdaMux is a request multiplexer for AWS Lambda functions
16 | type LambdaMux struct {
17 | tree *radix.Node
18 | }
19 |
20 | // NewLambdaMux creates and returns a new LambdaMux instance
21 | func NewLambdaMux() *LambdaMux {
22 | return &LambdaMux{
23 | tree: radix.NewNode("", false),
24 | }
25 | }
26 |
27 | func (r *LambdaMux) addRoute(method, path string, handler HandlerFunc) {
28 | fullPath := method + " " + path
29 | r.tree.InsertWithHandler(fullPath, handler)
30 | }
31 |
32 | // GET registers a new GET route with the given path and handler
33 | func (r *LambdaMux) GET(path string, handler HandlerFunc) {
34 | r.addRoute("GET", path, handler)
35 | }
36 |
37 | // POST registers a new POST route with the given path and handler
38 | func (r *LambdaMux) POST(path string, handler HandlerFunc) {
39 | r.addRoute("POST", path, handler)
40 | }
41 |
42 | // PUT registers a new PUT route with the given path and handler
43 | func (r *LambdaMux) PUT(path string, handler HandlerFunc) {
44 | r.addRoute("PUT", path, handler)
45 | }
46 |
47 | // DELETE registers a new DELETE route with the given path and handler
48 | func (r *LambdaMux) DELETE(path string, handler HandlerFunc) {
49 | r.addRoute("DELETE", path, handler)
50 | }
51 |
52 | // Handle processes the incoming API Gateway proxy request and returns the appropriate response
53 | func (r *LambdaMux) Handle(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
54 | path := req.HTTPMethod + " " + req.Path
55 | node, params := r.tree.Search(path)
56 |
57 | if node != nil && node.Handler != nil {
58 | req.PathParameters = params
59 | return node.Handler(ctx, req)
60 | }
61 |
62 | return events.APIGatewayProxyResponse{
63 | StatusCode: http.StatusNotFound,
64 | Body: `{"error": "404 Not Found"}`,
65 | Headers: map[string]string{"Content-Type": "application/json"},
66 | }, nil
67 | }
68 |
--------------------------------------------------------------------------------
/examples/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 |
8 | "github.com/D-Andreev/lambdamux"
9 | "github.com/aws/aws-lambda-go/events"
10 | "github.com/aws/aws-lambda-go/lambda"
11 | )
12 |
13 | func main() {
14 | router := lambdamux.NewLambdaMux()
15 |
16 | // GET request
17 | router.GET("/users", listUsers)
18 |
19 | // GET request with path parameter
20 | router.GET("/users/:id", getUser)
21 |
22 | // POST request
23 | router.POST("/users", createUser)
24 |
25 | // PUT request with path parameter
26 | router.PUT("/users/:id", updateUser)
27 |
28 | // DELETE request with path parameter
29 | router.DELETE("/users/:id", deleteUser)
30 |
31 | lambda.Start(router.Handle)
32 | }
33 |
34 | func listUsers(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
35 | users := []string{"Alice", "Bob", "Charlie"}
36 | body, _ := json.Marshal(users)
37 | return events.APIGatewayProxyResponse{
38 | StatusCode: 200,
39 | Body: string(body),
40 | Headers: map[string]string{"Content-Type": "application/json"},
41 | }, nil
42 | }
43 |
44 | func getUser(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
45 | userID := req.PathParameters["id"]
46 | return events.APIGatewayProxyResponse{
47 | StatusCode: 200,
48 | Body: fmt.Sprintf("User details for ID: %s", userID),
49 | }, nil
50 | }
51 |
52 | func createUser(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
53 | // Here you would typically parse the request body and create a user
54 | return events.APIGatewayProxyResponse{
55 | StatusCode: 201,
56 | Body: "User created successfully",
57 | }, nil
58 | }
59 |
60 | func updateUser(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
61 | userID := req.PathParameters["id"]
62 | return events.APIGatewayProxyResponse{
63 | StatusCode: 200,
64 | Body: fmt.Sprintf("User %s updated successfully", userID),
65 | }, nil
66 | }
67 |
68 | func deleteUser(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
69 | userID := req.PathParameters["id"]
70 | return events.APIGatewayProxyResponse{
71 | StatusCode: 200,
72 | Body: fmt.Sprintf("User %s deleted successfully", userID),
73 | }, nil
74 | }
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | A simple and lightweight high performance HTTP router specifically designed for AWS Lambda functions handling API Gateway requests.
14 |
15 | ## Features
16 | - Fast and efficient routing for static routes
17 | - Seamless handling of path parameters in routes (e.g. `/users/:id`)
18 | - Simple and intuitive API for easy integration
19 |
20 | ## Motivation
21 | When deploying REST APIs on AWS Lambda, a common approach is to use one of the popular HTTP routers with a proxy like [aws-lambda-go-api-proxy](https://github.com/awslabs/aws-lambda-go-api-proxy). While this leverages existing routers, it introduces overhead by converting the `APIGatewayProxyRequest` event to `http.Request`, so that it can be processed by the router.
22 |
23 | `lambdamux` is designed to work directly with API Gateway events, offering several advantages:
24 |
25 | 1. **Efficient Storage**: Uses a radix tree to compactly store routes, reducing memory usage.
26 | 2. **Fast Matching**: Achieves O(m) time complexity for route matching, where m is the url length.
27 | 3. **No Conversion Overhead**: Processes API Gateway events directly, eliminating request conversion time.
28 |
29 | These features make `lambdamux` ideal for serverless environments, optimizing both memory usage and execution time - crucial factors in Lambda performance and cost.
30 |
31 | **Note**: While `lambdamux` offers performance improvements, it's important to recognize that HTTP routers are typically not the main bottleneck in API performance. If you're looking to drastically improve your application's performance, you should primarily focus on optimizing database operations, external network calls, and other potentially time-consuming operations within your application logic. The router's performance becomes more significant in high-throughput scenarios or when dealing with very large numbers of routes.
32 |
33 | ## Installation
34 |
35 | ```
36 | go get github.com/D-Andreev/lambdamux
37 | ```
38 |
39 | ## Usage
40 |
41 | ```go
42 | package main
43 |
44 | import (
45 | "context"
46 | "encoding/json"
47 | "fmt"
48 |
49 | "github.com/D-Andreev/lambdamux"
50 | "github.com/aws/aws-lambda-go/events"
51 | "github.com/aws/aws-lambda-go/lambda"
52 | )
53 |
54 | func main() {
55 | router := lambdamux.NewLambdaMux()
56 |
57 | // GET request
58 | router.GET("/users", listUsers)
59 |
60 | // GET request with path parameter
61 | router.GET("/users/:id", getUser)
62 |
63 | // POST request
64 | router.POST("/users", createUser)
65 |
66 | // PUT request with path parameter
67 | router.PUT("/users/:id", updateUser)
68 |
69 | // DELETE request with path parameter
70 | router.DELETE("/users/:id", deleteUser)
71 |
72 | lambda.Start(router.Handle)
73 | }
74 |
75 | func listUsers(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
76 | users := []string{"Alice", "Bob", "Charlie"}
77 | body, _ := json.Marshal(users)
78 | return events.APIGatewayProxyResponse{
79 | StatusCode: 200,
80 | Body: string(body),
81 | Headers: map[string]string{"Content-Type": "application/json"},
82 | }, nil
83 | }
84 |
85 | func getUser(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
86 | userID := req.PathParameters["id"]
87 | return events.APIGatewayProxyResponse{
88 | StatusCode: 200,
89 | Body: fmt.Sprintf("User details for ID: %s", userID),
90 | }, nil
91 | }
92 |
93 | func createUser(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
94 | // Here you would typically parse the request body and create a user
95 | return events.APIGatewayProxyResponse{
96 | StatusCode: 201,
97 | Body: "User created successfully",
98 | }, nil
99 | }
100 |
101 | func updateUser(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
102 | userID := req.PathParameters["id"]
103 | return events.APIGatewayProxyResponse{
104 | StatusCode: 200,
105 | Body: fmt.Sprintf("User %s updated successfully", userID),
106 | }, nil
107 | }
108 |
109 | func deleteUser(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
110 | userID := req.PathParameters["id"]
111 | return events.APIGatewayProxyResponse{
112 | StatusCode: 200,
113 | Body: fmt.Sprintf("User %s deleted successfully", userID),
114 | }, nil
115 | }
116 |
117 | ```
118 |
119 | ## Running the Examples
120 |
121 | ### Prerequisites
122 |
123 | - [Docker](https://www.docker.com/products/docker-desktop) installed and running
124 | - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) installed
125 | - [Go](https://golang.org/doc/install) installed
126 |
127 | ### Local Testing
128 |
129 | To run the example locally:
130 |
131 | 1. Ensure Docker is running on your machine.
132 |
133 | 2. Navigate to the `examples` directory:
134 | ```
135 | cd examples
136 | ```
137 |
138 | 3. Build the Lambda function:
139 | ```
140 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bootstrap example.go
141 | ```
142 |
143 | 4. Run the example using the AWS SAM CLI:
144 | ```
145 | sam local start-api
146 | ```
147 |
148 | This command will start a local API Gateway emulator using Docker.
149 |
150 | 5. In a new terminal, you can now test your API with curl:
151 | ```
152 | curl http://localhost:3000/users
153 | curl http://localhost:3000/users/123
154 | curl -X POST http://localhost:3000/users -d '{"name": "John Doe"}'
155 | curl -X PUT http://localhost:3000/users/123 -d '{"name": "Jane Doe"}'
156 | curl -X DELETE http://localhost:3000/users/123
157 | ```
158 |
159 | ## Benchmarks
160 |
161 | Benchmarks can be run with `make benchmark` and the full benchmark code can be found [here](https://github.com/D-Andreev/lambdamux/blob/main/lambdamux_benchmark_test.go).
162 | The router used in the benchmarks consists of 50 routes in total, some static and some dynamic.
163 | | Benchmark | Operations | Time per Operation | Bytes per Operation | Allocations per Operation | Using aws-lambda-go-api-proxy | % Slower than LambdaMux |
164 | |--------------------------------------------------------------------------|------------|---------------------|---------------------|---------------------------|-------------------------------|--------------------------|
165 | | LambdaMux | 378,866 | 3,130 ns/op | 2,444 B/op | 40 allocs/op | No | 0% |
166 | | [LmdRouter](https://github.com/aquasecurity/lmdrouter) | 322,635 | 3,707 ns/op | 2,060 B/op | 33 allocs/op | No | 18.43% |
167 | | [Gin](https://github.com/gin-gonic/gin) | 294,595 | 4,069 ns/op | 3,975 B/op | 47 allocs/op | Yes | 29.99% |
168 | | [Chi](https://github.com/go-chi/chi) | 276,445 | 4,360 ns/op | 4,312 B/op | 49 allocs/op | Yes | 39.30% |
169 | | [Standard Library](https://pkg.go.dev/net/http#ServeMux) | 266,296 | 4,552 ns/op | 3,989 B/op | 48 allocs/op | Yes | 45.43% |
170 | | [Fiber](https://github.com/gofiber/fiber) | 211,684 | 5,653 ns/op | 6,324 B/op | 61 allocs/op | Yes | 80.61% |
171 |
--------------------------------------------------------------------------------
/lambdamux_test.go:
--------------------------------------------------------------------------------
1 | package lambdamux
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "testing"
8 |
9 | "github.com/aws/aws-lambda-go/events"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestRouter(t *testing.T) {
14 | router := NewLambdaMux()
15 |
16 | router.POST("/pet", createHandler("POST", "/pet"))
17 | router.PUT("/pet", createHandler("PUT", "/pet"))
18 | router.GET("/pet/findByStatus", createHandler("GET", "/pet/findByStatus"))
19 | router.GET("/pet/findByTags", createHandler("GET", "/pet/findByTags"))
20 | router.GET("/pet/:petId", createHandler("GET", "/pet/:petId"))
21 | router.POST("/pet/:petId", createHandler("POST", "/pet/:petId"))
22 | router.DELETE("/pet/:petId", createHandler("DELETE", "/pet/:petId"))
23 | router.POST("/pet/:petId/uploadImage", createHandler("POST", "/pet/:petId/uploadImage"))
24 | router.GET("/store/inventory", createHandler("GET", "/store/inventory"))
25 | router.POST("/store/order", createHandler("POST", "/store/order"))
26 | router.GET("/store/order/:orderId", createHandler("GET", "/store/order/:orderId"))
27 | router.DELETE("/store/order/:orderId", createHandler("DELETE", "/store/order/:orderId"))
28 | router.POST("/user", createHandler("POST", "/user"))
29 | router.POST("/user/createWithList", createHandler("POST", "/user/createWithList"))
30 | router.GET("/user/login", createHandler("GET", "/user/login"))
31 | router.GET("/user/logout", createHandler("GET", "/user/logout"))
32 | router.GET("/user/:username", createHandler("GET", "/user/:username"))
33 | router.PUT("/user/:username", createHandler("PUT", "/user/:username"))
34 | router.DELETE("/user/:username", createHandler("DELETE", "/user/:username"))
35 |
36 | testCases := []struct {
37 | id int
38 | name string
39 | method string
40 | path string
41 | expectedStatus int
42 | expectedBody map[string]interface{}
43 | expectedParams map[string]string
44 | }{
45 | {
46 | 1,
47 | "POST /pet",
48 | "POST",
49 | "/pet",
50 | 200,
51 | map[string]interface{}{"message": "Handled POST request for /pet"},
52 | nil,
53 | },
54 | {
55 | 2,
56 | "PUT /pet",
57 | "PUT",
58 | "/pet",
59 | 200,
60 | map[string]interface{}{"message": "Handled PUT request for /pet"},
61 | nil,
62 | },
63 | {
64 | 3,
65 | "GET /pet/findByStatus",
66 | "GET",
67 | "/pet/findByStatus",
68 | 200,
69 | map[string]interface{}{"message": "Handled GET request for /pet/findByStatus"},
70 | nil,
71 | },
72 | {
73 | 4,
74 | "GET /pet/findByTags",
75 | "GET",
76 | "/pet/findByTags",
77 | 200,
78 | map[string]interface{}{"message": "Handled GET request for /pet/findByTags"},
79 | nil,
80 | },
81 | {
82 | 5,
83 | "GET /pet/:petId",
84 | "GET",
85 | "/pet/123",
86 | 200,
87 | map[string]interface{}{
88 | "message": "Handled GET request for /pet/:petId",
89 | "params": map[string]string{"petId": "123"},
90 | },
91 | map[string]string{"petId": "123"},
92 | },
93 | {
94 | 6,
95 | "POST /pet/:petId",
96 | "POST",
97 | "/pet/456",
98 | 200,
99 | map[string]interface{}{
100 | "message": "Handled POST request for /pet/:petId",
101 | "params": map[string]string{"petId": "456"},
102 | },
103 | map[string]string{"petId": "456"},
104 | },
105 | {
106 | 7,
107 | "DELETE /pet/:petId",
108 | "DELETE",
109 | "/pet/789",
110 | 200,
111 | map[string]interface{}{
112 | "message": "Handled DELETE request for /pet/:petId",
113 | "params": map[string]string{"petId": "789"},
114 | },
115 | map[string]string{"petId": "789"},
116 | },
117 | {
118 | 8,
119 | "POST /pet/:petId/uploadImage",
120 | "POST",
121 | "/pet/101/uploadImage",
122 | 200,
123 | map[string]interface{}{
124 | "message": "Handled POST request for /pet/:petId/uploadImage",
125 | "params": map[string]string{"petId": "101"},
126 | },
127 | map[string]string{"petId": "101"},
128 | },
129 | {
130 | 9,
131 | "GET /store/inventory",
132 | "GET",
133 | "/store/inventory",
134 | 200,
135 | map[string]interface{}{"message": "Handled GET request for /store/inventory"},
136 | nil,
137 | },
138 | {
139 | 10,
140 | "POST /store/order",
141 | "POST",
142 | "/store/order",
143 | 200,
144 | map[string]interface{}{"message": "Handled POST request for /store/order"},
145 | nil,
146 | },
147 | {
148 | 11,
149 | "GET /store/order/:orderId",
150 | "GET",
151 | "/store/order/1001",
152 | 200,
153 | map[string]interface{}{
154 | "message": "Handled GET request for /store/order/:orderId",
155 | "params": map[string]string{"orderId": "1001"},
156 | },
157 | map[string]string{"orderId": "1001"},
158 | },
159 | {
160 | 12,
161 | "DELETE /store/order/:orderId",
162 | "DELETE",
163 | "/store/order/1002",
164 | 200,
165 | map[string]interface{}{
166 | "message": "Handled DELETE request for /store/order/:orderId",
167 | "params": map[string]string{"orderId": "1002"},
168 | },
169 | map[string]string{"orderId": "1002"},
170 | },
171 | {
172 | 13,
173 | "POST /user",
174 | "POST",
175 | "/user",
176 | 200,
177 | map[string]interface{}{"message": "Handled POST request for /user"},
178 | nil,
179 | },
180 | {
181 | 14,
182 | "POST /user/createWithList",
183 | "POST",
184 | "/user/createWithList",
185 | 200,
186 | map[string]interface{}{"message": "Handled POST request for /user/createWithList"},
187 | nil,
188 | },
189 | {
190 | 15,
191 | "GET /user/login",
192 | "GET",
193 | "/user/login",
194 | 200,
195 | map[string]interface{}{"message": "Handled GET request for /user/login"},
196 | nil,
197 | },
198 | {
199 | 16,
200 | "GET /user/logout",
201 | "GET",
202 | "/user/logout",
203 | 200,
204 | map[string]interface{}{"message": "Handled GET request for /user/logout"},
205 | nil,
206 | },
207 | {
208 | 17,
209 | "GET /user/:username",
210 | "GET",
211 | "/user/johndoe",
212 | 200,
213 | map[string]interface{}{
214 | "message": "Handled GET request for /user/:username",
215 | "params": map[string]string{"username": "johndoe"},
216 | },
217 | map[string]string{"username": "johndoe"},
218 | },
219 | {
220 | 18,
221 | "PUT /user/:username",
222 | "PUT",
223 | "/user/janedoe",
224 | 200,
225 | map[string]interface{}{
226 | "message": "Handled PUT request for /user/:username",
227 | "params": map[string]string{"username": "janedoe"},
228 | },
229 | map[string]string{"username": "janedoe"},
230 | },
231 | {
232 | 19,
233 | "DELETE /user/:username",
234 | "DELETE",
235 | "/user/testuser",
236 | 200,
237 | map[string]interface{}{
238 | "message": "Handled DELETE request for /user/:username",
239 | "params": map[string]string{"username": "testuser"},
240 | },
241 | map[string]string{"username": "testuser"},
242 | },
243 | {20, "Not Found", "GET", "/nonexistent", 404, map[string]interface{}{"error": "404 Not Found"}, nil},
244 | }
245 |
246 | for _, tc := range testCases {
247 | t.Run(fmt.Sprintf("%d: %s", tc.id, tc.name), func(t *testing.T) {
248 | req := events.APIGatewayProxyRequest{
249 | HTTPMethod: tc.method,
250 | Path: tc.path,
251 | }
252 | resp, err := router.Handle(context.Background(), req)
253 |
254 | assert.NoError(t, err, "Test case %d: %s - Unexpected error", tc.id, tc.name)
255 | assert.Equal(t, tc.expectedStatus, resp.StatusCode, "Test case %d: %s - Status code mismatch", tc.id, tc.name)
256 |
257 | var bodyMap map[string]interface{}
258 | err = json.Unmarshal([]byte(resp.Body), &bodyMap)
259 | assert.NoError(t, err, "Test case %d: %s - Failed to unmarshal response body", tc.id, tc.name)
260 |
261 | for key, expectedValue := range tc.expectedBody {
262 | actualValue, exists := bodyMap[key]
263 | assert.True(t, exists, "Test case %d: %s - Key '%s' not found in response body", tc.id, tc.name, key)
264 | if exists {
265 | assert.Equal(
266 | t,
267 | fmt.Sprintf("%v", expectedValue),
268 | fmt.Sprintf("%v", actualValue),
269 | "Test case %d: %s - Value mismatch for key '%s'", tc.id, tc.name, key,
270 | )
271 | }
272 | }
273 | })
274 | }
275 | }
276 |
277 | func createHandler(method, path string) HandlerFunc {
278 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
279 | responseBody := map[string]interface{}{
280 | "message": "Handled " + method + " request for " + path,
281 | }
282 | if len(req.PathParameters) > 0 {
283 | responseBody["params"] = req.PathParameters
284 | }
285 | jsonBody, err := json.Marshal(responseBody)
286 | if err != nil {
287 | return events.APIGatewayProxyResponse{}, err
288 | }
289 | return events.APIGatewayProxyResponse{
290 | StatusCode: 200,
291 | Body: string(jsonBody),
292 | Headers: map[string]string{"Content-Type": "application/json"},
293 | }, nil
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/internal/radix/radix.go:
--------------------------------------------------------------------------------
1 | package radix
2 |
3 | import (
4 | "context"
5 | "log/slog"
6 | "slices"
7 | "sort"
8 | "strings"
9 |
10 | "github.com/aws/aws-lambda-go/events"
11 | )
12 |
13 | type Node struct {
14 | edges []*Node // sorted in ascending order
15 | isComplete bool
16 | value string
17 | fullValue string // used only when returning a node from Search method, otherwise it's not populated
18 | isParam bool
19 | paramNames []string
20 | Handler func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
21 | }
22 |
23 | // NewNode creates a new node
24 | func NewNode(value string, isComplete bool) *Node {
25 | isParam := strings.Contains(value, ":")
26 | paramNames := getParamNames(value)
27 |
28 | return &Node{
29 | edges: []*Node{},
30 | isComplete: isComplete,
31 | value: value,
32 | isParam: isParam,
33 | paramNames: paramNames,
34 | Handler: nil,
35 | }
36 | }
37 |
38 | // getParamNames returns the names of the parameters in the given value
39 | func getParamNames(value string) []string {
40 | var paramNames []string
41 | segments := strings.Split(value, "/")
42 | for _, segment := range segments {
43 | if strings.HasPrefix(segment, ":") {
44 | paramNames = append(paramNames, segment[1:])
45 | }
46 | }
47 | return paramNames
48 | }
49 |
50 | // InsertWithHandler inserts a new node in the tree with a handler
51 | func (n *Node) InsertWithHandler(
52 | input string,
53 | handler func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error),
54 | ) {
55 | node := n.Insert(input)
56 | if node == nil {
57 | return
58 | }
59 | node.Handler = handler
60 | }
61 |
62 | // Insert inserts a new node in the tree
63 | func (n *Node) Insert(input string) *Node {
64 | node := n
65 | search := input
66 | for {
67 | // The key is exhausted and the deepest possible node found.
68 | if len(search) == 0 {
69 | node.isComplete = true
70 | return node
71 | }
72 |
73 | parent := node
74 | node = node.getEdge(search[0], false)
75 |
76 | // No matching edge was found. Just create new edge.
77 | if node == nil {
78 | node = NewNode(search, true)
79 | parent.addEdge(node)
80 | return node
81 | }
82 |
83 | commonPrefix := getCommonPrefix(search, node.value)
84 |
85 | // The current node's value is a fullValue of the search string. Go deeper with remainder.
86 | if commonPrefix == len(node.value) {
87 | search = search[commonPrefix:]
88 | continue
89 | }
90 |
91 | // Split node
92 | child := NewNode(search[:commonPrefix], false)
93 |
94 | // Check for param name conflict
95 | if node.isParam && child.isParam && !slicesEqual(child.paramNames, node.paramNames) {
96 | slog.Error("Route param conflict",
97 | "path", input,
98 | "conflicting_param", node.value,
99 | "existing_param", node.paramNames,
100 | "new_route", input,
101 | )
102 | return nil
103 | }
104 | parent.updateEdge(search[0], child)
105 | node.value = node.value[commonPrefix:]
106 | node.isParam = strings.Contains(node.value, ":")
107 | node.paramNames = getParamNames(node.value)
108 | child.addEdge(node)
109 | search = search[commonPrefix:]
110 | newNode := NewNode(search, true)
111 | if len(search) == 0 {
112 | child.isComplete = true
113 | } else {
114 | child.addEdge(newNode)
115 | }
116 |
117 | return newNode
118 | }
119 | }
120 |
121 | // slicesEqual checks if two slices of strings are equal
122 | func slicesEqual(a, b []string) bool {
123 | if len(a) != len(b) {
124 | return false
125 | }
126 | return slices.Equal(a, b)
127 | }
128 |
129 | // Search gets an item from the tree
130 | func (n *Node) Search(input string) (*Node, map[string]string) {
131 | node := n
132 | search := input
133 | fullPath := ""
134 | params := map[string]string{}
135 | for {
136 | // If search string is empty, we've found the deepest possible node
137 | if len(search) == 0 {
138 | if !node.isComplete {
139 | return nil, nil
140 | }
141 | node.fullValue = fullPath
142 | return node, params
143 | }
144 |
145 | // Get the child node for the first character of the search string
146 | node = node.getEdge(search[0], true)
147 |
148 | // No match
149 | if node == nil {
150 | return nil, nil
151 | }
152 |
153 | // Handle param search
154 | if node.isParam {
155 | searchSegments := strings.Split(search, "/")
156 | nodeSegments := strings.Split(node.value, "/")
157 |
158 | if len(nodeSegments) > len(searchSegments) {
159 | return nil, nil
160 | }
161 |
162 | paramIndex := 0
163 | for i := range nodeSegments {
164 | if i < len(searchSegments) {
165 | if paramIndex > 0 && paramIndex > len(node.paramNames)-1 {
166 | break
167 | }
168 | if nodeSegments[i] != searchSegments[i] && !strings.HasPrefix(nodeSegments[i], ":") {
169 | return nil, nil
170 | }
171 | if strings.HasPrefix(nodeSegments[i], ":") {
172 | if paramIndex < len(node.paramNames) {
173 |
174 | params[node.paramNames[paramIndex]] = searchSegments[i]
175 | searchSegments[i] = nodeSegments[i]
176 | paramIndex++
177 | }
178 | }
179 | }
180 | }
181 |
182 | search = strings.Join(searchSegments, "/")
183 | fullPath += node.value
184 |
185 | if len(search) > len(node.value) {
186 | search = search[len(node.value):]
187 | } else {
188 | search = ""
189 | }
190 | continue
191 | }
192 |
193 | // Find the common prefix between the search string and the node's value
194 | commonPrefix := getCommonPrefix(search, node.value)
195 | fullPath += search[:commonPrefix]
196 |
197 | // If the common prefix length equals the node's value length, continue searching
198 | if commonPrefix == len(node.value) {
199 | search = search[commonPrefix:]
200 | continue
201 | }
202 |
203 | // No match
204 | return nil, nil
205 | }
206 | }
207 |
208 | // getFirstMatchIdx performs a binary search to find the index of the first edge
209 | // that matches or exceeds the given label.
210 | func (n *Node) getFirstMatchIdx(label byte) int {
211 | num := len(n.edges)
212 | return sort.Search(
213 | num, func(i int) bool {
214 | return n.edges[i].value[0] >= label
215 | },
216 | )
217 | }
218 |
219 | // addEdge inserts a new edge while maintaining sorted order
220 | func (n *Node) addEdge(e *Node) {
221 | idx := n.getFirstMatchIdx(e.value[0])
222 | n.edges = append(n.edges, e)
223 | copy(n.edges[idx+1:], n.edges[idx:])
224 | n.edges[idx] = e
225 | }
226 |
227 | // updateEdge updates an existing edge with a new node
228 | func (n *Node) updateEdge(label byte, node *Node) {
229 | idx := n.getFirstMatchIdx(label)
230 | if idx < len(n.edges) && n.edges[idx].value[0] == label {
231 | n.edges[idx] = node
232 | return
233 | }
234 |
235 | panic("We're trying to replace a missing node. This should never happen.")
236 | }
237 |
238 | // getEdge returns the edge that matches the given label
239 | func (n *Node) getEdge(label byte, matchParam bool) *Node {
240 | idx := n.getFirstMatchIdx(label)
241 |
242 | if idx < len(n.edges) && n.edges[idx].value[0] == label {
243 | return n.edges[idx]
244 | }
245 |
246 | if !matchParam {
247 | return nil
248 | }
249 |
250 | // There was no exact match, so check for slug edges
251 | for _, e := range n.edges {
252 | nodeSegments := strings.Split(e.value, "/")
253 | if len(nodeSegments) == 0 {
254 | continue
255 | }
256 | if strings.Contains(nodeSegments[0], ":") {
257 | return e
258 | }
259 | }
260 |
261 | return nil
262 | }
263 |
264 | // getCommonPrefix returns the length of the longest common prefix between two strings
265 | func getCommonPrefix(k1, k2 string) int {
266 | maxLen := len(k1)
267 | if len(k2) < maxLen {
268 | maxLen = len(k2)
269 | }
270 | for i := 0; i < maxLen; i++ {
271 | if k1[i] != k2[i] {
272 | return i
273 | }
274 | }
275 | return maxLen
276 | }
277 |
278 | // GetAllCompleteItems returns all complete items in the tree
279 | func (n *Node) GetAllCompleteItems() []string {
280 | var result []string
281 |
282 | result = dfsCompleteItems(n, "", result)
283 | sort.Strings(result)
284 | return result
285 | }
286 |
287 | // GetAllNodeValues returns all node values in the tree
288 | func (n *Node) GetAllNodeValues() []string {
289 | var result []string
290 |
291 | result = dfs(n, result)
292 | sort.Strings(result)
293 | return result
294 | }
295 |
296 | // dfsCompleteItems performs a depth-first search to find all complete items
297 | func dfsCompleteItems(node *Node, prefix string, result []string) []string {
298 | if node.isComplete {
299 | result = append(result, prefix)
300 | }
301 |
302 | for _, edge := range node.edges {
303 | result = dfsCompleteItems(edge, prefix+edge.value, result)
304 | }
305 |
306 | return result
307 | }
308 |
309 | // dfs performs a depth-first search to find all keys in the tree
310 | func dfs(node *Node, result []string) []string {
311 | if len(node.value) > 0 {
312 | result = append(result, node.value)
313 | }
314 |
315 | for _, child := range node.edges {
316 | result = dfs(child, result)
317 | }
318 |
319 | return result
320 | }
321 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
3 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
4 | github.com/aquasecurity/lmdrouter v0.4.4 h1:wh0qHM7s74HFOZiINI3fsBYyquBTWpF1U+JrcJbR+dA=
5 | github.com/aquasecurity/lmdrouter v0.4.4/go.mod h1:vkF/ZqcXXUIcJXeAtUF867A/SHONd8MUw/+sy7uLXRA=
6 | github.com/aws/aws-lambda-go v1.15.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw=
7 | github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI=
8 | github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
9 | github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 h1:CJyGEyO1CIwOnXTU40urf0mchf6t3voxpvUDikOU9LY=
10 | github.com/awslabs/aws-lambda-go-api-proxy v0.16.2/go.mod h1:vxxjwBHe/KbgFeNlAP/Tvp4SsVRL3WQamcWRxqVh0z0=
11 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
12 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
13 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
14 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
15 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
16 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
17 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
18 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
19 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
23 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
24 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
25 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
26 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
27 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
28 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
29 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
30 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
31 | github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
32 | github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
33 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
34 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
35 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
36 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
37 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
38 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
39 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
40 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
41 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
42 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
43 | github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI=
44 | github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
45 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
46 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
47 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
48 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
49 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
50 | github.com/jgroeneveld/schema v1.0.0 h1:J0E10CrOkiSEsw6dfb1IfrDJD14pf6QLVJ3tRPl/syI=
51 | github.com/jgroeneveld/schema v1.0.0/go.mod h1:M14lv7sNMtGvo3ops1MwslaSYgDYxrSmbzWIQ0Mr5rs=
52 | github.com/jgroeneveld/trial v2.0.0+incompatible h1:d59ctdgor+VqdZCAiUfVN8K13s0ALDioG5DWwZNtRuQ=
53 | github.com/jgroeneveld/trial v2.0.0+incompatible/go.mod h1:I6INLW96EN8WysNBXUFI3M4RIC8ePg9ntAc/Wy+U/+M=
54 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
55 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
56 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
57 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
58 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
59 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
60 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
61 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
62 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
63 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
64 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
65 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
66 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
67 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
68 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
69 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
70 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
71 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
73 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
74 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
75 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
76 | github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
77 | github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
78 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
79 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
80 | github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
81 | github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
82 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
83 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
84 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
85 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
86 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
87 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
88 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
89 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
90 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
91 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
92 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
93 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
94 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
95 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
96 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
97 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
98 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
99 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
100 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
101 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
102 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
103 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
104 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
105 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
106 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
107 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
108 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
109 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
110 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
111 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
112 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
113 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
114 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
115 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
116 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
117 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
118 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
119 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
120 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
121 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
123 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
124 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
125 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
126 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
127 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
128 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
129 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
130 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
131 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
132 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
133 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
134 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
135 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
136 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
137 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
138 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
139 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
140 |
--------------------------------------------------------------------------------
/lambdamux_benchmark_test.go:
--------------------------------------------------------------------------------
1 | package lambdamux
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "testing"
9 |
10 | "github.com/aquasecurity/lmdrouter"
11 | "github.com/aws/aws-lambda-go/events"
12 | "github.com/stretchr/testify/assert"
13 |
14 | fiberadapter "github.com/awslabs/aws-lambda-go-api-proxy/fiber"
15 | "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"
16 |
17 | chiadapter "github.com/awslabs/aws-lambda-go-api-proxy/chi"
18 | ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
19 | "github.com/gin-gonic/gin"
20 | "github.com/go-chi/chi/v5"
21 | "github.com/gofiber/fiber/v2"
22 | )
23 |
24 | var routes = []struct {
25 | method string
26 | path string
27 | }{
28 | {"POST", "/pet"},
29 | {"PUT", "/pet"},
30 | {"GET", "/pet/findByStatus"},
31 | {"GET", "/pet/findByTags"},
32 | {"GET", "/pet/:petId"},
33 | {"POST", "/pet/:petId"},
34 | {"DELETE", "/pet/:petId"},
35 | {"POST", "/pet/:petId/uploadImage"},
36 | {"GET", "/store/inventory"},
37 | {"POST", "/store/order"},
38 | {"GET", "/store/order/:orderId"},
39 | {"DELETE", "/store/order/:orderId"},
40 | {"POST", "/user"},
41 | {"POST", "/user/createWithList"},
42 | {"GET", "/user/login"},
43 | {"GET", "/user/logout"},
44 | {"GET", "/user/:username"},
45 | {"PUT", "/user/:username"},
46 | {"DELETE", "/user/:username"},
47 | {"GET", "/pet/:petId/medical-history"},
48 | {"POST", "/pet/:petId/vaccination"},
49 | {"GET", "/store/order/:orderId/tracking"},
50 | {"PUT", "/store/order/:orderId/status"},
51 | {"GET", "/user/:username/preferences"},
52 | {"POST", "/user/:username/address"},
53 | {"GET", "/pet/:petId/appointments"},
54 | {"POST", "/pet/:petId/appointment"},
55 | {"PUT", "/pet/:petId/appointment/:appointmentId"},
56 | {"DELETE", "/pet/:petId/appointment/:appointmentId"},
57 | {"GET", "/store/products"},
58 | {"GET", "/store/product/:productId"},
59 | {"POST", "/store/product"},
60 | {"PUT", "/store/product/:productId"},
61 | {"DELETE", "/store/product/:productId"},
62 | {"GET", "/user/:username/orders"},
63 | {"POST", "/user/:username/review"},
64 | {"GET", "/user/:username/review/:reviewId"},
65 | {"PUT", "/user/:username/review/:reviewId"},
66 | {"DELETE", "/user/:username/review/:reviewId"},
67 | {"GET", "/clinic/:clinicId"},
68 | {"POST", "/clinic"},
69 | {"PUT", "/clinic/:clinicId"},
70 | {"DELETE", "/clinic/:clinicId"},
71 | {"GET", "/clinic/:clinicId/staff"},
72 | {"POST", "/clinic/:clinicId/staff"},
73 | {"GET", "/clinic/:clinicId/staff/:staffId"},
74 | {"PUT", "/clinic/:clinicId/staff/:staffId"},
75 | {"DELETE", "/clinic/:clinicId/staff/:staffId"},
76 | {"GET", "/clinic/:clinicId/appointments"},
77 | {"POST", "/clinic/:clinicId/appointment/:appointmentId/reschedule"},
78 | }
79 |
80 | var routesWithBraces = []struct {
81 | method string
82 | path string
83 | }{
84 | {"POST", "/pet"},
85 | {"PUT", "/pet"},
86 | {"GET", "/pet/findByStatus"},
87 | {"GET", "/pet/findByTags"},
88 | {"GET", "/pet/{petId}"},
89 | {"POST", "/pet/{petId}"},
90 | {"DELETE", "/pet/{petId}"},
91 | {"POST", "/pet/{petId}/uploadImage"},
92 | {"GET", "/store/inventory"},
93 | {"POST", "/store/order"},
94 | {"GET", "/store/order/{orderId}"},
95 | {"DELETE", "/store/order/{orderId}"},
96 | {"POST", "/user"},
97 | {"POST", "/user/createWithList"},
98 | {"GET", "/user/login"},
99 | {"GET", "/user/logout"},
100 | {"GET", "/user/{username}"},
101 | {"PUT", "/user/{username}"},
102 | {"DELETE", "/user/{username}"},
103 | {"GET", "/pet/{petId}/medical-history"},
104 | {"POST", "/pet/{petId}/vaccination"},
105 | {"GET", "/store/order/{orderId}/tracking"},
106 | {"PUT", "/store/order/{orderId}/status"},
107 | {"GET", "/user/{username}/preferences"},
108 | {"POST", "/user/{username}/address"},
109 | {"GET", "/pet/{petId}/appointments"},
110 | {"POST", "/pet/{petId}/appointment"},
111 | {"PUT", "/pet/{petId}/appointment/{appointmentId}"},
112 | {"DELETE", "/pet/{petId}/appointment/{appointmentId}"},
113 | {"GET", "/store/products"},
114 | {"GET", "/store/product/{productId}"},
115 | {"POST", "/store/product"},
116 | {"PUT", "/store/product/{productId}"},
117 | {"DELETE", "/store/product/{productId}"},
118 | {"GET", "/user/{username}/orders"},
119 | {"POST", "/user/{username}/review"},
120 | {"GET", "/user/{username}/review/{reviewId}"},
121 | {"PUT", "/user/{username}/review/{reviewId}"},
122 | {"DELETE", "/user/{username}/review/{reviewId}"},
123 | {"GET", "/clinic/{clinicId}"},
124 | {"POST", "/clinic"},
125 | {"PUT", "/clinic/{clinicId}"},
126 | {"DELETE", "/clinic/{clinicId}"},
127 | {"GET", "/clinic/{clinicId}/staff"},
128 | {"POST", "/clinic/{clinicId}/staff"},
129 | {"GET", "/clinic/{clinicId}/staff/{staffId}"},
130 | {"PUT", "/clinic/{clinicId}/staff/{staffId}"},
131 | {"DELETE", "/clinic/{clinicId}/staff/{staffId}"},
132 | {"GET", "/clinic/{clinicId}/appointments"},
133 | {"POST", "/clinic/{clinicId}/appointment/{appointmentId}/reschedule"},
134 | }
135 |
136 | var allParams = []string{
137 | "petId", "orderId", "username", "appointmentId", "productId", "reviewId", "clinicId", "staffId",
138 | }
139 |
140 | func setupLambdaMux() *LambdaMux {
141 | router := NewLambdaMux()
142 | for _, route := range routes {
143 | router.addRoute(route.method, route.path, lambdahttpCreateHandler(route.method, route.path))
144 | }
145 | return router
146 | }
147 |
148 | func setupStandardLibrary() *http.ServeMux {
149 | router := http.NewServeMux()
150 | for _, route := range routesWithBraces {
151 | method := route.method
152 | path := route.path
153 | router.HandleFunc(fmt.Sprintf("%s %s", method, path), func(w http.ResponseWriter, r *http.Request) {
154 | w.WriteHeader(http.StatusOK)
155 | createStandardLibraryHandler(method, path)(w, r)
156 | })
157 | }
158 | return router
159 | }
160 |
161 | func setupGinRouter() *gin.Engine {
162 | gin.SetMode(gin.ReleaseMode)
163 | r := gin.New()
164 | for _, route := range routes {
165 | r.Handle(route.method, route.path, ginCreateHandler(route.method, route.path))
166 | }
167 | return r
168 | }
169 |
170 | func lambdahttpCreateHandler(method, path string) HandlerFunc {
171 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
172 | responseBody := map[string]interface{}{
173 | "message": "Handled " + method + " request for " + path,
174 | }
175 | if len(req.PathParameters) > 0 {
176 | responseBody["params"] = req.PathParameters
177 | }
178 | jsonBody, _ := json.Marshal(responseBody)
179 | return events.APIGatewayProxyResponse{
180 | StatusCode: 200,
181 | Body: string(jsonBody),
182 | Headers: map[string]string{"Content-Type": "application/json"},
183 | }, nil
184 | }
185 | }
186 |
187 | func createStandardLibraryHandler(method, path string) http.HandlerFunc {
188 | return func(w http.ResponseWriter, r *http.Request) {
189 | responseBody := map[string]interface{}{
190 | "message": "Handled " + method + " request for " + path,
191 | }
192 | params := make(map[string]string)
193 | for _, param := range allParams {
194 | if value := r.PathValue(param); value != "" {
195 | params[param] = value
196 | }
197 | }
198 | if len(params) > 0 {
199 | responseBody["params"] = params
200 | }
201 | json.NewEncoder(w).Encode(responseBody)
202 | }
203 | }
204 |
205 | func ginCreateHandler(method, path string) gin.HandlerFunc {
206 | return func(c *gin.Context) {
207 | responseBody := map[string]interface{}{
208 | "message": "Handled " + method + " request for " + path,
209 | }
210 | params := make(map[string]string)
211 | for _, param := range allParams {
212 | if value := c.Param(param); value != "" {
213 | params[param] = value
214 | }
215 | }
216 | if len(params) > 0 {
217 | responseBody["params"] = params
218 | }
219 | c.JSON(200, responseBody)
220 | }
221 | }
222 |
223 | func setupFiberRouter() *fiber.App {
224 | app := fiber.New()
225 | for _, route := range routes {
226 | app.Add(route.method, route.path, fiberCreateHandler(route.method, route.path))
227 | }
228 | return app
229 | }
230 |
231 | func fiberCreateHandler(method, path string) fiber.Handler {
232 | return func(c *fiber.Ctx) error {
233 | responseBody := map[string]interface{}{
234 | "message": "Handled " + method + " request for " + path,
235 | }
236 | params := make(map[string]string)
237 | for _, param := range allParams {
238 | if value := c.Params(param); value != "" {
239 | params[param] = value
240 | }
241 | }
242 | if len(params) > 0 {
243 | responseBody["params"] = params
244 | }
245 | return c.JSON(responseBody)
246 | }
247 | }
248 |
249 | func setupChiRouter() *chi.Mux {
250 | r := chi.NewRouter()
251 | for _, route := range routesWithBraces {
252 | r.MethodFunc(route.method, route.path, chiCreateHandler(route.method, route.path))
253 | }
254 | return r
255 | }
256 |
257 | func chiCreateHandler(method, path string) http.HandlerFunc {
258 | return func(w http.ResponseWriter, r *http.Request) {
259 | responseBody := map[string]interface{}{
260 | "message": "Handled " + method + " request for " + path,
261 | }
262 |
263 | params := make(map[string]string)
264 | for _, param := range allParams {
265 | if value := chi.URLParam(r, param); value != "" {
266 | params[param] = value
267 | }
268 | }
269 |
270 | if len(params) > 0 {
271 | responseBody["params"] = params
272 | }
273 |
274 | w.Header().Set("Content-Type", "application/json")
275 | json.NewEncoder(w).Encode(responseBody)
276 | }
277 | }
278 |
279 | func setupLmdRouter() *lmdrouter.Router {
280 | router := lmdrouter.NewRouter("")
281 | for _, route := range routes {
282 | router.Route(route.method, route.path, lmdCreateHandler(route.method, route.path))
283 | }
284 | return router
285 | }
286 |
287 | func lmdCreateHandler(method, path string) lmdrouter.Handler {
288 | return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
289 | responseBody := map[string]interface{}{
290 | "message": "Handled " + method + " request for " + path,
291 | }
292 | if len(req.PathParameters) > 0 {
293 | responseBody["params"] = req.PathParameters
294 | }
295 | jsonBody, _ := json.Marshal(responseBody)
296 | return events.APIGatewayProxyResponse{
297 | StatusCode: 200,
298 | Body: string(jsonBody),
299 | Headers: map[string]string{"Content-Type": "application/json"},
300 | }, nil
301 | }
302 | }
303 |
304 | var benchmarkRequests = []events.APIGatewayProxyRequest{
305 | {HTTPMethod: "POST", Path: "/pet"},
306 | {HTTPMethod: "PUT", Path: "/pet"},
307 | {HTTPMethod: "GET", Path: "/pet/findByStatus"},
308 | {HTTPMethod: "GET", Path: "/pet/findByTags"},
309 | {HTTPMethod: "GET", Path: "/pet/123", PathParameters: map[string]string{"petId": "123"}},
310 | {HTTPMethod: "POST", Path: "/pet/456", PathParameters: map[string]string{"petId": "456"}},
311 | {HTTPMethod: "DELETE", Path: "/pet/789", PathParameters: map[string]string{"petId": "789"}},
312 | {HTTPMethod: "POST", Path: "/pet/101/uploadImage", PathParameters: map[string]string{"petId": "101"}},
313 | {HTTPMethod: "GET", Path: "/store/inventory"},
314 | {HTTPMethod: "POST", Path: "/store/order"},
315 | {HTTPMethod: "GET", Path: "/store/order/202", PathParameters: map[string]string{"orderId": "202"}},
316 | {HTTPMethod: "DELETE", Path: "/store/order/303", PathParameters: map[string]string{"orderId": "303"}},
317 | {HTTPMethod: "POST", Path: "/user"},
318 | {HTTPMethod: "POST", Path: "/user/createWithList"},
319 | {HTTPMethod: "GET", Path: "/user/login"},
320 | {HTTPMethod: "GET", Path: "/user/logout"},
321 | {HTTPMethod: "GET", Path: "/user/johndoe", PathParameters: map[string]string{"username": "johndoe"}},
322 | {HTTPMethod: "PUT", Path: "/user/janedoe", PathParameters: map[string]string{"username": "janedoe"}},
323 | {HTTPMethod: "DELETE", Path: "/user/bobsmith", PathParameters: map[string]string{"username": "bobsmith"}},
324 | {HTTPMethod: "GET", Path: "/pet/404/medical-history", PathParameters: map[string]string{"petId": "404"}},
325 | {HTTPMethod: "POST", Path: "/pet/505/vaccination", PathParameters: map[string]string{"petId": "505"}},
326 | {HTTPMethod: "GET", Path: "/store/order/606/tracking", PathParameters: map[string]string{"orderId": "606"}},
327 | {HTTPMethod: "PUT", Path: "/store/order/707/status", PathParameters: map[string]string{"orderId": "707"}},
328 | {HTTPMethod: "GET", Path: "/user/alicesmith/preferences", PathParameters: map[string]string{"username": "alicesmith"}},
329 | {HTTPMethod: "POST", Path: "/user/bobdoe/address", PathParameters: map[string]string{"username": "bobdoe"}},
330 | {HTTPMethod: "GET", Path: "/pet/808/appointments", PathParameters: map[string]string{"petId": "808"}},
331 | {HTTPMethod: "POST", Path: "/pet/909/appointment", PathParameters: map[string]string{"petId": "909"}},
332 | {HTTPMethod: "PUT", Path: "/pet/1010/appointment/2020", PathParameters: map[string]string{"petId": "1010", "appointmentId": "2020"}},
333 | {HTTPMethod: "DELETE", Path: "/pet/1111/appointment/2121", PathParameters: map[string]string{"petId": "1111", "appointmentId": "2121"}},
334 | {HTTPMethod: "GET", Path: "/store/products"},
335 | {HTTPMethod: "GET", Path: "/store/product/3030", PathParameters: map[string]string{"productId": "3030"}},
336 | {HTTPMethod: "POST", Path: "/store/product"},
337 | {HTTPMethod: "PUT", Path: "/store/product/4040", PathParameters: map[string]string{"productId": "4040"}},
338 | {HTTPMethod: "DELETE", Path: "/store/product/5050", PathParameters: map[string]string{"productId": "5050"}},
339 | {HTTPMethod: "GET", Path: "/user/charlielee/orders", PathParameters: map[string]string{"username": "charlielee"}},
340 | {HTTPMethod: "POST", Path: "/user/davidwang/review", PathParameters: map[string]string{"username": "davidwang"}},
341 | {HTTPMethod: "GET", Path: "/user/evebrown/review/6060", PathParameters: map[string]string{"username": "evebrown", "reviewId": "6060"}},
342 | {HTTPMethod: "PUT", Path: "/user/frankgreen/review/7070", PathParameters: map[string]string{"username": "frankgreen", "reviewId": "7070"}},
343 | {HTTPMethod: "DELETE", Path: "/user/gracewu/review/8080", PathParameters: map[string]string{"username": "gracewu", "reviewId": "8080"}},
344 | {HTTPMethod: "GET", Path: "/clinic/9090", PathParameters: map[string]string{"clinicId": "9090"}},
345 | {HTTPMethod: "POST", Path: "/clinic"},
346 | {HTTPMethod: "PUT", Path: "/clinic/1212", PathParameters: map[string]string{"clinicId": "1212"}},
347 | {HTTPMethod: "DELETE", Path: "/clinic/1313", PathParameters: map[string]string{"clinicId": "1313"}},
348 | {HTTPMethod: "GET", Path: "/clinic/1414/staff", PathParameters: map[string]string{"clinicId": "1414"}},
349 | {HTTPMethod: "POST", Path: "/clinic/1515/staff", PathParameters: map[string]string{"clinicId": "1515"}},
350 | {HTTPMethod: "GET", Path: "/clinic/1616/staff/1717", PathParameters: map[string]string{"clinicId": "1616", "staffId": "1717"}},
351 | {HTTPMethod: "PUT", Path: "/clinic/1818/staff/1919", PathParameters: map[string]string{"clinicId": "1818", "staffId": "1919"}},
352 | {HTTPMethod: "DELETE", Path: "/clinic/2020/staff/2121", PathParameters: map[string]string{"clinicId": "2020", "staffId": "2121"}},
353 | {HTTPMethod: "GET", Path: "/clinic/2222/appointments", PathParameters: map[string]string{"clinicId": "2222"}},
354 | {HTTPMethod: "POST", Path: "/clinic/2323/appointment/2424/reschedule", PathParameters: map[string]string{"clinicId": "2323", "appointmentId": "2424"}},
355 | }
356 |
357 | func assertResponse(b *testing.B, resp events.APIGatewayProxyResponse, req events.APIGatewayProxyRequest) {
358 | b.Helper()
359 | assert.Equal(b, 200, resp.StatusCode)
360 | if len(req.PathParameters) > 0 {
361 | var body map[string]interface{}
362 | err := json.Unmarshal([]byte(resp.Body), &body)
363 | assert.NoError(b, err)
364 | params, ok := body["params"].(map[string]interface{})
365 | assert.True(b, ok, "params should be a map")
366 | for key, expectedValue := range req.PathParameters {
367 | actualValue, exists := params[key]
368 | assert.True(b, exists, "param %s should exist", key)
369 | assert.Equal(b, expectedValue, actualValue, "param %s should match", key)
370 | }
371 | }
372 | }
373 |
374 | func BenchmarkLambdaMux(b *testing.B) {
375 | router := setupLambdaMux()
376 | b.ResetTimer()
377 | for i := 0; i < b.N; i++ {
378 | req := benchmarkRequests[i%len(benchmarkRequests)]
379 | resp, err := router.Handle(context.Background(), req)
380 | assert.NoError(b, err)
381 | assertResponse(b, resp, req)
382 | }
383 | }
384 |
385 | func BenchmarkLmdRouter(b *testing.B) {
386 | router := setupLmdRouter()
387 | b.ResetTimer()
388 | for i := 0; i < b.N; i++ {
389 | req := benchmarkRequests[i%len(benchmarkRequests)]
390 | resp, err := router.Handler(context.Background(), req)
391 | assert.NoError(b, err)
392 | assertResponse(b, resp, req)
393 | }
394 | }
395 |
396 | func BenchmarkAWSLambdaGoAPIProxyWithGin(b *testing.B) {
397 | r := setupGinRouter()
398 | adapter := ginadapter.New(r)
399 | b.ResetTimer()
400 | for i := 0; i < b.N; i++ {
401 | req := benchmarkRequests[i%len(benchmarkRequests)]
402 | resp, err := adapter.ProxyWithContext(context.Background(), req)
403 | assert.NoError(b, err)
404 | assertResponse(b, resp, req)
405 | }
406 | }
407 |
408 | func BenchmarkAWSLambdaGoAPIProxyWithFiber(b *testing.B) {
409 | app := setupFiberRouter()
410 | adapter := fiberadapter.New(app)
411 | b.ResetTimer()
412 | for i := 0; i < b.N; i++ {
413 | req := benchmarkRequests[i%len(benchmarkRequests)]
414 | resp, err := adapter.ProxyWithContext(context.Background(), req)
415 | assert.NoError(b, err)
416 | assertResponse(b, resp, req)
417 | }
418 | }
419 |
420 | func BenchmarkAWSLambdaGoAPIProxyWithChi(b *testing.B) {
421 | r := setupChiRouter()
422 | adapter := chiadapter.New(r)
423 | b.ResetTimer()
424 | for i := 0; i < b.N; i++ {
425 | req := benchmarkRequests[i%len(benchmarkRequests)]
426 | resp, err := adapter.ProxyWithContext(context.Background(), req)
427 | assert.NoError(b, err)
428 | assertResponse(b, resp, req)
429 | }
430 | }
431 |
432 | func BenchmarkStandardLibrary(b *testing.B) {
433 | router := setupStandardLibrary()
434 | adapter := httpadapter.New(router)
435 | b.ResetTimer()
436 | for i := 0; i < b.N; i++ {
437 | req := benchmarkRequests[i%len(benchmarkRequests)]
438 | resp, err := adapter.ProxyWithContext(context.Background(), req)
439 | assert.NoError(b, err)
440 | assertResponse(b, resp, req)
441 | }
442 | }
443 |
--------------------------------------------------------------------------------
/internal/radix/radix_test.go:
--------------------------------------------------------------------------------
1 | package radix
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "testing"
7 |
8 | "github.com/google/uuid"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | type InsertTestCase struct {
13 | id int
14 | input []string
15 | keys []string
16 | fullItems []string
17 | }
18 |
19 | type SearchTestCase struct {
20 | id int
21 | input []string
22 | search string
23 | output string
24 | params map[string]string
25 | notFoundExpected bool
26 | }
27 |
28 | func TestInsert(t *testing.T) {
29 | testCases := []InsertTestCase{
30 | {id: 1, input: []string{"water"}, keys: []string{"water"}, fullItems: []string{"water"}},
31 | {
32 | id: 2, input: []string{"water", "water"}, keys: []string{"water"}, fullItems: []string{"water"},
33 | },
34 | {
35 | id: 3, input: []string{"water", "slow"}, keys: []string{"slow", "water"},
36 | fullItems: []string{"slow", "water"},
37 | },
38 | {
39 | id: 4,
40 | input: []string{"water", "slow", "slower"},
41 | keys: []string{"er", "slow", "water"},
42 | fullItems: []string{"slow", "slower", "water"},
43 | },
44 | {
45 | id: 5,
46 | input: []string{"water", "slow", "slower", "wash"},
47 | keys: []string{"er", "sh", "slow", "ter", "wa"},
48 | fullItems: []string{"slow", "slower", "wash", "water"},
49 | },
50 | {
51 | id: 6,
52 | input: []string{"water", "slow", "slower", "wash", "washer"},
53 | keys: []string{"er", "er", "sh", "slow", "ter", "wa"},
54 | fullItems: []string{"slow", "slower", "wash", "washer", "water"},
55 | },
56 | {
57 | id: 7,
58 | input: []string{"water", "slow", "slower", "wash", "washer", "wasnt"},
59 | keys: []string{"er", "er", "h", "nt", "s", "slow", "ter", "wa"},
60 | fullItems: []string{"slow", "slower", "wash", "washer", "wasnt", "water"},
61 | },
62 | {
63 | id: 8,
64 | input: []string{"water", "slow", "slower", "wash", "washer", "wasnt", "watering"},
65 | keys: []string{"er", "er", "h", "ing", "nt", "s", "slow", "ter", "wa"},
66 | fullItems: []string{"slow", "slower", "wash", "washer", "wasnt", "water", "watering"},
67 | },
68 | {
69 | id: 9,
70 | input: []string{
71 | "alligator", "alien", "baloon", "chromodynamic", "romane", "romanus", "romulus", "rubens", "ruber",
72 | "rubicon", "rubicundus", "all", "rub", "ba",
73 | },
74 | keys: []string{
75 | "al", "an", "ba", "chromodynamic", "e", "e", "ic", "ien", "igator", "l", "loon", "ns", "om", "on", "r",
76 | "r", "ub", "ulus", "undus", "us",
77 | },
78 | fullItems: []string{
79 | "alien", "all", "alligator", "ba", "baloon", "chromodynamic", "romane", "romanus", "romulus", "rub",
80 | "rubens",
81 | "ruber", "rubicon", "rubicundus",
82 | },
83 | },
84 | }
85 |
86 | for _, tc := range testCases {
87 | tree := NewNode("", false)
88 | for _, j := range tc.input {
89 | tree.Insert(j)
90 | }
91 | allKeys := tree.GetAllNodeValues()
92 | assert.Equal(t, tc.keys, allKeys)
93 | allItems := tree.GetAllCompleteItems()
94 | assert.Equal(t, tc.fullItems, allItems)
95 | }
96 |
97 | }
98 |
99 | func TestInsertWithRandomIds(t *testing.T) {
100 | randomIds := generateUUIDs()
101 | sort.Strings(randomIds)
102 | testCases := []InsertTestCase{
103 | {input: randomIds, fullItems: randomIds},
104 | }
105 |
106 | for _, tc := range testCases {
107 | tree := NewNode("", false)
108 | for _, j := range tc.input {
109 | tree.Insert(j)
110 | }
111 | allItems := tree.GetAllCompleteItems()
112 | assert.Equal(t, tc.fullItems, allItems)
113 | }
114 | }
115 |
116 | func TestSearch(t *testing.T) {
117 | testCases := []SearchTestCase{
118 | {id: 0, input: []string{"water"}, search: "non-existing-item", output: ""},
119 | {id: 1, input: []string{"water"}, search: "water", output: "water"},
120 | {
121 | id: 2, input: []string{"water", "water"}, search: "water", output: "water",
122 | },
123 | {
124 | id: 3, input: []string{"water", "slow"}, output: "slow", search: "slow",
125 | },
126 | {
127 | id: 4,
128 | input: []string{"water", "slow", "slower", "wash", "washer", "wasnt", "watering"},
129 | search: "wasnt",
130 | output: "wasnt",
131 | },
132 | {
133 | id: 5,
134 | input: []string{
135 | "alligator", "alien", "baloon", "chromodynamic", "romane", "romanus", "romulus", "rubens", "ruber",
136 | "rubicon", "rubicundus", "all", "rub", "ba",
137 | },
138 | search: "chromodynamic",
139 | output: "chromodynamic",
140 | },
141 | {
142 | id: 6,
143 | input: []string{
144 | "alligator", "alien", "baloon", "chromodynamic", "romane", "romanus", "romulus", "rubens", "ruber",
145 | "rubicon", "rubicundus", "all", "rub", "ba",
146 | },
147 | search: "rubicundus",
148 | output: "rubicundus",
149 | },
150 | {
151 | id: 7,
152 | input: []string{
153 | "alligator", "alien", "baloon", "chromodynamic", "romane", "romanus", "romulus", "rubens", "ruber",
154 | "rubicon", "rubicundus", "all", "rub", "ba",
155 | },
156 | search: "rub",
157 | output: "rub",
158 | },
159 | {
160 | id: 8,
161 | input: []string{
162 | "alligator", "alien", "baloon", "chromodynamic", "romane", "romanus", "romulus", "rubens", "ruber",
163 | "rubicon", "rubicundus", "all", "rub", "ba",
164 | },
165 | search: "ba",
166 | output: "ba",
167 | },
168 | }
169 |
170 | for _, tc := range testCases {
171 | tree := NewNode("", false)
172 | for _, j := range tc.input {
173 | tree.Insert(j)
174 | }
175 |
176 | node, _ := tree.Search(tc.search)
177 | if tc.output == "" {
178 | assert.Nil(t, node, fmt.Sprintf("Failed test id: %d\n", tc.id))
179 | } else {
180 | assert.NotNil(t, node, fmt.Sprintf("Failed test id: %d\n", tc.id))
181 | assert.Equal(t, tc.output, node.fullValue, fmt.Sprintf("Failed test id: %d\n", tc.id))
182 | }
183 | }
184 | }
185 |
186 | func TestSearchWithRandomIds(t *testing.T) {
187 | randomIds := generateUUIDs()
188 | sort.Strings(randomIds)
189 | var testCases []*SearchTestCase
190 | for i, randId := range randomIds {
191 | testCases = append(
192 | testCases, &SearchTestCase{
193 | id: i, input: randomIds, search: randId, output: randId,
194 | },
195 | )
196 | }
197 |
198 | for _, tc := range testCases {
199 | tree := NewNode("", false)
200 | for _, j := range tc.input {
201 | tree.Insert(j)
202 | }
203 |
204 | node, _ := tree.Search(tc.search)
205 |
206 | assert.NotNil(t, node)
207 | assert.Equal(t, tc.output, node.fullValue)
208 | }
209 | }
210 |
211 | func generateUUIDs() []string {
212 | var res []string
213 | for i := 0; i < 1000; i++ {
214 | res = append(res, uuid.New().String())
215 | }
216 | return res
217 | }
218 |
219 | type HashmapImplementation struct {
220 | items map[string]string
221 | }
222 |
223 | func NewHashmapImplementation() *HashmapImplementation {
224 | return &HashmapImplementation{
225 | items: make(map[string]string),
226 | }
227 | }
228 |
229 | func (hs *HashmapImplementation) Insert(input string) {
230 | hs.items[input] = input
231 | }
232 |
233 | func (hs *HashmapImplementation) Search(input string) string {
234 | return hs.items[input]
235 | }
236 |
237 | func TestRouter(t *testing.T) {
238 | input := []string{
239 | "GET /",
240 | "GET /contact",
241 | "GET /api/widgets",
242 | "POST /api/widgets",
243 | "POST /api/widgets/:id",
244 | "POST /api/widgets/:id/parts",
245 | "POST /api/widgets/:id/parts/:partId/update",
246 | "POST /api/widgets/:id/parts/:partId/delete",
247 | "POST /:id",
248 | "POST /:id/admin",
249 | "POST /:id/image",
250 | "DELETE /users/:id",
251 | "DELETE /users/:id",
252 | "DELETE /users/:id/admin",
253 | "DELETE /images/:id",
254 | "GET /products/:category/:id",
255 | "PUT /customers/:customerId/orders/:orderId",
256 | "PATCH /articles/:articleId/comments/:commentId",
257 | "GET /search/:query/page/:pageNumber",
258 | "POST /upload/:fileType/:userId",
259 | }
260 | testCases := []SearchTestCase{
261 | {
262 | id: 1,
263 | input: input,
264 | search: "GET /",
265 | output: "GET /",
266 | params: map[string]string{},
267 | },
268 | {
269 | id: 2,
270 | input: input,
271 | search: "GET /contact",
272 | output: "GET /contact",
273 | params: map[string]string{},
274 | },
275 | {
276 | id: 3,
277 | input: input,
278 | search: "GET /api/widgets",
279 | output: "GET /api/widgets",
280 | params: map[string]string{},
281 | },
282 | {
283 | id: 4,
284 | input: input,
285 | search: "POST /api/widgets/123",
286 | output: "POST /api/widgets/:id",
287 | params: map[string]string{"id": "123"},
288 | },
289 | {
290 | id: 5,
291 | input: input,
292 | search: "POST /api/widgets/123/parts",
293 | output: "POST /api/widgets/:id/parts",
294 | params: map[string]string{"id": "123"},
295 | },
296 | {
297 | id: 6,
298 | input: input,
299 | search: "POST /api/widgets/123/parts/123",
300 | output: "POST /api/widgets/:id/parts/:partId",
301 | params: map[string]string{"id": "123", "partId": "123"},
302 | notFoundExpected: true,
303 | },
304 | {
305 | id: 7,
306 | input: input,
307 | search: "POST /api/widgets/123/parts/123/update",
308 | output: "POST /api/widgets/:id/parts/:partId/update",
309 | params: map[string]string{"id": "123", "partId": "123"},
310 | },
311 | {
312 | id: 8,
313 | input: input,
314 | search: "POST /api/widgets/123/parts/123/delete",
315 | output: "POST /api/widgets/:id/parts/:partId/delete",
316 | params: map[string]string{"id": "123", "partId": "123"},
317 | },
318 | {
319 | id: 9,
320 | input: input,
321 | search: "POST /123",
322 | output: "POST /:id",
323 | params: map[string]string{"id": "123"},
324 | },
325 | {
326 | id: 10,
327 | input: input,
328 | search: "POST /123/admin",
329 | output: "POST /:id/admin",
330 | params: map[string]string{"id": "123"},
331 | },
332 | {
333 | id: 11,
334 | input: input,
335 | search: "POST /123/image",
336 | output: "POST /:id/image",
337 | params: map[string]string{"id": "123"},
338 | },
339 | {
340 | id: 12,
341 | input: input,
342 | search: "POST /123/images",
343 | output: "",
344 | params: map[string]string{},
345 | notFoundExpected: true,
346 | },
347 | {
348 | id: 13,
349 | input: input,
350 | search: "GET /nonexistent",
351 | output: "",
352 | params: map[string]string{},
353 | notFoundExpected: true,
354 | },
355 | {
356 | id: 14,
357 | input: input,
358 | search: "POST /api/widgets/123/nonexistent",
359 | output: "",
360 | params: map[string]string{},
361 | notFoundExpected: true,
362 | },
363 | {
364 | id: 15,
365 | input: input,
366 | search: "PUT /api/widgets/123",
367 | output: "",
368 | params: map[string]string{},
369 | notFoundExpected: true,
370 | },
371 | {
372 | id: 16,
373 | input: input,
374 | search: "POST /api/widgets/123/parts/456/unknown",
375 | output: "",
376 | params: map[string]string{},
377 | notFoundExpected: true,
378 | },
379 | {
380 | id: 18,
381 | input: input,
382 | search: "POST /api/widgets/very-long-slug-with-dashes",
383 | output: "POST /api/widgets/:id",
384 | params: map[string]string{"id": "very-long-slug-with-dashes"},
385 | },
386 | {
387 | id: 19,
388 | input: input,
389 | search: "POST /api/widgets/123-456/parts/789-abc/update",
390 | output: "POST /api/widgets/:id/parts/:partId/update",
391 | params: map[string]string{"id": "123-456", "partId": "789-abc"},
392 | },
393 | {
394 | id: 20,
395 | input: input,
396 | search: "POST /api/widgets/123_456/parts/789_abc/delete",
397 | output: "POST /api/widgets/:id/parts/:partId/delete",
398 | params: map[string]string{"id": "123_456", "partId": "789_abc"},
399 | },
400 | {
401 | id: 21,
402 | input: input,
403 | search: "POST /complex-slug-with-numbers-123",
404 | output: "POST /:id",
405 | params: map[string]string{"id": "complex-slug-with-numbers-123"},
406 | },
407 | {
408 | id: 22,
409 | input: input,
410 | search: "POST /UpperCaseSlug/admin",
411 | output: "POST /:id/admin",
412 | params: map[string]string{"id": "UpperCaseSlug"},
413 | },
414 | {
415 | id: 23,
416 | input: input,
417 | search: "GET /api/widgets/",
418 | output: "",
419 | notFoundExpected: true,
420 | },
421 | {
422 | id: 24,
423 | input: input,
424 | search: "DELETE /users",
425 | output: "",
426 | notFoundExpected: true,
427 | },
428 | {
429 | id: 25,
430 | input: input,
431 | search: "DELETE /users/123",
432 | output: "DELETE /users/:id",
433 | params: map[string]string{"id": "123"},
434 | },
435 | {
436 | id: 26,
437 | input: input,
438 | search: "DELETE /users/123",
439 | output: "DELETE /users/:id",
440 | params: map[string]string{"id": "123"},
441 | },
442 | {
443 | id: 27,
444 | input: input,
445 | search: "DELETE /users/123/admin",
446 | output: "DELETE /users/:id/admin",
447 | params: map[string]string{"id": "123"},
448 | },
449 | {
450 | id: 28,
451 | input: input,
452 | search: "DELETE /images/123",
453 | output: "DELETE /images/:id",
454 | params: map[string]string{"id": "123"},
455 | },
456 | {
457 | id: 29,
458 | input: input,
459 | search: "DELETE /images/123",
460 | output: "DELETE /images/:id",
461 | params: map[string]string{"id": "123"},
462 | },
463 | {
464 | id: 30,
465 | input: input,
466 | search: "GET /products/electronics/laptop-123",
467 | output: "GET /products/:category/:id",
468 | params: map[string]string{"category": "electronics", "id": "laptop-123"},
469 | },
470 | {
471 | id: 31,
472 | input: input,
473 | search: "PUT /customers/cust-456/orders/order-789",
474 | output: "PUT /customers/:customerId/orders/:orderId",
475 | params: map[string]string{"customerId": "cust-456", "orderId": "order-789"},
476 | },
477 | {
478 | id: 32,
479 | input: input,
480 | search: "PATCH /articles/art-101/comments/comment-202",
481 | output: "PATCH /articles/:articleId/comments/:commentId",
482 | params: map[string]string{"articleId": "art-101", "commentId": "comment-202"},
483 | },
484 | {
485 | id: 33,
486 | input: input,
487 | search: "GET /search/golang/page/2",
488 | output: "GET /search/:query/page/:pageNumber",
489 | params: map[string]string{"query": "golang", "pageNumber": "2"},
490 | },
491 | {
492 | id: 34,
493 | input: input,
494 | search: "POST /upload/image/user-303",
495 | output: "POST /upload/:fileType/:userId",
496 | params: map[string]string{"fileType": "image", "userId": "user-303"},
497 | },
498 | }
499 |
500 | for _, tc := range testCases {
501 | tree := NewNode("", false)
502 | for _, j := range tc.input {
503 | tree.Insert(j)
504 | }
505 | result, params := tree.Search(tc.search)
506 | if tc.notFoundExpected {
507 | assert.Nil(t, result, fmt.Sprintf("Test id %d failed: expected nil result, but got %v", tc.id, result))
508 | } else {
509 | assert.NotNil(t, result, fmt.Sprintf("Test id %d failed: expected non-nil result, but got nil", tc.id))
510 | assert.NotNil(t, params, fmt.Sprintf("Test id %d failed: expected non-nil params, but got nil", tc.id))
511 | assert.Equal(t, tc.output, result.fullValue, fmt.Sprintf("Test id %d failed: expected output %s, but got %s", tc.id, tc.output, result.fullValue))
512 | assert.Equal(t, tc.params, params, fmt.Sprintf("Test id %d failed: expected params %v, but got %v", tc.id, tc.params, params))
513 | }
514 | }
515 | }
516 |
517 | func TestSearchPetStoreAPI(t *testing.T) {
518 | input := []string{
519 | "POST /pet",
520 | "PUT /pet",
521 | "GET /pet/findByStatus",
522 | "GET /pet/findByTags",
523 | "GET /pet/:petId",
524 | "POST /pet/:petId",
525 | "DELETE /pet/:petId",
526 | "POST /pet/:petId/uploadImage",
527 | "GET /store/inventory",
528 | "POST /store/order",
529 | "GET /store/order/:orderId",
530 | "DELETE /store/order/:orderId",
531 | "POST /user",
532 | "POST /user/createWithList",
533 | "GET /user/login",
534 | "GET /user/logout",
535 | "GET /user/:username",
536 | "PUT /user/:username",
537 | "DELETE /user/:username",
538 | }
539 | testCases := []SearchTestCase{
540 | {
541 | id: 1,
542 | input: input,
543 | search: "POST /pet",
544 | output: "POST /pet",
545 | params: map[string]string{},
546 | },
547 | {
548 | id: 2,
549 | input: input,
550 | search: "PUT /pet",
551 | output: "PUT /pet",
552 | params: map[string]string{},
553 | },
554 | {
555 | id: 3,
556 | input: input,
557 | search: "GET /pet/findByStatus",
558 | output: "GET /pet/findByStatus",
559 | params: map[string]string{},
560 | },
561 | {
562 | id: 4,
563 | input: input,
564 | search: "GET /pet/findByTags",
565 | output: "GET /pet/findByTags",
566 | params: map[string]string{},
567 | },
568 | {
569 | id: 5,
570 | input: input,
571 | search: "GET /pet/123",
572 | output: "GET /pet/:petId",
573 | params: map[string]string{"petId": "123"},
574 | },
575 | {
576 | id: 6,
577 | input: input,
578 | search: "POST /pet/456",
579 | output: "POST /pet/:petId",
580 | params: map[string]string{"petId": "456"},
581 | },
582 | {
583 | id: 7,
584 | input: input,
585 | search: "DELETE /pet/789",
586 | output: "DELETE /pet/:petId",
587 | params: map[string]string{"petId": "789"},
588 | },
589 | {
590 | id: 8,
591 | input: input,
592 | search: "POST /pet/101/uploadImage",
593 | output: "POST /pet/:petId/uploadImage",
594 | params: map[string]string{"petId": "101"},
595 | },
596 | {
597 | id: 9,
598 | input: input,
599 | search: "GET /store/inventory",
600 | output: "GET /store/inventory",
601 | params: map[string]string{},
602 | },
603 | {
604 | id: 10,
605 | input: input,
606 | search: "POST /store/order",
607 | output: "POST /store/order",
608 | params: map[string]string{},
609 | },
610 | {
611 | id: 11,
612 | input: input,
613 | search: "GET /store/order/202",
614 | output: "GET /store/order/:orderId",
615 | params: map[string]string{"orderId": "202"},
616 | },
617 | {
618 | id: 12,
619 | input: input,
620 | search: "DELETE /store/order/303",
621 | output: "DELETE /store/order/:orderId",
622 | params: map[string]string{"orderId": "303"},
623 | },
624 | {
625 | id: 13,
626 | input: input,
627 | search: "POST /user",
628 | output: "POST /user",
629 | params: map[string]string{},
630 | },
631 | {
632 | id: 14,
633 | input: input,
634 | search: "POST /user/createWithList",
635 | output: "POST /user/createWithList",
636 | params: map[string]string{},
637 | },
638 | {
639 | id: 15,
640 | input: input,
641 | search: "GET /user/login",
642 | output: "GET /user/login",
643 | params: map[string]string{},
644 | },
645 | {
646 | id: 16,
647 | input: input,
648 | search: "GET /user/logout",
649 | output: "GET /user/logout",
650 | params: map[string]string{},
651 | },
652 | {
653 | id: 17,
654 | input: input,
655 | search: "GET /user/johndoe",
656 | output: "GET /user/:username",
657 | params: map[string]string{"username": "johndoe"},
658 | },
659 | {
660 | id: 18,
661 | input: input,
662 | search: "PUT /user/janedoe",
663 | output: "PUT /user/:username",
664 | params: map[string]string{"username": "janedoe"},
665 | },
666 | {
667 | id: 19,
668 | input: input,
669 | search: "DELETE /user/testuser",
670 | output: "DELETE /user/:username",
671 | params: map[string]string{"username": "testuser"},
672 | },
673 | {
674 | id: 20,
675 | input: input,
676 | search: "GET /nonexistent/path",
677 | notFoundExpected: true,
678 | },
679 | }
680 |
681 | for _, tc := range testCases {
682 | tree := NewNode("", false)
683 | for _, j := range tc.input {
684 | tree.Insert(j)
685 | }
686 | result, params := tree.Search(tc.search)
687 | if tc.notFoundExpected {
688 | assert.Nil(t, result, fmt.Sprintf("Test id %d failed: expected nil result, but got %v", tc.id, result))
689 | } else {
690 | assert.NotNil(t, result, fmt.Sprintf("Test id %d failed: expected non-nil result, but got nil", tc.id))
691 | assert.NotNil(t, params, fmt.Sprintf("Test id %d failed: expected non-nil params, but got nil", tc.id))
692 | assert.Equal(t, tc.output, result.fullValue, fmt.Sprintf("Test id %d failed: expected output %s, but got %s", tc.id, tc.output, result.fullValue))
693 | assert.Equal(t, tc.params, params, fmt.Sprintf("Test id %d failed: expected params %v, but got %v", tc.id, tc.params, params))
694 | }
695 | }
696 | }
697 |
698 | func TestInsertConflictParams(t *testing.T) {
699 | tree := NewNode("", false)
700 |
701 | tree.Insert("GET /users/:id")
702 | n := tree.Insert("GET /users/:username")
703 |
704 | assert.Nil(t, n)
705 | }
706 |
707 | func TestSearchConflictStaticAndParam(t *testing.T) {
708 | tree := NewNode("", false)
709 |
710 | tree.Insert("GET /users/:id")
711 | tree.Insert("GET /users/history")
712 |
713 | result, _ := tree.Search("GET /users/history")
714 | assert.NotNil(t, result)
715 | assert.Equal(t, "GET /users/history", result.fullValue)
716 |
717 | result, _ = tree.Search("GET /users/123")
718 | assert.NotNil(t, result)
719 | assert.Equal(t, "GET /users/:id", result.fullValue)
720 | }
721 |
--------------------------------------------------------------------------------