├── .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 | LambdaMux logo 3 |

4 |

5 | 6 | Test 7 | 8 | 9 | GoDoc 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 | --------------------------------------------------------------------------------