├── .github ├── .release-please-manifest.json ├── dependabot.yml ├── release-please-config.json └── workflows │ ├── benchmark.yml │ ├── ci.yml │ ├── codeql.yml │ ├── dependabot.yml │ ├── pr-title.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Taskfile.yml ├── api.go ├── api_test.go ├── benchmarks ├── binding_test.go ├── decoder_test.go ├── go.mod ├── go.sum └── http_test.go ├── decoder ├── adapters.go ├── adapters_test.go ├── compile.go ├── decode.go └── decode_test.go ├── encoding ├── decode.go ├── decode_test.go ├── encode.go ├── encode_test.go ├── form │ ├── form.go │ └── form_test.go ├── json │ ├── json.go │ └── json_test.go ├── msgpack │ ├── msgpack.go │ └── msgpack_test.go ├── protobuf │ ├── protobuf.go │ ├── protobuf_test.go │ └── testdata │ │ ├── test.pb.go │ │ └── test.proto ├── text │ ├── decode.go │ ├── decode_test.go │ ├── encode.go │ ├── encode_test.go │ ├── text.go │ └── text_test.go ├── toml │ ├── toml.go │ └── toml_test.go ├── xml │ ├── xml.go │ └── xml_test.go └── yaml │ ├── yaml.go │ └── yaml_test.go ├── errors.go ├── errors_test.go ├── examples └── simple │ └── main.go ├── export_test.go ├── go.mod ├── go.sum ├── group.go ├── group_test.go ├── handler.go ├── handler_test.go ├── internal ├── byteconv │ ├── byteconv.go │ └── byteconv_test.go └── test │ ├── encoding.go │ └── router.go ├── nilcheck.go ├── nilcheck_test.go ├── pkg └── httptest │ └── request.go ├── pool.go ├── pool_test.go ├── request.go ├── scripts └── pre-commit.sh └── unsafe.go /.github/.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { ".": "0.2.1" } 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: gomod 10 | directory: benchmarks 11 | schedule: 12 | interval: daily 13 | open-pull-requests-limit: 10 14 | 15 | - package-ecosystem: github-actions 16 | directory: / 17 | schedule: 18 | interval: daily 19 | open-pull-requests-limit: 10 20 | -------------------------------------------------------------------------------- /.github/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "release-type": "go", 3 | "bump-minor-pre-major": true, 4 | "bump-patch-for-minor-pre-major": true, 5 | "packages": { 6 | ".": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | pull-requests: write 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | packages: 16 | name: Changed packages 17 | runs-on: ubuntu-latest 18 | outputs: 19 | matrix: ${{ steps.set-matrix.outputs.matrix }} 20 | steps: 21 | - name: Find packages with changes 22 | id: packages 23 | uses: tj-actions/changed-files@v46 24 | with: 25 | files: '**/*.go' 26 | dir_names: true 27 | json: true 28 | 29 | - name: Set output 30 | id: set-matrix 31 | run: | 32 | echo "matrix={\"package\":${{ steps.packages.outputs.all_changed_and_modified_files }}}" >> $GITHUB_OUTPUT 33 | 34 | benchmark: 35 | name: Benchmark ${{ matrix.package }} 36 | needs: packages 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: ${{ fromJSON(needs.packages.outputs.matrix) }} 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | - name: Setup Go 45 | uses: actions/setup-go@v5 46 | with: 47 | go-version-file: go.mod 48 | 49 | - name: Run benchmark 50 | run: go test -run ^$ -bench=. -count=10 -benchmem ./${{ matrix.package }} | tee after 51 | 52 | - name: Run benchmark for base code 53 | run: | 54 | git fetch --quiet origin master ${{ github.event.pull_request.base.sha }} 55 | git reset --quiet --hard ${{ github.event.pull_request.base.sha }} 56 | go test -run ^$ -bench=. -count=10 -benchmem ./${{ matrix.package }} | tee before 57 | 58 | - name: Compare benchmarks 59 | id: bench 60 | run: | 61 | go install golang.org/x/perf/cmd/benchstat@latest 62 | OUTPUT=$(benchstat before after) 63 | echo "${OUTPUT}" 64 | echo "diff<> $GITHUB_OUTPUT && echo "$OUTPUT" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT 65 | 66 | - name: Save benchmark results 67 | uses: cloudposse/github-action-matrix-outputs-write@1.0.0 68 | if: steps.bench.outputs.diff != '' 69 | with: 70 | matrix-step-name: ${{ github.job }} 71 | matrix-key: ${{ matrix.package }} 72 | outputs: ${{ toJSON(steps.bench.outputs) }} 73 | 74 | comment: 75 | name: Comment 76 | needs: benchmark 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Load benchmark results 80 | uses: cloudposse/github-action-matrix-outputs-read@1.0.0 81 | id: read 82 | with: 83 | matrix-step-name: benchmark 84 | 85 | - name: Generate comment text 86 | uses: actions/github-script@v7 87 | if: steps.read.outputs.result != '{}' 88 | id: parse 89 | with: 90 | result-encoding: string 91 | script: | 92 | const result = ${{ steps.read.outputs.result }} 93 | return Object.keys(result.diff).sort().map((key) => ` 94 |
${key} 95 | 96 | ` + "```" + ` 97 | ${result.diff[key]} 98 | ` + "```" + ` 99 |
100 | `).join('') 101 | 102 | - name: Create comment 103 | if: steps.parse.outputs.result != '' 104 | uses: marocchino/sticky-pull-request-comment@v2 105 | with: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | header: benchmarks 108 | message: | 109 | ### Benchmark Results 110 | ${{ steps.parse.outputs.result }} 111 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | 25 | - name: Run tests 26 | run: go test ./... -coverprofile coverage.out 27 | 28 | - name: Upload coverage reports to Codecov 29 | uses: codecov/codecov-action@v5 30 | env: 31 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 32 | 33 | lint: 34 | name: Lint 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | - name: Setup Go 41 | uses: actions/setup-go@v5 42 | with: 43 | go-version-file: go.mod 44 | 45 | - name: Run golangci-lint 46 | uses: golangci/golangci-lint-action@v8 47 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: '25 9 * * 4' 10 | 11 | permissions: 12 | actions: read 13 | contents: read 14 | security-events: write 15 | 16 | jobs: 17 | analyze: 18 | name: Analyze 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v3 27 | with: 28 | languages: go 29 | 30 | - name: Perform CodeQL Analysis 31 | uses: github/codeql-action/analyze@v3 32 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | merge: 13 | name: Merge 14 | runs-on: ubuntu-latest 15 | if: github.actor == 'dependabot[bot]' 16 | steps: 17 | - uses: fastify/github-action-merge-dependabot@v3 18 | with: 19 | target: patch 20 | use-github-auto-merge: true 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: PR Title 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - edited 9 | - synchronize 10 | 11 | permissions: 12 | pull-requests: read 13 | 14 | jobs: 15 | validate: 16 | name: Validate 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Validate PR title 20 | uses: amannn/action-semantic-pull-request@v5 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Release 17 | uses: GoogleCloudPlatform/release-please-action@v4 18 | with: 19 | config-file: .github/release-please-config.json 20 | manifest-file: .github/.release-please-manifest.json 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Test binary, built with `go test -c` 2 | *.test 3 | 4 | # Output of the go coverage tool 5 | *.out 6 | 7 | # Dependency directories 8 | vendor/ 9 | 10 | # Taskfile checksums directory 11 | .task/ 12 | 13 | # Go workspace 14 | go.work 15 | go.work.sum 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | linters: 3 | default: all 4 | disable: 5 | - depguard 6 | - exhaustive 7 | - exhaustruct 8 | - gochecknoglobals 9 | - gochecknoinits 10 | - godox 11 | - ireturn 12 | - mnd 13 | - nilnil 14 | - nlreturn 15 | - paralleltest 16 | - varnamelen 17 | - wrapcheck 18 | - wsl 19 | settings: 20 | gomodguard: 21 | blocked: 22 | modules: 23 | - encoding/json: 24 | recommendations: [github.com/goccy/go-json] 25 | - github.com/pkg/errors: 26 | recommendations: [errors] 27 | govet: 28 | enable-all: true 29 | nolintlint: 30 | require-specific: true 31 | exclusions: 32 | generated: lax 33 | presets: 34 | - common-false-positives 35 | - legacy 36 | rules: 37 | - linters: 38 | - cyclop 39 | - err113 40 | - forcetypeassert 41 | - funlen 42 | - goconst 43 | - gosec 44 | path: (.+)_test.go 45 | - linters: [govet] 46 | path: ((.+)_test.go|internal/test/(.+)) 47 | text: '^fieldalignment:' 48 | - linters: [govet] 49 | text: '^shadow: declaration of "err" shadows declaration' 50 | - linters: [revive] 51 | path: ((.+)_test.go|internal/test/(.+)) 52 | text: '^unused-parameter:' 53 | - linters: [revive] 54 | text: '^(exported|package-comments): .*$' 55 | 56 | formatters: 57 | enable: 58 | - gci 59 | - gofmt 60 | - gofumpt 61 | - goimports 62 | settings: 63 | gofmt: 64 | rewrite-rules: 65 | - pattern: interface{} 66 | replacement: any 67 | gofumpt: 68 | extra-rules: true 69 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.1](https://github.com/abemedia/go-don/compare/v0.2.0...v0.2.1) (2023-06-04) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * content-length is 0 when responding with nil value ([#148](https://github.com/abemedia/go-don/issues/148)) ([9cf40b2](https://github.com/abemedia/go-don/commit/9cf40b2c8a36468d072c3b17cebb2dc745c0e520)) 9 | 10 | 11 | ### Performance Improvements 12 | 13 | * **encoding/text:** reduce allocs ([#142](https://github.com/abemedia/go-don/issues/142)) ([5759715](https://github.com/abemedia/go-don/commit/575971580296f0acaa929c6e849bbe707e580165)) 14 | * improve pool performance for pointer types ([#147](https://github.com/abemedia/go-don/issues/147)) ([d9464de](https://github.com/abemedia/go-don/commit/d9464deb560eac1500b51f45c5cfe64822efed63)) 15 | 16 | ## [0.2.0](https://github.com/abemedia/go-don/compare/v0.1.4...v0.2.0) (2023-05-14) 17 | 18 | 19 | ### ⚠ BREAKING CHANGES 20 | 21 | * remove Empty type (use any instead) ([#135](https://github.com/abemedia/go-don/issues/135)) 22 | * move encoding logic to sub-package ([#111](https://github.com/abemedia/go-don/issues/111)) 23 | 24 | ### Features 25 | 26 | * **encoding:** support more media type aliases ([#115](https://github.com/abemedia/go-don/issues/115)) ([d88115c](https://github.com/abemedia/go-don/commit/d88115c058e6d81c9fd0ec1d27d55bd44b4cf8e6)) 27 | * **encoding:** support protocol buffers ([#117](https://github.com/abemedia/go-don/issues/117)) ([ace6006](https://github.com/abemedia/go-don/commit/ace600620fbe9c67e56ecfb1b7394536cc1da0a4)) 28 | * **encoding:** support toml ([#114](https://github.com/abemedia/go-don/issues/114)) ([e95b4ae](https://github.com/abemedia/go-don/commit/e95b4aed2a43c5bd87dbf3bb4591faf0d0fd3c97)) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * no content issues on error or pointer to Empty ([#125](https://github.com/abemedia/go-don/issues/125)) ([fa50d36](https://github.com/abemedia/go-don/commit/fa50d363e872d51baeed84cb516d6d4a45fc345b)) 34 | 35 | 36 | ### Performance Improvements 37 | 38 | * **encoding/protobuf:** cache reflection results ([#138](https://github.com/abemedia/go-don/issues/138)) ([99e0cea](https://github.com/abemedia/go-don/commit/99e0cea46d5e42e91dda63bfdd365835161a9a03)) 39 | * **encoding:** improve encoding performance ([#113](https://github.com/abemedia/go-don/issues/113)) ([a541544](https://github.com/abemedia/go-don/commit/a541544614d07121266a2ebf1eebfd75b9d7541d)) 40 | * reuse requests to reduce allocs ([#127](https://github.com/abemedia/go-don/issues/127)) ([827209b](https://github.com/abemedia/go-don/commit/827209bca6cfa7a91c414f6bced4a10308d9573f)) 41 | 42 | 43 | ### Code Refactoring 44 | 45 | * move encoding logic to sub-package ([#111](https://github.com/abemedia/go-don/issues/111)) ([8f50031](https://github.com/abemedia/go-don/commit/8f50031717f53348d31619b96411dcbf60e1e6fc)) 46 | * remove Empty type (use any instead) ([#135](https://github.com/abemedia/go-don/issues/135)) ([72848e8](https://github.com/abemedia/go-don/commit/72848e8389c67f4443a1f99fc1e4a8610c831b65)) 47 | 48 | ## [0.1.4](https://github.com/abemedia/go-don/compare/v0.1.3...v0.1.4) (2023-05-09) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * error parsing GET request ([#108](https://github.com/abemedia/go-don/issues/108)) ([8298a2c](https://github.com/abemedia/go-don/commit/8298a2c7a8d46858420fccbbe39909db71838b38)) 54 | 55 | ## [0.1.3](https://github.com/abemedia/go-don/compare/v0.1.2...v0.1.3) (2023-05-08) 56 | 57 | 58 | ### Features 59 | 60 | * api serve ([#98](https://github.com/abemedia/go-don/issues/98)) ([602b24c](https://github.com/abemedia/go-don/commit/602b24c5220bee9955d30ec38e7fbc8b41aa2e10)) 61 | * implement errors interfaces & improve tests ([#91](https://github.com/abemedia/go-don/issues/91)) ([0a282f0](https://github.com/abemedia/go-don/commit/0a282f0fc2fbe289a89fd9cc0ba94939108fb205)) 62 | 63 | 64 | ### Bug Fixes 65 | 66 | * decoder panics on tag not found ([#100](https://github.com/abemedia/go-don/issues/100)) ([3a73c35](https://github.com/abemedia/go-don/commit/3a73c35dd996e1035360733d4b60d52b88c3243b)) 67 | 68 | 69 | ### Performance Improvements 70 | 71 | * improve encoding performance ([#104](https://github.com/abemedia/go-don/issues/104)) ([9dbebfa](https://github.com/abemedia/go-don/commit/9dbebfa81db3277efd964d6d8fa9f1755ef9683a)) 72 | 73 | ## [0.1.2](https://github.com/abemedia/go-don/compare/v0.1.1...v0.1.2) (2023-05-05) 74 | 75 | 76 | ### Features 77 | 78 | * improve error handling ([#85](https://github.com/abemedia/go-don/issues/85)) ([3f976fc](https://github.com/abemedia/go-don/commit/3f976fca67e518b9c786c4af32c46586fd5cdc06)) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * panic on interface request ([#82](https://github.com/abemedia/go-don/issues/82)) ([8e83bd6](https://github.com/abemedia/go-don/commit/8e83bd692db5569b36426b112d4d243cc106968a)) 84 | 85 | ## [0.1.1](https://github.com/abemedia/go-don/compare/v0.1.0...v0.1.1) (2023-01-14) 86 | 87 | 88 | ### Features 89 | 90 | * better error handling, minor refactor ([#58](https://github.com/abemedia/go-don/issues/58)) ([0de3fc3](https://github.com/abemedia/go-don/commit/0de3fc32deb4692a7e768f1f650122b664785810)) 91 | * **encoding/text:** support marshaler & stringer, improve performance ([#60](https://github.com/abemedia/go-don/issues/60)) ([b7bffe8](https://github.com/abemedia/go-don/commit/b7bffe81d2ca0651a78d694462e6684df211f0ca)) 92 | 93 | ## 0.1.0 (2022-08-29) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * **encoding/text:** int conversion on 32bit ([#32](https://github.com/abemedia/go-don/issues/32)) ([3e469fe](https://github.com/abemedia/go-don/commit/3e469fe24189849d25e24395500eca23d6043a96)) 99 | * use error's marshaler, circular ref in Handler, lint issues ([#28](https://github.com/abemedia/go-don/issues/28)) ([f8b32ea](https://github.com/abemedia/go-don/commit/f8b32eaa0150d96a6ce186f2bdf41ef0e90a39e0)) 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Adam Bouqdib 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Don - Go API Framework 2 | 3 | [![GoDoc](https://pkg.go.dev/badge/github.com/abemedia/go-don)](https://pkg.go.dev/github.com/abemedia/go-don) 4 | [![Codecov](https://codecov.io/gh/abemedia/go-don/branch/master/graph/badge.svg)](https://codecov.io/gh/abemedia/go-don) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/abemedia/go-don)](https://goreportcard.com/report/github.com/abemedia/go-don) 6 | 7 | Don is a fast & simple API framework written in Go. It features a super-simple API and thanks to 8 | [fasthttp](https://github.com/valyala/fasthttp) and a custom version of 9 | [httprouter](https://github.com/abemedia/httprouter) it's blazing fast and has a low memory 10 | footprint. 11 | 12 | As Don uses Go generics it requires Go 1.18 to work. 13 | Minor version updates should be considered breaking changes. 14 | 15 | ## Contents 16 | 17 | - [Overview](#don---go-api-framework) 18 | - [Basic Example](#basic-example) 19 | - [Configuration](#configuration) 20 | - [Support multiple formats](#support-multiple-formats) 21 | - [Currently supported formats](#currently-supported-formats) 22 | - [Adding custom encoding](#adding-custom-encoding) 23 | - [Request parsing](#request-parsing) 24 | - [Headers & response codes](#headers--response-codes) 25 | - [Sub-routers](#sub-routers) 26 | - [Middleware](#middleware) 27 | - [Benchmarks](#benchmarks) 28 | 29 | ## Basic Example 30 | 31 | ```go 32 | package main 33 | 34 | import ( 35 | "context" 36 | "errors" 37 | "fmt" 38 | "net/http" 39 | 40 | "github.com/abemedia/go-don" 41 | _ "github.com/abemedia/go-don/encoding/json" // Enable JSON parsing & rendering. 42 | _ "github.com/abemedia/go-don/encoding/yaml" // Enable YAML parsing & rendering. 43 | ) 44 | 45 | type GreetRequest struct { 46 | Name string `path:"name"` // Get name from the URL path. 47 | Age int `header:"X-User-Age"` // Get age from HTTP header. 48 | } 49 | 50 | type GreetResponse struct { 51 | // Remember to add all the tags for the renderers you enable. 52 | Greeting string `json:"data" yaml:"data"` 53 | } 54 | 55 | func Greet(ctx context.Context, req GreetRequest) (*GreetResponse, error) { 56 | if req.Name == "" { 57 | return nil, don.Error(errors.New("missing name"), http.StatusBadRequest) 58 | } 59 | 60 | res := &GreetResponse{ 61 | Greeting: fmt.Sprintf("Hello %s, you're %d years old.", req.Name, req.Age), 62 | } 63 | 64 | return res, nil 65 | } 66 | 67 | func Pong(context.Context, any) (string, error) { 68 | return "pong", nil 69 | } 70 | 71 | func main() { 72 | r := don.New(nil) 73 | r.Get("/ping", don.H(Pong)) // Handlers are wrapped with `don.H`. 74 | r.Post("/greet/:name", don.H(Greet)) 75 | r.ListenAndServe(":8080") 76 | } 77 | ``` 78 | 79 | ## Configuration 80 | 81 | Don is configured by passing in the `Config` struct to `don.New`. 82 | 83 | ```go 84 | r := don.New(&don.Config{ 85 | DefaultEncoding: "application/json", 86 | DisableNoContent: false, 87 | }) 88 | ``` 89 | 90 | ### DefaultEncoding 91 | 92 | Set this to the format you'd like to use if no `Content-Type` or `Accept` headers are in the 93 | request. 94 | 95 | ### DisableNoContent 96 | 97 | If you return `nil` from your handler, Don will respond with an empty body and a `204 No Content` 98 | status code. Set this to `true` to disable that behaviour. 99 | 100 | ## Support multiple formats 101 | 102 | Support multiple request & response formats without writing extra parsing or rendering code. The API 103 | uses the `Content-Type` and `Accept` headers to determine what input and output encoding to use. 104 | 105 | You can mix multiple formats, for example if the `Content-Type` header is set to `application/json`, 106 | however the `Accept` header is set to `application/x-yaml`, then the request will be parsed as JSON, 107 | and the response will be YAML encoded. 108 | 109 | If no `Content-Type` or `Accept` header is passed the default will be used. 110 | 111 | Formats need to be explicitly imported e.g. 112 | 113 | ```go 114 | import _ "github.com/abemedia/go-don/encoding/yaml" 115 | ``` 116 | 117 | ### Currently supported formats 118 | 119 | #### JSON 120 | 121 | MIME: `application/json` 122 | 123 | Parses JSON requests & encodes responses as JSON. Use the `json` tag in your request & response 124 | structs. 125 | 126 | #### XML 127 | 128 | MIME: `application/xml`, `text/xml` 129 | 130 | Parses XML requests & encodes responses as XML. Use the `xml` tag in your request & response 131 | structs. 132 | 133 | #### YAML 134 | 135 | MIME: `application/yaml`, `text/yaml`, `application/x-yaml`, `text/x-yaml`, `text/vnd.yaml` 136 | 137 | Parses YAML requests & encodes responses as YAML. Use the `yaml` tag in your request & response 138 | structs. 139 | 140 | #### Form (input only) 141 | 142 | MIME: `application/x-www-form-urlencoded`, `multipart/form-data` 143 | 144 | Parses form data requests. Use the `form` tag in your request struct. 145 | 146 | #### Text 147 | 148 | MIME: `text/plain` 149 | 150 | Parses non-struct requests and encodes non-struct responses e.g. `string`, `int`, `bool` etc. 151 | 152 | ```go 153 | func MyHandler(ctx context.Context, req int64) (string, error) { 154 | // ... 155 | } 156 | ``` 157 | 158 | If the request is a struct and the `Content-Type` header is set to `text/plain` it returns a 159 | `415 Unsupported Media Type` error. 160 | 161 | #### MessagePack 162 | 163 | MIME: `application/msgpack`, `application/x-msgpack`, `application/vnd.msgpack` 164 | 165 | Parses MessagePack requests & encodes responses as MessagePack. Use the `msgpack` tag in your 166 | request & response structs. 167 | 168 | #### TOML 169 | 170 | MIME: `application/toml` 171 | 172 | Parses TOML requests & encodes responses as TOML. Use the `toml` tag in your request & response 173 | structs. 174 | 175 | #### Protocol Buffers 176 | 177 | MIME: `application/protobuf`, `application/x-protobuf` 178 | 179 | Parses protobuf requests & encodes responses as protobuf. Use pointer types generated with `protoc` 180 | as your request & response structs. 181 | 182 | ### Adding custom encoding 183 | 184 | Adding your own is easy. See [encoding/json/json.go](./encoding/json/json.go). 185 | 186 | ## Request parsing 187 | 188 | Automatically unmarshals values from headers, URL query, URL path & request body into your request 189 | struct. 190 | 191 | ```go 192 | type MyRequest struct { 193 | // Get from the URL path. 194 | ID int64 `path:"id"` 195 | 196 | // Get from the URL query. 197 | Filter string `query:"filter"` 198 | 199 | // Get from the JSON, YAML, XML or form body. 200 | Content float64 `form:"bar" json:"bar" yaml:"bar" xml:"bar"` 201 | 202 | // Get from the HTTP header. 203 | Lang string `header:"Accept-Language"` 204 | } 205 | ``` 206 | 207 | Please note that using a pointer as the request type negatively affects performance. 208 | 209 | ## Headers & response codes 210 | 211 | Implement the `StatusCoder` and `Headerer` interfaces to customise headers and response codes. 212 | 213 | ```go 214 | type MyResponse struct { 215 | Foo string `json:"foo"` 216 | } 217 | 218 | // Set a custom HTTP response code. 219 | func (nr *MyResponse) StatusCode() int { 220 | return 201 221 | } 222 | 223 | // Add custom headers to the response. 224 | func (nr *MyResponse) Header() http.Header { 225 | header := http.Header{} 226 | header.Set("foo", "bar") 227 | return header 228 | } 229 | ``` 230 | 231 | ## Sub-routers 232 | 233 | You can create sub-routers using the `Group` function: 234 | 235 | ```go 236 | r := don.New(nil) 237 | sub := r.Group("/api") 238 | sub.Get("/hello", don.H(Hello)) 239 | ``` 240 | 241 | ## Middleware 242 | 243 | Don uses the standard fasthttp middleware format of 244 | `func(fasthttp.RequestHandler) fasthttp.RequestHandler`. 245 | 246 | For example: 247 | 248 | ```go 249 | func loggingMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler { 250 | return func(ctx *fasthttp.RequestCtx) { 251 | log.Println(string(ctx.RequestURI())) 252 | next(ctx) 253 | } 254 | } 255 | ``` 256 | 257 | It is registered on a router using `Use` e.g. 258 | 259 | ```go 260 | r := don.New(nil) 261 | r.Post("/", don.H(handler)) 262 | r.Use(loggingMiddleware) 263 | ``` 264 | 265 | Middleware registered on a group only applies to routes in that group and child groups. 266 | 267 | ```go 268 | r := don.New(nil) 269 | r.Get("/login", don.H(loginHandler)) 270 | r.Use(loggingMiddleware) // applied to all routes 271 | 272 | api := r.Group("/api") 273 | api.Get("/hello", don.H(helloHandler)) 274 | api.Use(authMiddleware) // applied to routes `/api/hello` and `/api/v2/bye` 275 | 276 | 277 | v2 := api.Group("/v2") 278 | v2.Get("/bye", don.H(byeHandler)) 279 | v2.Use(corsMiddleware) // only applied to `/api/v2/bye` 280 | 281 | ``` 282 | 283 | To pass values from the middleware to the handler extend the context e.g. 284 | 285 | ```go 286 | func myMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler { 287 | return func(ctx *fasthttp.RequestCtx) { 288 | ctx.SetUserValue(ContextUserKey, "my_user") 289 | next(ctx) 290 | } 291 | } 292 | ``` 293 | 294 | This can now be accessed in the handler: 295 | 296 | ```go 297 | user := ctx.Value(ContextUserKey).(string) 298 | ``` 299 | 300 | ## Benchmarks 301 | 302 | To give you a rough idea of Don's performance, here is a comparison with Gin. 303 | 304 | ### Request Parsing 305 | 306 | Don has extremely fast & efficient binding of request data. 307 | 308 | | Benchmark name | (1) | (2) | (3) | (4) | 309 | | ------------------------ | ------: | ----------: | --------: | -----------: | 310 | | BenchmarkDon_BindRequest | 2947474 | 390.3 ns/op | 72 B/op | 2 allocs/op | 311 | | BenchmarkGin_BindRequest | 265609 | 4377 ns/op | 1193 B/op | 21 allocs/op | 312 | 313 | Source: [benchmarks/binding_test.go](./benchmarks/binding_test.go) 314 | 315 | ### Serving HTTP Requests 316 | 317 | Keep in mind that the majority of time here is actually the HTTP roundtrip. 318 | 319 | | Benchmark name | (1) | (2) | (3) | (4) | 320 | | ----------------- | ----: | ----------: | --------: | -----------: | 321 | | BenchmarkDon_HTTP | 45500 | 25384 ns/op | 32 B/op | 3 allocs/op | 322 | | BenchmarkGin_HTTP | 22995 | 49865 ns/op | 2313 B/op | 21 allocs/op | 323 | 324 | Source: [benchmarks/http_test.go](./benchmarks/http_test.go) 325 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: '3' 4 | 5 | tasks: 6 | default: 7 | desc: Runs the default tasks 8 | cmds: 9 | - task: init 10 | - task: mod 11 | - task: lint 12 | - task: test 13 | 14 | init: 15 | desc: Setup git hooks 16 | cmds: 17 | - cp -f scripts/pre-commit.sh .git/hooks/pre-commit 18 | 19 | mod: 20 | desc: Download Go modules 21 | cmds: 22 | - go mod tidy 23 | - cd benchmarks && go mod tidy 24 | 25 | lint: 26 | desc: Runs golangci-lint 27 | aliases: [l] 28 | sources: 29 | - ./**/*.go 30 | - .golangci.yml 31 | cmds: 32 | - golangci-lint run --fix 33 | 34 | test: 35 | desc: Runs test suite 36 | aliases: [t] 37 | cmds: 38 | - go test -cover ./... 39 | 40 | benchmark: 41 | desc: Runs benchmarks 42 | aliases: [b] 43 | dir: benchmarks 44 | cmds: 45 | - go test -bench=. -benchmem . 46 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | // Package don provides a fast and efficient API framework. 2 | package don 3 | 4 | import ( 5 | "bytes" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/abemedia/httprouter" 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | // DefaultEncoding contains the media type of the default encoding to fall back 14 | // on if no `Accept` or `Content-Type` header was provided. 15 | var DefaultEncoding = "text/plain" 16 | 17 | type Middleware func(fasthttp.RequestHandler) fasthttp.RequestHandler 18 | 19 | type Router interface { 20 | Get(path string, handle httprouter.Handle) 21 | Post(path string, handle httprouter.Handle) 22 | Put(path string, handle httprouter.Handle) 23 | Patch(path string, handle httprouter.Handle) 24 | Delete(path string, handle httprouter.Handle) 25 | Handle(method, path string, handle httprouter.Handle) 26 | Handler(method, path string, handle http.Handler) 27 | HandleFunc(method, path string, handle http.HandlerFunc) 28 | Group(path string) Router 29 | Use(mw ...Middleware) 30 | } 31 | 32 | type API struct { 33 | NotFound fasthttp.RequestHandler 34 | MethodNotAllowed fasthttp.RequestHandler 35 | PanicHandler func(*fasthttp.RequestCtx, any) 36 | 37 | router *httprouter.Router 38 | config *Config 39 | mw []Middleware 40 | } 41 | 42 | type Config struct { 43 | // DefaultEncoding contains the mime type of the default encoding to fall 44 | // back on if no `Accept` or `Content-Type` header was provided. 45 | DefaultEncoding string 46 | 47 | // DisableNoContent controls whether a nil or zero value response should 48 | // automatically return 204 No Content with an empty body. 49 | DisableNoContent bool 50 | } 51 | 52 | // New creates a new API instance. 53 | func New(c *Config) *API { 54 | if c == nil { 55 | c = &Config{} 56 | } 57 | 58 | if c.DefaultEncoding == "" { 59 | c.DefaultEncoding = DefaultEncoding 60 | } 61 | 62 | return &API{ 63 | router: httprouter.New(), 64 | config: c, 65 | NotFound: E(ErrNotFound), 66 | MethodNotAllowed: E(ErrMethodNotAllowed), 67 | } 68 | } 69 | 70 | // Get is a shortcut for router.Handle(http.MethodGet, path, handle). 71 | func (r *API) Get(path string, handle httprouter.Handle) { 72 | r.Handle(http.MethodGet, path, handle) 73 | } 74 | 75 | // Post is a shortcut for router.Handle(http.MethodPost, path, handle). 76 | func (r *API) Post(path string, handle httprouter.Handle) { 77 | r.Handle(http.MethodPost, path, handle) 78 | } 79 | 80 | // Put is a shortcut for router.Handle(http.MethodPut, path, handle). 81 | func (r *API) Put(path string, handle httprouter.Handle) { 82 | r.Handle(http.MethodPut, path, handle) 83 | } 84 | 85 | // Patch is a shortcut for router.Handle(http.MethodPatch, path, handle). 86 | func (r *API) Patch(path string, handle httprouter.Handle) { 87 | r.Handle(http.MethodPatch, path, handle) 88 | } 89 | 90 | // Delete is a shortcut for router.Handle(http.MethodDelete, path, handle). 91 | func (r *API) Delete(path string, handle httprouter.Handle) { 92 | r.Handle(http.MethodDelete, path, handle) 93 | } 94 | 95 | // Handle registers a new request handle with the given path and method. 96 | func (r *API) Handle(method, path string, handle httprouter.Handle) { 97 | r.router.Handle(method, path, handle) 98 | } 99 | 100 | // Handler is an adapter which allows the usage of an http.Handler as a request handle. 101 | func (r *API) Handler(method, path string, handle http.Handler) { 102 | r.router.Handler(method, path, handle) 103 | } 104 | 105 | // HandleFunc is an adapter which allows the usage of an http.HandlerFunc as a request handle. 106 | func (r *API) HandleFunc(method, path string, handle http.HandlerFunc) { 107 | r.router.HandlerFunc(method, path, handle) 108 | } 109 | 110 | // Group creates a new sub-router with a common prefix. 111 | func (r *API) Group(path string) Router { 112 | return &group{prefix: path, r: r} 113 | } 114 | 115 | // Use registers a middleware. 116 | func (r *API) Use(mw ...Middleware) { 117 | r.mw = append(r.mw, mw...) 118 | } 119 | 120 | // RequestHandler creates a fasthttp.RequestHandler for the API. 121 | func (r *API) RequestHandler() fasthttp.RequestHandler { 122 | r.router.NotFound = r.NotFound 123 | r.router.MethodNotAllowed = r.MethodNotAllowed 124 | r.router.PanicHandler = r.PanicHandler 125 | 126 | h := r.router.HandleFastHTTP 127 | for _, mw := range r.mw { 128 | h = mw(h) 129 | } 130 | 131 | return func(ctx *fasthttp.RequestCtx) { 132 | ct := ctx.Request.Header.ContentType() 133 | if len(ct) == 0 || bytes.HasPrefix(ct, anyEncoding) { 134 | ctx.Request.Header.SetContentType(r.config.DefaultEncoding) 135 | } 136 | 137 | ac := ctx.Request.Header.Peek(fasthttp.HeaderAccept) 138 | if len(ac) == 0 || bytes.HasPrefix(ac, anyEncoding) { 139 | ctx.Request.Header.Set(fasthttp.HeaderAccept, r.config.DefaultEncoding) 140 | } 141 | 142 | h(ctx) 143 | 144 | // Content-Length of -3 means handler returned nil. 145 | if ctx.Response.Header.ContentLength() == -3 { 146 | ctx.Response.Header.Del(fasthttp.HeaderTransferEncoding) 147 | 148 | if !r.config.DisableNoContent { 149 | ctx.Response.SetBody(nil) 150 | 151 | if ctx.Response.StatusCode() == fasthttp.StatusOK { 152 | ctx.Response.SetStatusCode(fasthttp.StatusNoContent) 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | // ListenAndServe serves HTTP requests from the given TCP4 addr. 160 | func (r *API) ListenAndServe(addr string) error { 161 | return newServer(r).ListenAndServe(addr) 162 | } 163 | 164 | // Serve serves incoming connections from the given listener. 165 | func (r *API) Serve(ln net.Listener) error { 166 | return newServer(r).Serve(ln) 167 | } 168 | 169 | func newServer(r *API) *fasthttp.Server { 170 | return &fasthttp.Server{ 171 | Handler: r.RequestHandler(), 172 | StreamRequestBody: true, 173 | NoDefaultContentType: true, 174 | NoDefaultServerHeader: true, 175 | } 176 | } 177 | 178 | var anyEncoding = []byte("*/*") 179 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package don_test 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/abemedia/go-don" 10 | _ "github.com/abemedia/go-don/encoding/text" 11 | "github.com/abemedia/go-don/internal/test" 12 | "github.com/abemedia/httprouter" 13 | "github.com/valyala/fasthttp" 14 | ) 15 | 16 | func TestAPI(t *testing.T) { 17 | t.Run("Router", func(t *testing.T) { 18 | api := don.New(nil) 19 | test.Router(t, api, api.RequestHandler(), "") 20 | }) 21 | 22 | t.Run("ListenAndServe", func(t *testing.T) { 23 | testAPI(t, func(api *don.API, ln net.Listener) error { 24 | ln.Close() //nolint:errcheck 25 | return api.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", ln.Addr().(*net.TCPAddr).Port)) 26 | }) 27 | }) 28 | 29 | t.Run("Serve", func(t *testing.T) { 30 | testAPI(t, func(api *don.API, ln net.Listener) error { return api.Serve(ln) }) 31 | }) 32 | } 33 | 34 | func testAPI(t *testing.T, serve func(api *don.API, ln net.Listener) error) { 35 | t.Helper() 36 | 37 | api := don.New(nil) 38 | api.Get("/", func(ctx *fasthttp.RequestCtx, _ httprouter.Params) { 39 | ctx.WriteString("OK") //nolint:errcheck 40 | }) 41 | 42 | ln, err := net.Listen("tcp", ":0") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | go func() { 48 | if err := serve(api, ln); err != nil { 49 | t.Error(err) 50 | } 51 | }() 52 | 53 | time.Sleep(time.Millisecond) // Wait for server to be listening. 54 | 55 | if t.Failed() { 56 | t.FailNow() // Fail fast if calling serve failed. 57 | } 58 | 59 | code, body, err := fasthttp.Get(nil, fmt.Sprintf("http://localhost:%d/", ln.Addr().(*net.TCPAddr).Port)) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | if code != fasthttp.StatusOK { 65 | t.Fatal("should return success status") 66 | } 67 | 68 | if string(body) != "OK" { 69 | t.Fatalf(`should return body "OK": %q`, body) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /benchmarks/binding_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks_test 2 | 3 | import ( 4 | "context" 5 | stdhttptest "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/abemedia/go-don" 9 | _ "github.com/abemedia/go-don/encoding/text" 10 | "github.com/abemedia/go-don/pkg/httptest" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | func BenchmarkDon_BindRequest(b *testing.B) { 15 | type request struct { 16 | Path string `path:"path"` 17 | Header string `header:"Header"` 18 | Query string `query:"query"` 19 | } 20 | 21 | api := don.New(nil) 22 | api.Post("/:path", don.H(func(ctx context.Context, req request) (any, error) { 23 | return nil, nil 24 | })) 25 | 26 | h := api.RequestHandler() 27 | ctx := httptest.NewRequest("POST", "/path?query=query", "", map[string]string{"header": "header"}) 28 | 29 | for i := 0; i < b.N; i++ { 30 | h(ctx) 31 | } 32 | } 33 | 34 | func BenchmarkGin_BindRequest(b *testing.B) { 35 | type request struct { 36 | Path string `uri:"path"` 37 | Header string `header:"Header"` 38 | Query string `form:"query"` 39 | } 40 | 41 | gin.SetMode("release") 42 | router := gin.New() 43 | router.POST("/:path", func(c *gin.Context) { 44 | req := &request{} 45 | c.ShouldBindHeader(req) 46 | c.ShouldBindQuery(req) 47 | c.ShouldBindUri(req) 48 | }) 49 | 50 | w := stdhttptest.NewRecorder() 51 | r := stdhttptest.NewRequest("POST", "/path?query=query", nil) 52 | r.Header.Add("header", "header") 53 | 54 | for i := 0; i < b.N; i++ { 55 | router.ServeHTTP(w, r) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /benchmarks/decoder_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/decoder" 7 | "github.com/gorilla/schema" 8 | ) 9 | 10 | func BenchmarkDecoder(b *testing.B) { 11 | type child struct { 12 | String string `schema:"string"` 13 | } 14 | 15 | type test struct { 16 | String string `schema:"string"` 17 | StringPtr *string `schema:"string"` 18 | Int int `schema:"int"` 19 | Int8 int8 `schema:"int8"` 20 | Int16 int16 `schema:"int16"` 21 | Int32 int32 `schema:"int32"` 22 | Int64 int64 `schema:"int64"` 23 | Uint uint `schema:"uint"` 24 | Uint8 uint8 `schema:"uint8"` 25 | Uint16 uint16 `schema:"uint16"` 26 | Uint32 uint32 `schema:"uint32"` 27 | Uint64 uint64 `schema:"uint64"` 28 | Float32 float32 `schema:"float32"` 29 | Float64 float64 `schema:"float64"` 30 | Bool bool `schema:"bool"` 31 | Strings []string `schema:"strings"` 32 | Nested child 33 | NestedPtr *child 34 | } 35 | 36 | in := map[string][]string{ 37 | "string": {"string"}, 38 | "strings": {"string", "string"}, 39 | "int": {"1"}, 40 | "int8": {"1"}, 41 | "int16": {"1"}, 42 | "int32": {"1"}, 43 | "int64": {"1"}, 44 | "uint": {"1"}, 45 | "uint8": {"1"}, 46 | "uint16": {"1"}, 47 | "uint32": {"1"}, 48 | "uint64": {"1"}, 49 | "float32": {"1"}, 50 | "float64": {"1"}, 51 | "bool": {"true"}, 52 | } 53 | 54 | b.Run("Gorilla", func(b *testing.B) { 55 | dec := schema.NewDecoder() 56 | 57 | for i := 0; i < b.N; i++ { 58 | out := &test{} 59 | if err := dec.Decode(out, in); err != nil { 60 | b.Fatal(err) 61 | } 62 | } 63 | }) 64 | 65 | b.Run("DonCached", func(b *testing.B) { 66 | dec, err := decoder.NewCached(test{}, "schema") 67 | if err != nil { 68 | b.Fatal(err) 69 | } 70 | 71 | for i := 0; i < b.N; i++ { 72 | out := &test{} 73 | if err := dec.Decode(decoder.Map(in), out); err != nil { 74 | b.Fatal(err) 75 | } 76 | } 77 | }) 78 | 79 | b.Run("Don", func(b *testing.B) { 80 | dec := decoder.New("schema") 81 | 82 | for i := 0; i < b.N; i++ { 83 | out := &test{} 84 | if err := dec.Decode(decoder.Map(in), out); err != nil { 85 | b.Fatal(err) 86 | } 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | // benchmarks in separate module to avoid needless dependencies 2 | 3 | module benchmarks 4 | 5 | go 1.18 6 | 7 | replace github.com/abemedia/go-don => ../ 8 | 9 | require ( 10 | github.com/abemedia/go-don v0.0.0-00010101000000-000000000000 11 | github.com/gin-gonic/gin v1.10.0 12 | github.com/gorilla/schema v1.4.1 13 | github.com/valyala/fasthttp v1.59.0 14 | ) 15 | 16 | require ( 17 | github.com/abemedia/fasthttpfs v0.0.0-20220405193636-731805b0c723 // indirect 18 | github.com/abemedia/httprouter v0.0.0-20230505023925-232e0e5a4b1b // indirect 19 | github.com/andybalholm/brotli v1.1.1 // indirect 20 | github.com/bytedance/sonic v1.11.6 // indirect 21 | github.com/bytedance/sonic/loader v0.1.1 // indirect 22 | github.com/cloudwego/base64x v0.1.4 // indirect 23 | github.com/cloudwego/iasm v0.2.0 // indirect 24 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 25 | github.com/gin-contrib/sse v0.1.0 // indirect 26 | github.com/go-playground/locales v0.14.1 // indirect 27 | github.com/go-playground/universal-translator v0.18.1 // indirect 28 | github.com/go-playground/validator/v10 v10.20.0 // indirect 29 | github.com/goccy/go-json v0.10.2 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/klauspost/compress v1.17.11 // indirect 32 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 33 | github.com/leodido/go-urn v1.4.0 // indirect 34 | github.com/mattn/go-isatty v0.0.20 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 38 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 39 | github.com/ugorji/go/codec v1.2.12 // indirect 40 | github.com/valyala/bytebufferpool v1.0.0 // indirect 41 | golang.org/x/arch v0.8.0 // indirect 42 | golang.org/x/crypto v0.36.0 // indirect 43 | golang.org/x/net v0.38.0 // indirect 44 | golang.org/x/sys v0.31.0 // indirect 45 | golang.org/x/text v0.23.0 // indirect 46 | google.golang.org/protobuf v1.34.1 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/abemedia/fasthttpfs v0.0.0-20220321013016-a7e6ad30856d/go.mod h1:Q7fRPwbRn+E/hqQEU2ZnMbp9juhSZBcSl3/aQrr+apQ= 2 | github.com/abemedia/fasthttpfs v0.0.0-20220405193636-731805b0c723 h1:gq2jKsEsHvR+R0InqnbxLQ5/L2bRTfXieNLAMhQir3I= 3 | github.com/abemedia/fasthttpfs v0.0.0-20220405193636-731805b0c723/go.mod h1:Q7fRPwbRn+E/hqQEU2ZnMbp9juhSZBcSl3/aQrr+apQ= 4 | github.com/abemedia/httprouter v0.0.0-20230505023925-232e0e5a4b1b h1:/0hlaEP0jHdeE/SxFO0fT6AdHUtaQp0rbOkxydJ2Bsc= 5 | github.com/abemedia/httprouter v0.0.0-20230505023925-232e0e5a4b1b/go.mod h1:jMATPO/ttg245+tl+jaLzgZs03RS9coyM87qNPzk594= 6 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 7 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 8 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 9 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 10 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 11 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 12 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 13 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 14 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 15 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 16 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 17 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 22 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 23 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 24 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 25 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 26 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 27 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 28 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 29 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 30 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 31 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 32 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 33 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 34 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 35 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 36 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 37 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 38 | github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= 39 | github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 40 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 41 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 42 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 43 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 44 | github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 45 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 46 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 47 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 48 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 49 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 50 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 51 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 52 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 53 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 54 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 55 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 59 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 60 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 61 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 65 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 66 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 67 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 68 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 69 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 70 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 71 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 72 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 73 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 74 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 75 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 76 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 77 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 78 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 79 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 80 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 81 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 82 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 83 | github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 84 | github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= 85 | github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= 86 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 87 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 88 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 89 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 90 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 91 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 92 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 93 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 94 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 95 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 96 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 97 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 98 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 99 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 100 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 101 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 102 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 103 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 104 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 105 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 106 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 107 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 108 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 109 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 110 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 114 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 124 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 125 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 126 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 127 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 128 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 129 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 130 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 131 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 132 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 133 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 134 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 135 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 136 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 137 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 138 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 139 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 140 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 141 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 143 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 144 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 145 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 146 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 147 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 148 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 149 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 150 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 151 | -------------------------------------------------------------------------------- /benchmarks/http_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/abemedia/go-don" 11 | _ "github.com/abemedia/go-don/encoding/text" 12 | "github.com/gin-gonic/gin" 13 | "github.com/valyala/fasthttp" 14 | ) 15 | 16 | func BenchmarkDon_HTTP(b *testing.B) { 17 | api := don.New(nil) 18 | api.Get("/:path", don.H(func(ctx context.Context, req any) (string, error) { 19 | return "foo", nil 20 | })) 21 | 22 | ln, err := net.Listen("tcp", "localhost:0") 23 | if err != nil { 24 | b.Fatal(err) 25 | } 26 | 27 | srv := fasthttp.Server{Handler: api.RequestHandler()} 28 | go srv.Serve(ln) 29 | 30 | url := fmt.Sprintf("http://%s/path", ln.Addr()) 31 | 32 | for i := 0; i < b.N; i++ { 33 | fasthttp.Get(nil, url) 34 | } 35 | 36 | srv.Shutdown() 37 | } 38 | 39 | func BenchmarkGin_HTTP(b *testing.B) { 40 | gin.SetMode("release") 41 | 42 | router := gin.New() 43 | router.GET("/:path", func(c *gin.Context) { 44 | c.String(200, "foo") 45 | }) 46 | 47 | ln, err := net.Listen("tcp", "localhost:0") 48 | if err != nil { 49 | b.Fatal(err) 50 | } 51 | 52 | srv := http.Server{Handler: router} 53 | go srv.Serve(ln) 54 | 55 | url := fmt.Sprintf("http://%s/path", ln.Addr()) 56 | 57 | for i := 0; i < b.N; i++ { 58 | fasthttp.Get(nil, url) 59 | } 60 | 61 | srv.Shutdown(context.Background()) 62 | } 63 | -------------------------------------------------------------------------------- /decoder/adapters.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "github.com/abemedia/go-don/internal/byteconv" 5 | "github.com/abemedia/httprouter" 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | type Map map[string][]string 10 | 11 | func (m Map) Get(key string) string { 12 | if m == nil { 13 | return "" 14 | } 15 | if vs := m[key]; len(vs) > 0 { 16 | return vs[0] 17 | } 18 | return "" 19 | } 20 | 21 | func (m Map) Values(key string) []string { 22 | if m == nil { 23 | return nil 24 | } 25 | return m[key] 26 | } 27 | 28 | type Args fasthttp.Args 29 | 30 | func (ps *Args) Get(key string) string { 31 | return byteconv.Btoa((*fasthttp.Args)(ps).Peek(key)) 32 | } 33 | 34 | func (ps *Args) Values(key string) []string { 35 | args := (*fasthttp.Args)(ps).PeekMulti(key) 36 | if len(args) == 0 { 37 | return nil 38 | } 39 | 40 | res := make([]string, len(args)) 41 | for i := range args { 42 | res[i] = byteconv.Btoa(args[i]) 43 | } 44 | 45 | return res 46 | } 47 | 48 | type Header fasthttp.RequestHeader 49 | 50 | func (ps *Header) Get(key string) string { 51 | return byteconv.Btoa((*fasthttp.RequestHeader)(ps).Peek(key)) 52 | } 53 | 54 | func (ps *Header) Values(key string) []string { 55 | args := (*fasthttp.RequestHeader)(ps).PeekAll(key) 56 | if len(args) == 0 { 57 | return nil 58 | } 59 | 60 | res := make([]string, len(args)) 61 | for i := range args { 62 | res[i] = byteconv.Btoa(args[i]) 63 | } 64 | 65 | return res 66 | } 67 | 68 | type Params httprouter.Params 69 | 70 | func (ps Params) Get(key string) string { 71 | for i := range ps { 72 | if ps[i].Key == key { 73 | return ps[i].Value 74 | } 75 | } 76 | return "" 77 | } 78 | 79 | func (ps Params) Values(key string) []string { 80 | for i := range ps { 81 | if ps[i].Key == key { 82 | return []string{ps[i].Value} 83 | } 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /decoder/adapters_test.go: -------------------------------------------------------------------------------- 1 | package decoder_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/decoder" 7 | "github.com/abemedia/httprouter" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | func TestAdapters(t *testing.T) { 13 | t.Run("Map", func(t *testing.T) { 14 | in := map[string][]string{"string": {"string"}} 15 | testAdapter(t, decoder.Map(in)) 16 | 17 | var m decoder.Map 18 | if m.Get("test") != "" { 19 | t.Error("should return empty string") 20 | } 21 | if m.Values("test") != nil { 22 | t.Error("should return nil") 23 | } 24 | }) 25 | 26 | t.Run("Args", func(t *testing.T) { 27 | in := &fasthttp.Args{} 28 | in.Add("string", "string") 29 | testAdapter(t, (*decoder.Args)(in)) 30 | }) 31 | 32 | t.Run("Header", func(t *testing.T) { 33 | in := &fasthttp.RequestHeader{} 34 | in.Add("string", "string") 35 | testAdapter(t, (*decoder.Header)(in)) 36 | }) 37 | 38 | t.Run("Params", func(t *testing.T) { 39 | in := httprouter.Params{{Key: "string", Value: "string"}} 40 | testAdapter(t, decoder.Params(in)) 41 | }) 42 | } 43 | 44 | func testAdapter(t *testing.T, in decoder.Getter) { 45 | t.Helper() 46 | 47 | type item struct { 48 | Zero string `field:"empty"` 49 | Nil []string `field:"empty"` 50 | String string `field:"string"` 51 | Strings []string `field:"string"` 52 | } 53 | 54 | want := &item{ 55 | String: "string", 56 | Strings: []string{"string"}, 57 | } 58 | 59 | dec, err := decoder.NewCached(item{}, "field") 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | got := &item{} 65 | if err = dec.Decode(in, got); err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | if diff := cmp.Diff(want, got); diff != "" { 70 | t.Error(diff) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /decoder/compile.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "encoding" 5 | "reflect" 6 | "strconv" 7 | "unsafe" 8 | 9 | "github.com/abemedia/go-don/internal/byteconv" 10 | ) 11 | 12 | type decoder = func(reflect.Value, Getter) error 13 | 14 | func noopDecoder(reflect.Value, Getter) error { return nil } 15 | 16 | var unmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() 17 | 18 | //nolint:cyclop,funlen 19 | func compile(typ reflect.Type, tagKey string, isPtr bool) (decoder, error) { 20 | decoders := []decoder{} 21 | 22 | for i := 0; i < typ.NumField(); i++ { 23 | f := typ.Field(i) 24 | if f.PkgPath != "" { 25 | continue // skip unexported fields 26 | } 27 | 28 | t, k, ptr := typeKind(f.Type) 29 | 30 | tag, ok := f.Tag.Lookup(tagKey) 31 | if !ok && k != reflect.Struct { 32 | continue 33 | } 34 | 35 | if reflect.PointerTo(t).Implements(unmarshalerType) { 36 | decoders = append(decoders, decodeTextUnmarshaler(get(ptr, i, t), tag)) 37 | continue 38 | } 39 | 40 | switch k { 41 | case reflect.Struct: 42 | dec, err := compile(t, tagKey, ptr) 43 | if err != nil { 44 | return nil, err 45 | } 46 | index := i 47 | decoders = append(decoders, func(v reflect.Value, m Getter) error { 48 | return dec(v.Field(index), m) 49 | }) 50 | case reflect.String: 51 | decoders = append(decoders, decodeString(set[string](ptr, i, t), tag)) 52 | case reflect.Int: 53 | decoders = append(decoders, decodeInt(set[int](ptr, i, t), tag, strconv.IntSize)) 54 | case reflect.Int8: 55 | decoders = append(decoders, decodeInt(set[int8](ptr, i, t), tag, 8)) 56 | case reflect.Int16: 57 | decoders = append(decoders, decodeInt(set[int16](ptr, i, t), tag, 16)) 58 | case reflect.Int32: 59 | decoders = append(decoders, decodeInt(set[int32](ptr, i, t), tag, 32)) 60 | case reflect.Int64: 61 | decoders = append(decoders, decodeInt(set[int64](ptr, i, t), tag, 64)) 62 | case reflect.Uint: 63 | decoders = append(decoders, decodeUint(set[uint](ptr, i, t), tag, strconv.IntSize)) 64 | case reflect.Uint8: 65 | decoders = append(decoders, decodeUint(set[uint8](ptr, i, t), tag, 8)) 66 | case reflect.Uint16: 67 | decoders = append(decoders, decodeUint(set[uint16](ptr, i, t), tag, 16)) 68 | case reflect.Uint32: 69 | decoders = append(decoders, decodeUint(set[uint32](ptr, i, t), tag, 32)) 70 | case reflect.Uint64: 71 | decoders = append(decoders, decodeUint(set[uint64](ptr, i, t), tag, 64)) 72 | case reflect.Float32: 73 | decoders = append(decoders, decodeFloat(set[float32](ptr, i, t), tag, 32)) 74 | case reflect.Float64: 75 | decoders = append(decoders, decodeFloat(set[float64](ptr, i, t), tag, 64)) 76 | case reflect.Bool: 77 | decoders = append(decoders, decodeBool(set[bool](ptr, i, t), tag)) 78 | case reflect.Slice: 79 | switch t.Elem().Kind() { 80 | case reflect.String: 81 | decoders = append(decoders, decodeStrings(set[[]string](ptr, i, t), tag)) 82 | case reflect.Uint8: 83 | decoders = append(decoders, decodeBytes(set[[]byte](ptr, i, t), tag)) 84 | } 85 | default: 86 | return nil, ErrUnsupportedType 87 | } 88 | } 89 | 90 | if len(decoders) == 0 { 91 | return nil, ErrTagNotFound 92 | } 93 | 94 | return func(v reflect.Value, d Getter) error { 95 | if isPtr { 96 | if v.IsNil() { 97 | v.Set(reflect.New(typ)) 98 | } 99 | v = v.Elem() 100 | } 101 | 102 | for _, dec := range decoders { 103 | if err := dec(v, d); err != nil { 104 | return err 105 | } 106 | } 107 | 108 | return nil 109 | }, nil 110 | } 111 | 112 | func typeKind(t reflect.Type) (reflect.Type, reflect.Kind, bool) { 113 | var isPtr bool 114 | 115 | k := t.Kind() 116 | if k == reflect.Pointer { 117 | t = t.Elem() 118 | k = t.Kind() 119 | isPtr = true 120 | } 121 | 122 | return t, k, isPtr 123 | } 124 | 125 | func set[T any](ptr bool, i int, t reflect.Type) func(reflect.Value, T) { 126 | if ptr { 127 | return func(v reflect.Value, d T) { 128 | f := v.Field(i) 129 | if f.IsNil() { 130 | f.Set(reflect.New(t)) 131 | } 132 | *(*T)(unsafe.Pointer(f.Elem().UnsafeAddr())) = d 133 | } 134 | } 135 | 136 | return func(v reflect.Value, d T) { 137 | *(*T)(unsafe.Pointer(v.Field(i).UnsafeAddr())) = d 138 | } 139 | } 140 | 141 | func get(ptr bool, i int, t reflect.Type) func(v reflect.Value) reflect.Value { 142 | if ptr { 143 | return func(v reflect.Value) reflect.Value { 144 | f := v.Field(i) 145 | if f.IsNil() { 146 | f.Set(reflect.New(t)) 147 | } 148 | return f 149 | } 150 | } 151 | 152 | return func(v reflect.Value) reflect.Value { 153 | return v.Field(i).Addr() 154 | } 155 | } 156 | 157 | func decodeTextUnmarshaler(get func(reflect.Value) reflect.Value, k string) decoder { 158 | return func(v reflect.Value, g Getter) error { 159 | if s := g.Get(k); s != "" { 160 | return get(v).Interface().(encoding.TextUnmarshaler).UnmarshalText(byteconv.Atob(s)) //nolint:forcetypeassert 161 | } 162 | return nil 163 | } 164 | } 165 | 166 | func decodeString(set func(reflect.Value, string), k string) decoder { 167 | return func(v reflect.Value, g Getter) error { 168 | if s := g.Get(k); s != "" { 169 | set(v, s) 170 | } 171 | return nil 172 | } 173 | } 174 | 175 | func decodeInt[T int | int8 | int16 | int32 | int64](set func(reflect.Value, T), k string, bits int) decoder { 176 | return func(v reflect.Value, g Getter) error { 177 | if s := g.Get(k); s != "" { 178 | n, err := strconv.ParseInt(s, 10, bits) 179 | if err != nil { 180 | return err 181 | } 182 | set(v, T(n)) 183 | } 184 | return nil 185 | } 186 | } 187 | 188 | func decodeFloat[T float32 | float64](set func(reflect.Value, T), k string, bits int) decoder { 189 | return func(v reflect.Value, g Getter) error { 190 | if s := g.Get(k); s != "" { 191 | f, err := strconv.ParseFloat(s, bits) 192 | if err != nil { 193 | return err 194 | } 195 | set(v, T(f)) 196 | } 197 | return nil 198 | } 199 | } 200 | 201 | func decodeUint[T uint | uint8 | uint16 | uint32 | uint64](set func(reflect.Value, T), k string, bits int) decoder { 202 | return func(v reflect.Value, g Getter) error { 203 | if s := g.Get(k); s != "" { 204 | n, err := strconv.ParseUint(s, 10, bits) 205 | if err != nil { 206 | return err 207 | } 208 | set(v, T(n)) 209 | } 210 | return nil 211 | } 212 | } 213 | 214 | func decodeBool(set func(reflect.Value, bool), k string) decoder { 215 | return func(v reflect.Value, g Getter) error { 216 | if s := g.Get(k); s != "" { 217 | b, err := strconv.ParseBool(s) 218 | if err != nil { 219 | return err 220 | } 221 | set(v, b) 222 | } 223 | return nil 224 | } 225 | } 226 | 227 | func decodeBytes(set func(reflect.Value, []byte), k string) decoder { 228 | return func(v reflect.Value, g Getter) error { 229 | if s := g.Get(k); s != "" { 230 | set(v, byteconv.Atob(s)) 231 | } 232 | return nil 233 | } 234 | } 235 | 236 | func decodeStrings(set func(reflect.Value, []string), k string) decoder { 237 | return func(v reflect.Value, g Getter) error { 238 | if s := g.Values(k); s != nil { 239 | set(v, s) 240 | } 241 | return nil 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /decoder/decode.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "sync" 7 | ) 8 | 9 | var ( 10 | ErrUnsupportedType = errors.New("decoder: unsupported type") 11 | ErrTagNotFound = errors.New("decoder: tag not found") 12 | ) 13 | 14 | type Getter interface { 15 | Get(key string) string 16 | Values(key string) []string 17 | } 18 | 19 | type Decoder struct { 20 | tag string 21 | cache sync.Map 22 | } 23 | 24 | func New(tag string) *Decoder { 25 | return &Decoder{tag: tag} 26 | } 27 | 28 | func (d *Decoder) Decode(data Getter, v any) error { 29 | val := reflect.ValueOf(v) 30 | if val.Kind() != reflect.Pointer { 31 | return ErrUnsupportedType 32 | } 33 | 34 | val = val.Elem() 35 | if val.Kind() != reflect.Struct { 36 | return ErrUnsupportedType 37 | } 38 | 39 | t := val.Type() 40 | 41 | dec, ok := d.cache.Load(t) 42 | if !ok { 43 | var err error 44 | dec, err = compile(t, d.tag, t.Kind() == reflect.Ptr) 45 | if err != nil { 46 | if err != ErrTagNotFound { //nolint:errorlint,err113 47 | return err 48 | } 49 | dec = noopDecoder 50 | } 51 | 52 | d.cache.Store(t, dec) 53 | } 54 | 55 | return dec.(decoder)(val, data) //nolint:forcetypeassert 56 | } 57 | 58 | type CachedDecoder[V any] struct { 59 | dec decoder 60 | } 61 | 62 | func NewCached[V any](v V, tag string) (*CachedDecoder[V], error) { 63 | t := reflect.TypeOf(v) 64 | if t == nil { 65 | return nil, ErrUnsupportedType 66 | } 67 | 68 | t, k, ptr := typeKind(t) 69 | if k != reflect.Struct { 70 | return nil, ErrUnsupportedType 71 | } 72 | 73 | dec, err := compile(t, tag, ptr) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return &CachedDecoder[V]{dec}, nil 79 | } 80 | 81 | func (d *CachedDecoder[V]) Decode(data Getter, v *V) error { 82 | return d.dec(reflect.ValueOf(v).Elem(), data) 83 | } 84 | 85 | func (d *CachedDecoder[V]) DecodeValue(data Getter, v reflect.Value) error { 86 | return d.dec(v, data) 87 | } 88 | -------------------------------------------------------------------------------- /decoder/decode_test.go: -------------------------------------------------------------------------------- 1 | package decoder_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/abemedia/go-don/decoder" 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | type unmarshaler string 14 | 15 | func (h *unmarshaler) UnmarshalText(b []byte) error { 16 | *h = unmarshaler(":" + string(b) + ":") 17 | return nil 18 | } 19 | 20 | func TestDecode(t *testing.T) { 21 | type child struct { 22 | String string `field:"string"` 23 | } 24 | 25 | type test struct { 26 | Unmarshaler unmarshaler `field:"string"` 27 | UnmarshalerPtr *unmarshaler `field:"string"` 28 | String string `field:"string"` 29 | StringPtr *string `field:"string"` 30 | Int int `field:"number"` 31 | Int8 int8 `field:"number"` 32 | Int16 int16 `field:"number"` 33 | Int32 int32 `field:"number"` 34 | Int64 int64 `field:"number"` 35 | Uint uint `field:"number"` 36 | Uint8 uint8 `field:"number"` 37 | Uint16 uint16 `field:"number"` 38 | Uint32 uint32 `field:"number"` 39 | Uint64 uint64 `field:"number"` 40 | Float32 float32 `field:"number"` 41 | Float64 float64 `field:"number"` 42 | Bool bool `field:"bool"` 43 | Bytes []byte `field:"string"` 44 | Strings []string `field:"strings"` 45 | Nested child 46 | NestedPtr *child 47 | unexported string `field:"string"` //nolint:unused 48 | } 49 | 50 | in := decoder.Map{ 51 | "string": {"string"}, 52 | "strings": {"string", "string"}, 53 | "number": {"1"}, 54 | "bool": {"true"}, 55 | } 56 | 57 | s := "string" 58 | u := unmarshaler(":string:") 59 | expected := &test{ 60 | Unmarshaler: ":string:", 61 | UnmarshalerPtr: &u, 62 | String: "string", 63 | StringPtr: &s, 64 | Int: 1, 65 | Int8: 1, 66 | Int16: 1, 67 | Int32: 1, 68 | Int64: 1, 69 | Uint: 1, 70 | Uint8: 1, 71 | Uint16: 1, 72 | Uint32: 1, 73 | Uint64: 1, 74 | Float32: 1, 75 | Float64: 1, 76 | Bool: true, 77 | Bytes: []byte("string"), 78 | Strings: []string{"string", "string"}, 79 | Nested: child{ 80 | String: "string", 81 | }, 82 | NestedPtr: &child{ 83 | String: "string", 84 | }, 85 | } 86 | 87 | exportAll := cmp.Exporter(func(t reflect.Type) bool { return true }) 88 | 89 | t.Run("Decoder", func(t *testing.T) { 90 | dec := decoder.New("field") 91 | actual := &test{} 92 | if err := dec.Decode(in, actual); err != nil { 93 | t.Fatal(err) 94 | } 95 | if diff := cmp.Diff(expected, actual, exportAll); diff != "" { 96 | t.Error(diff) 97 | } 98 | }) 99 | 100 | t.Run("CachedDecoder", func(t *testing.T) { 101 | dec, err := decoder.NewCached(test{}, "field") 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | actual := &test{} 107 | if err := dec.Decode(in, actual); err != nil { 108 | t.Fatal(err) 109 | } 110 | if diff := cmp.Diff(expected, actual, exportAll); diff != "" { 111 | t.Error(diff) 112 | } 113 | 114 | actual = &test{} 115 | val := reflect.ValueOf(actual).Elem() 116 | if err = dec.DecodeValue(in, val); err != nil { 117 | t.Fatal(err) 118 | } 119 | if diff := cmp.Diff(expected, actual, exportAll); diff != "" { 120 | t.Error(diff) 121 | } 122 | }) 123 | 124 | t.Run("CachedDecoder_NilPointer", func(t *testing.T) { 125 | dec, err := decoder.NewCached(&test{}, "field") 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | var actual *test 131 | if err := dec.Decode(in, &actual); err != nil { 132 | t.Fatal(err) 133 | } 134 | if diff := cmp.Diff(expected, actual, exportAll); diff != "" { 135 | t.Error(diff) 136 | } 137 | }) 138 | } 139 | 140 | func TestDecodeError(t *testing.T) { 141 | data := decoder.Map{"test": {"test"}} 142 | 143 | tests := []struct { 144 | target any 145 | error error 146 | }{ 147 | { 148 | target: nil, 149 | error: decoder.ErrUnsupportedType, 150 | }, 151 | { 152 | target: "", 153 | error: decoder.ErrUnsupportedType, 154 | }, 155 | { 156 | target: new(string), 157 | error: decoder.ErrUnsupportedType, 158 | }, 159 | { 160 | target: new(int), 161 | error: decoder.ErrUnsupportedType, 162 | }, 163 | { 164 | target: &struct { 165 | Test string `json:"test"` 166 | }{}, 167 | error: decoder.ErrTagNotFound, 168 | }, 169 | { 170 | target: &struct { 171 | Test chan string `field:"test"` 172 | }{}, 173 | error: decoder.ErrUnsupportedType, 174 | }, 175 | { 176 | target: &struct { 177 | Test string `field:"test"` 178 | Child struct { 179 | Test chan string `field:"test"` 180 | } 181 | }{}, 182 | error: decoder.ErrUnsupportedType, 183 | }, 184 | { 185 | target: &struct { 186 | Test int `field:"test"` 187 | }{}, 188 | error: strconv.ErrSyntax, 189 | }, 190 | { 191 | target: &struct { 192 | Test uint `field:"test"` 193 | }{}, 194 | error: strconv.ErrSyntax, 195 | }, 196 | { 197 | target: &struct { 198 | Test float64 `field:"test"` 199 | }{}, 200 | error: strconv.ErrSyntax, 201 | }, 202 | { 203 | target: &struct { 204 | Test bool `field:"test"` 205 | }{}, 206 | error: strconv.ErrSyntax, 207 | }, 208 | } 209 | 210 | t.Run("Decoder", func(t *testing.T) { 211 | for _, test := range tests { 212 | dec := decoder.New("field") 213 | err := dec.Decode(data, test.target) 214 | if errors.Is(test.error, decoder.ErrTagNotFound) { 215 | if err != nil { 216 | t.Errorf("should silently ignore error %q for %T", test.error, test.target) 217 | } 218 | } else { 219 | if !errors.Is(err, test.error) { 220 | t.Errorf("should return %q for %T: %q", test.error, test.target, err) 221 | } 222 | } 223 | } 224 | }) 225 | 226 | t.Run("CachedDecoder", func(t *testing.T) { 227 | for _, test := range tests { 228 | dec, err := decoder.NewCached(test.target, "field") 229 | if err != nil { 230 | if !errors.Is(err, test.error) { 231 | t.Errorf("should return %q for %T: %q", test.error, test.target, err) 232 | } 233 | continue 234 | } 235 | err = dec.Decode(data, &test.target) 236 | if !errors.Is(err, test.error) { 237 | t.Errorf("should return %q for %T: %q", test.error, test.target, err) 238 | } 239 | } 240 | }) 241 | } 242 | 243 | func BenchmarkDecoder(b *testing.B) { 244 | type child struct { 245 | String string `schema:"string"` 246 | } 247 | 248 | type test struct { 249 | String string `schema:"string"` 250 | StringPtr *string `schema:"string"` 251 | Int int `schema:"int"` 252 | Int8 int8 `schema:"int8"` 253 | Int16 int16 `schema:"int16"` 254 | Int32 int32 `schema:"int32"` 255 | Int64 int64 `schema:"int64"` 256 | Uint uint `schema:"uint"` 257 | Uint8 uint8 `schema:"uint8"` 258 | Uint16 uint16 `schema:"uint16"` 259 | Uint32 uint32 `schema:"uint32"` 260 | Uint64 uint64 `schema:"uint64"` 261 | Float32 float32 `schema:"float32"` 262 | Float64 float64 `schema:"float64"` 263 | Bool bool `schema:"bool"` 264 | Strings []string `schema:"strings"` 265 | Nested child 266 | NestedPtr *child 267 | } 268 | 269 | in := map[string][]string{ 270 | "string": {"string"}, 271 | "strings": {"string", "string"}, 272 | "int": {"1"}, 273 | "int8": {"1"}, 274 | "int16": {"1"}, 275 | "int32": {"1"}, 276 | "int64": {"1"}, 277 | "uint": {"1"}, 278 | "uint8": {"1"}, 279 | "uint16": {"1"}, 280 | "uint32": {"1"}, 281 | "uint64": {"1"}, 282 | "float32": {"1"}, 283 | "float64": {"1"}, 284 | "bool": {"true"}, 285 | } 286 | 287 | dec := decoder.New("schema") 288 | 289 | for i := 0; i < b.N; i++ { 290 | out := &test{} 291 | if err := dec.Decode(decoder.Map(in), out); err != nil { 292 | b.Fatal(err) 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /encoding/decode.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | type ( 10 | Unmarshaler = func(data []byte, v any) error 11 | ContextUnmarshaler = func(ctx context.Context, data []byte, v any) error 12 | RequestParser = func(ctx *fasthttp.RequestCtx, v any) error 13 | ) 14 | 15 | type DecoderConstraint interface { 16 | Unmarshaler | ContextUnmarshaler | RequestParser 17 | } 18 | 19 | // RegisterDecoder registers a request decoder for a given media type. 20 | func RegisterDecoder[T DecoderConstraint](dec T, mime string, aliases ...string) { 21 | switch d := any(dec).(type) { 22 | case Unmarshaler: 23 | decoders[mime] = func(ctx *fasthttp.RequestCtx, v any) error { 24 | return d(ctx.Request.Body(), v) 25 | } 26 | 27 | case ContextUnmarshaler: 28 | decoders[mime] = func(ctx *fasthttp.RequestCtx, v any) error { 29 | return d(ctx, ctx.Request.Body(), v) 30 | } 31 | 32 | case RequestParser: 33 | decoders[mime] = d 34 | } 35 | 36 | for _, alias := range aliases { 37 | decoders[alias] = decoders[mime] 38 | } 39 | } 40 | 41 | // GetDecoder returns the request decoder for a given media type. 42 | func GetDecoder(mime string) RequestParser { 43 | return decoders[mime] 44 | } 45 | 46 | var decoders = map[string]RequestParser{} 47 | -------------------------------------------------------------------------------- /encoding/decode_test.go: -------------------------------------------------------------------------------- 1 | package encoding_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/abemedia/go-don/encoding" 10 | "github.com/abemedia/go-don/pkg/httptest" 11 | "github.com/valyala/fasthttp" 12 | ) 13 | 14 | func TestRegisterDecoder(t *testing.T) { 15 | t.Run("Unmarshaler", func(t *testing.T) { 16 | testRegisterDecoder(t, func(data []byte, v any) error { 17 | if len(data) == 0 { 18 | return io.EOF 19 | } 20 | reflect.ValueOf(v).Elem().SetBytes(data) 21 | return nil 22 | }, "unmarshaler", "unmarshaler-alias") 23 | }) 24 | 25 | t.Run("ContextUnmarshaler", func(t *testing.T) { 26 | testRegisterDecoder(t, func(ctx context.Context, data []byte, v any) error { 27 | if len(data) == 0 { 28 | return io.EOF 29 | } 30 | reflect.ValueOf(v).Elem().SetBytes(data) 31 | return nil 32 | }, "context-unmarshaler", "context-unmarshaler-alias") 33 | }) 34 | 35 | t.Run("RequestParser", func(t *testing.T) { 36 | testRegisterDecoder(t, func(ctx *fasthttp.RequestCtx, v any) error { 37 | b := ctx.Request.Body() 38 | if len(b) == 0 { 39 | return io.EOF 40 | } 41 | reflect.ValueOf(v).Elem().SetBytes(b) 42 | return nil 43 | }, "request-parser", "request-parser-alias") 44 | }) 45 | } 46 | 47 | func testRegisterDecoder[T encoding.DecoderConstraint](t *testing.T, dec T, contentType, alias string) { 48 | t.Helper() 49 | 50 | encoding.RegisterDecoder(dec, contentType, alias) 51 | 52 | for _, v := range []string{contentType, alias} { 53 | decode := encoding.GetDecoder(v) 54 | if decode == nil { 55 | t.Error("decoder not found") 56 | continue 57 | } 58 | 59 | req := httptest.NewRequest("", "", v, nil) 60 | 61 | var b []byte 62 | if err := decode(req, &b); err != nil { 63 | t.Error(err) 64 | } else if string(b) != v { 65 | t.Error("should decode request") 66 | } 67 | 68 | req.Request.SetBody(nil) 69 | if err := decode(req, &b); err == nil { 70 | t.Error("should return error") 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /encoding/encode.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | type ( 11 | Marshaler = func(v any) ([]byte, error) 12 | ContextMarshaler = func(ctx context.Context, v any) ([]byte, error) 13 | ResponseEncoder = func(ctx *fasthttp.RequestCtx, v any) error 14 | ) 15 | 16 | type EncoderConstraint interface { 17 | Marshaler | ContextMarshaler | ResponseEncoder 18 | } 19 | 20 | // RegisterEncoder registers a response encoder on a given media type. 21 | func RegisterEncoder[T EncoderConstraint](enc T, mime string, aliases ...string) { 22 | switch e := any(enc).(type) { 23 | case Marshaler: 24 | encoders[mime] = func(ctx *fasthttp.RequestCtx, v any) error { 25 | b, err := e(v) 26 | if err != nil { 27 | return err 28 | } 29 | ctx.Response.SetBodyRaw(b) 30 | return nil 31 | } 32 | 33 | case ContextMarshaler: 34 | encoders[mime] = func(ctx *fasthttp.RequestCtx, v any) error { 35 | b, err := e(ctx, v) 36 | if err != nil { 37 | return err 38 | } 39 | ctx.Response.SetBodyRaw(b) 40 | return nil 41 | } 42 | 43 | case ResponseEncoder: 44 | encoders[mime] = e 45 | } 46 | 47 | for _, alias := range aliases { 48 | encoders[alias] = encoders[mime] 49 | } 50 | } 51 | 52 | // GetEncoder returns the response encoder for a given media type. 53 | func GetEncoder(mime string) ResponseEncoder { 54 | mimeParts := strings.Split(mime, ",") 55 | for _, part := range mimeParts { 56 | if enc, ok := encoders[part]; ok { 57 | return enc 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | var encoders = map[string]ResponseEncoder{} 64 | -------------------------------------------------------------------------------- /encoding/encode_test.go: -------------------------------------------------------------------------------- 1 | package encoding_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | 8 | "github.com/abemedia/go-don/encoding" 9 | "github.com/abemedia/go-don/pkg/httptest" 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | func TestRegisterEncoder(t *testing.T) { 14 | t.Run("Marshaler", func(t *testing.T) { 15 | testRegisterEncoder(t, func(v any) ([]byte, error) { 16 | b := v.([]byte) 17 | if len(b) == 0 { 18 | return nil, io.EOF 19 | } 20 | return b, nil 21 | }, "unmarshaler", "marshaler-alias") 22 | }) 23 | 24 | t.Run("ContextMarshaler", func(t *testing.T) { 25 | testRegisterEncoder(t, func(ctx context.Context, v any) ([]byte, error) { 26 | b := v.([]byte) 27 | if len(b) == 0 { 28 | return nil, io.EOF 29 | } 30 | return b, nil 31 | }, "context-marshaler", "context-marshaler-alias") 32 | }) 33 | 34 | t.Run("ResponseEncoder", func(t *testing.T) { 35 | testRegisterEncoder(t, func(ctx *fasthttp.RequestCtx, v any) error { 36 | b := v.([]byte) 37 | if len(b) == 0 { 38 | return io.EOF 39 | } 40 | ctx.Response.SetBodyRaw(b) 41 | return nil 42 | }, "response-encoder", "response-encoder-alias") 43 | }) 44 | } 45 | 46 | func testRegisterEncoder[T encoding.EncoderConstraint](t *testing.T, dec T, contentType, alias string) { 47 | t.Helper() 48 | 49 | encoding.RegisterEncoder(dec, contentType, alias) 50 | 51 | for _, v := range []string{contentType, alias} { 52 | encode := encoding.GetEncoder(v) 53 | if encode == nil { 54 | t.Error("encoder not found") 55 | continue 56 | } 57 | 58 | req := httptest.NewRequest("", "", v, nil) 59 | 60 | if err := encode(req, []byte(v)); err != nil { 61 | t.Error(err) 62 | } else if string(req.Response.Body()) != v { 63 | t.Error("should encode response") 64 | } 65 | 66 | if err := encode(req, []byte{}); err == nil { 67 | t.Error("should return error") 68 | } 69 | } 70 | } 71 | 72 | func TestGetEncoderMultipleContentTypes(t *testing.T) { 73 | encFn := func(ctx *fasthttp.RequestCtx, v any) error { 74 | return nil 75 | } 76 | 77 | encoding.RegisterEncoder(encFn, "application/xml") 78 | 79 | enc := encoding.GetEncoder("text/html,application/xhtml+xml,application/xml") 80 | if enc == nil { 81 | t.Fatal("encoder not found") 82 | } 83 | 84 | enc = encoding.GetEncoder("application/xhtml+xml") 85 | if enc != nil { 86 | t.Fatal("encoder should not be found") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /encoding/form/form.go: -------------------------------------------------------------------------------- 1 | // Package form provides decoding of form data. 2 | package form 3 | 4 | import ( 5 | "github.com/abemedia/go-don/decoder" 6 | "github.com/abemedia/go-don/encoding" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | var dec = decoder.New("form") 11 | 12 | func decodeForm(ctx *fasthttp.RequestCtx, v any) error { 13 | return dec.Decode((*decoder.Args)(ctx.PostArgs()), v) 14 | } 15 | 16 | func decodeMultipartForm(ctx *fasthttp.RequestCtx, v any) error { 17 | f, err := ctx.MultipartForm() 18 | if err != nil { 19 | return err 20 | } 21 | return dec.Decode(decoder.Map(f.Value), v) 22 | } 23 | 24 | func init() { 25 | encoding.RegisterDecoder(decodeForm, "application/x-www-form-urlencoded") 26 | encoding.RegisterDecoder(decodeMultipartForm, "multipart/form-data") 27 | } 28 | -------------------------------------------------------------------------------- /encoding/form/form_test.go: -------------------------------------------------------------------------------- 1 | package form_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/internal/test" 7 | ) 8 | 9 | func TestForm(t *testing.T) { 10 | type item struct { 11 | Foo string `form:"foo"` 12 | } 13 | 14 | t.Run("URLEncoded", func(t *testing.T) { 15 | test.Decode(t, test.EncodingOptions[item]{ 16 | Mime: "application/x-www-form-urlencoded", 17 | Raw: "foo=bar", 18 | Parsed: item{Foo: "bar"}, 19 | }) 20 | }) 21 | 22 | t.Run("Multipart", func(t *testing.T) { 23 | test.Decode(t, test.EncodingOptions[item]{ 24 | Mime: `multipart/form-data;boundary="boundary"`, 25 | Raw: "--boundary\nContent-Disposition: form-data; name=\"foo\"\n\nbar\n--boundary\n", 26 | Parsed: item{Foo: "bar"}, 27 | }) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /encoding/json/json.go: -------------------------------------------------------------------------------- 1 | // Package json provides encoding and decoding of JSON data. 2 | package json 3 | 4 | import ( 5 | "github.com/abemedia/go-don/encoding" 6 | "github.com/goccy/go-json" 7 | ) 8 | 9 | func init() { 10 | mediaType := "application/json" 11 | 12 | encoding.RegisterDecoder(json.Unmarshal, mediaType) 13 | encoding.RegisterEncoder(json.Marshal, mediaType) 14 | } 15 | -------------------------------------------------------------------------------- /encoding/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/internal/test" 7 | ) 8 | 9 | type item struct { 10 | Foo string `json:"foo"` 11 | } 12 | 13 | var opt = test.EncodingOptions[item]{ 14 | Mime: "application/json", 15 | Raw: `{"foo":"bar"}`, 16 | Parsed: item{Foo: "bar"}, 17 | } 18 | 19 | func TestJSON(t *testing.T) { 20 | test.Encoding(t, opt) 21 | } 22 | 23 | func BenchmarkJSON(b *testing.B) { 24 | test.BenchmarkEncoding(b, opt) 25 | } 26 | -------------------------------------------------------------------------------- /encoding/msgpack/msgpack.go: -------------------------------------------------------------------------------- 1 | // Package msgpack provides encoding and decoding of MessagePack data. 2 | package msgpack 3 | 4 | import ( 5 | "github.com/abemedia/go-don/encoding" 6 | "github.com/vmihailenco/msgpack/v5" 7 | ) 8 | 9 | func init() { 10 | mediaType := "application/msgpack" 11 | aliases := []string{"application/x-msgpack", "application/vnd.msgpack"} 12 | 13 | encoding.RegisterDecoder(msgpack.Unmarshal, mediaType, aliases...) 14 | encoding.RegisterEncoder(msgpack.Marshal, mediaType, aliases...) 15 | } 16 | -------------------------------------------------------------------------------- /encoding/msgpack/msgpack_test.go: -------------------------------------------------------------------------------- 1 | package msgpack_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/internal/test" 7 | ) 8 | 9 | type item struct { 10 | Foo string `msgpack:"foo"` 11 | } 12 | 13 | var opt = test.EncodingOptions[item]{ 14 | Mime: "application/x-msgpack", 15 | Raw: "\x81\xa3foo\xa3bar", 16 | Parsed: item{Foo: "bar"}, 17 | } 18 | 19 | func TestMsgpack(t *testing.T) { 20 | test.Encoding(t, opt) 21 | } 22 | 23 | func BenchmarkMsgpack(b *testing.B) { 24 | test.BenchmarkEncoding(b, opt) 25 | } 26 | -------------------------------------------------------------------------------- /encoding/protobuf/protobuf.go: -------------------------------------------------------------------------------- 1 | // Package protobuf provides encoding and decoding of Protocol Buffers data. 2 | package protobuf 3 | 4 | import ( 5 | "reflect" 6 | "sync" 7 | 8 | "github.com/abemedia/go-don" 9 | "github.com/abemedia/go-don/encoding" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | var ( 14 | messageType = reflect.TypeOf((*proto.Message)(nil)).Elem() 15 | cache sync.Map 16 | ) 17 | 18 | func unmarshal(b []byte, v any) error { 19 | typ := reflect.TypeOf(v) 20 | fn, ok := cache.Load(typ) 21 | if !ok { 22 | if typ.Elem().Implements(messageType) { 23 | fn = func(v any) any { return reflect.ValueOf(v).Elem().Interface() } 24 | } else { 25 | fn = func(v any) any { return v } 26 | } 27 | cache.Store(typ, fn) 28 | } 29 | 30 | m, ok := fn.(func(v any) any)(v).(proto.Message) 31 | if !ok { 32 | return don.ErrUnsupportedMediaType 33 | } 34 | return proto.Unmarshal(b, m) 35 | } 36 | 37 | func marshal(v any) ([]byte, error) { 38 | m, ok := v.(proto.Message) 39 | if !ok { 40 | return nil, don.ErrNotAcceptable 41 | } 42 | return proto.Marshal(m) 43 | } 44 | 45 | func init() { 46 | mediaType := "application/protobuf" 47 | alias := "application/x-protobuf" 48 | 49 | encoding.RegisterDecoder(unmarshal, mediaType, alias) 50 | encoding.RegisterEncoder(marshal, mediaType, alias) 51 | } 52 | -------------------------------------------------------------------------------- /encoding/protobuf/protobuf_test.go: -------------------------------------------------------------------------------- 1 | package protobuf_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/abemedia/go-don" 8 | "github.com/abemedia/go-don/encoding" 9 | "github.com/abemedia/go-don/encoding/protobuf/testdata" 10 | "github.com/abemedia/go-don/internal/test" 11 | "github.com/abemedia/go-don/pkg/httptest" 12 | ) 13 | 14 | //go:generate protoc testdata/test.proto --go_out=. --go_opt=paths=source_relative 15 | 16 | var opt = test.EncodingOptions[*testdata.Item]{ 17 | Mime: "application/protobuf", 18 | Raw: "\n\x03bar", 19 | Parsed: &testdata.Item{Foo: "bar"}, 20 | } 21 | 22 | func TestProtobuf(t *testing.T) { 23 | test.Encoding(t, opt) 24 | } 25 | 26 | func TestProtobufError(t *testing.T) { 27 | ctx := httptest.NewRequest("", "", "", nil) 28 | v := "test" 29 | 30 | t.Run("Decode", func(t *testing.T) { 31 | dec := encoding.GetDecoder("application/protobuf") 32 | err := dec(ctx, &v) 33 | if !errors.Is(err, don.ErrUnsupportedMediaType) { 34 | t.Fatal("should fail") 35 | } 36 | }) 37 | 38 | t.Run("Encode", func(t *testing.T) { 39 | enc := encoding.GetEncoder("application/protobuf") 40 | err := enc(ctx, &v) 41 | if !errors.Is(err, don.ErrNotAcceptable) { 42 | t.Fatal("should fail") 43 | } 44 | }) 45 | } 46 | 47 | func BenchmarkProtobuf(b *testing.B) { 48 | test.BenchmarkEncoding(b, opt) 49 | } 50 | -------------------------------------------------------------------------------- /encoding/protobuf/testdata/test.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.25.0-devel 4 | // protoc v3.14.0 5 | // source: testdata/test.proto 6 | 7 | package testdata 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type Item struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Foo string `protobuf:"bytes,1,opt,name=foo,proto3" json:"foo,omitempty"` 29 | } 30 | 31 | func (x *Item) Reset() { 32 | *x = Item{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_testdata_test_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *Item) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*Item) ProtoMessage() {} 45 | 46 | func (x *Item) ProtoReflect() protoreflect.Message { 47 | mi := &file_testdata_test_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use Item.ProtoReflect.Descriptor instead. 59 | func (*Item) Descriptor() ([]byte, []int) { 60 | return file_testdata_test_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *Item) GetFoo() string { 64 | if x != nil { 65 | return x.Foo 66 | } 67 | return "" 68 | } 69 | 70 | var File_testdata_test_proto protoreflect.FileDescriptor 71 | 72 | var file_testdata_test_proto_rawDesc = []byte{ 73 | 0x0a, 0x13, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x2e, 74 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x18, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a, 75 | 0x03, 0x66, 0x6f, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x6f, 0x6f, 0x42, 76 | 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x62, 77 | 0x65, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x2f, 0x67, 0x6f, 0x2d, 0x64, 0x6f, 0x6e, 0x2f, 0x65, 0x6e, 78 | 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 79 | 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 80 | } 81 | 82 | var ( 83 | file_testdata_test_proto_rawDescOnce sync.Once 84 | file_testdata_test_proto_rawDescData = file_testdata_test_proto_rawDesc 85 | ) 86 | 87 | func file_testdata_test_proto_rawDescGZIP() []byte { 88 | file_testdata_test_proto_rawDescOnce.Do(func() { 89 | file_testdata_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_testdata_test_proto_rawDescData) 90 | }) 91 | return file_testdata_test_proto_rawDescData 92 | } 93 | 94 | var file_testdata_test_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 95 | var file_testdata_test_proto_goTypes = []interface{}{ 96 | (*Item)(nil), // 0: Item 97 | } 98 | var file_testdata_test_proto_depIdxs = []int32{ 99 | 0, // [0:0] is the sub-list for method output_type 100 | 0, // [0:0] is the sub-list for method input_type 101 | 0, // [0:0] is the sub-list for extension type_name 102 | 0, // [0:0] is the sub-list for extension extendee 103 | 0, // [0:0] is the sub-list for field type_name 104 | } 105 | 106 | func init() { file_testdata_test_proto_init() } 107 | func file_testdata_test_proto_init() { 108 | if File_testdata_test_proto != nil { 109 | return 110 | } 111 | if !protoimpl.UnsafeEnabled { 112 | file_testdata_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 113 | switch v := v.(*Item); i { 114 | case 0: 115 | return &v.state 116 | case 1: 117 | return &v.sizeCache 118 | case 2: 119 | return &v.unknownFields 120 | default: 121 | return nil 122 | } 123 | } 124 | } 125 | type x struct{} 126 | out := protoimpl.TypeBuilder{ 127 | File: protoimpl.DescBuilder{ 128 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 129 | RawDescriptor: file_testdata_test_proto_rawDesc, 130 | NumEnums: 0, 131 | NumMessages: 1, 132 | NumExtensions: 0, 133 | NumServices: 0, 134 | }, 135 | GoTypes: file_testdata_test_proto_goTypes, 136 | DependencyIndexes: file_testdata_test_proto_depIdxs, 137 | MessageInfos: file_testdata_test_proto_msgTypes, 138 | }.Build() 139 | File_testdata_test_proto = out.File 140 | file_testdata_test_proto_rawDesc = nil 141 | file_testdata_test_proto_goTypes = nil 142 | file_testdata_test_proto_depIdxs = nil 143 | } 144 | -------------------------------------------------------------------------------- /encoding/protobuf/testdata/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/abemedia/go-don/encoding/protobuf/testdata"; 4 | 5 | message Item { string foo = 1; } 6 | -------------------------------------------------------------------------------- /encoding/text/decode.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "reflect" 7 | "strconv" 8 | "sync" 9 | 10 | "github.com/abemedia/go-don" 11 | "github.com/abemedia/go-don/internal/byteconv" 12 | "github.com/valyala/fasthttp" 13 | ) 14 | 15 | //nolint:cyclop 16 | func decode(ctx *fasthttp.RequestCtx, v any) error { 17 | b := bytes.TrimSpace(ctx.Request.Body()) 18 | if len(b) == 0 { 19 | return nil 20 | } 21 | 22 | var err error 23 | 24 | switch v := v.(type) { 25 | case *string: 26 | *v = byteconv.Btoa(b) 27 | case *[]byte: 28 | *v = b 29 | case *int: 30 | *v, err = strconv.Atoi(byteconv.Btoa(b)) 31 | case *int8: 32 | return decodeInt(b, v, 8) 33 | case *int16: 34 | return decodeInt(b, v, 16) 35 | case *int32: 36 | return decodeInt(b, v, 32) 37 | case *int64: 38 | return decodeInt(b, v, 64) 39 | case *uint: 40 | return decodeUint(b, v, 0) 41 | case *uint8: 42 | return decodeUint(b, v, 8) 43 | case *uint16: 44 | return decodeUint(b, v, 16) 45 | case *uint32: 46 | return decodeUint(b, v, 32) 47 | case *uint64: 48 | return decodeUint(b, v, 64) 49 | case *float32: 50 | return decodeFloat(b, v, 32) 51 | case *float64: 52 | return decodeFloat(b, v, 64) 53 | case *bool: 54 | *v, err = strconv.ParseBool(byteconv.Btoa(b)) 55 | default: 56 | return unmarshal(b, v) 57 | } 58 | 59 | return err 60 | } 61 | 62 | func decodeInt[T int | int8 | int16 | int32 | int64](b []byte, v *T, bits int) error { 63 | d, err := strconv.ParseInt(byteconv.Btoa(b), 10, bits) 64 | *v = T(d) 65 | return err 66 | } 67 | 68 | func decodeUint[T uint | uint8 | uint16 | uint32 | uint64](b []byte, v *T, bits int) error { 69 | d, err := strconv.ParseUint(byteconv.Btoa(b), 10, bits) 70 | *v = T(d) 71 | return err 72 | } 73 | 74 | func decodeFloat[T float32 | float64](b []byte, v *T, bits int) error { 75 | d, err := strconv.ParseFloat(byteconv.Btoa(b), bits) 76 | *v = T(d) 77 | return err 78 | } 79 | 80 | func unmarshal(b []byte, v any) error { 81 | val := reflect.ValueOf(v) 82 | typ := val.Type() 83 | if dec, ok := unmarshalers.Load(typ); ok { 84 | return dec.(func([]byte, reflect.Value) error)(b, val) //nolint:forcetypeassert 85 | } 86 | dec, err := newUnmarshaler(typ) 87 | if err != nil { 88 | return err 89 | } 90 | unmarshalers.Store(typ, dec) 91 | return dec(b, val) 92 | } 93 | 94 | func newUnmarshaler(typ reflect.Type) (func([]byte, reflect.Value) error, error) { 95 | if typ.Implements(unmarshalerType) { 96 | isPtr := typ.Kind() == reflect.Pointer 97 | typ = typ.Elem() 98 | return func(b []byte, v reflect.Value) error { 99 | if isPtr && v.IsNil() { 100 | v.Set(reflect.New(typ)) 101 | } 102 | return v.Interface().(encoding.TextUnmarshaler).UnmarshalText(b) //nolint:forcetypeassert 103 | }, nil 104 | } 105 | 106 | if typ.Kind() == reflect.Pointer { 107 | typ = typ.Elem() 108 | dec, err := newUnmarshaler(typ) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return func(b []byte, v reflect.Value) error { 113 | if v.IsNil() { 114 | v.Set(reflect.New(typ)) 115 | } 116 | return dec(b, v.Elem()) 117 | }, nil 118 | } 119 | 120 | return nil, don.ErrUnsupportedMediaType 121 | } 122 | 123 | var ( 124 | unmarshalers sync.Map 125 | unmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() 126 | ) 127 | -------------------------------------------------------------------------------- /encoding/text/decode_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "reflect" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/abemedia/go-don" 11 | "github.com/abemedia/go-don/encoding" 12 | "github.com/abemedia/go-don/pkg/httptest" 13 | "github.com/google/go-cmp/cmp" 14 | "github.com/valyala/fasthttp" 15 | ) 16 | 17 | func TestDecode(t *testing.T) { 18 | tests := []struct { 19 | in string 20 | want any 21 | }{ 22 | {" \n", ""}, 23 | {"test\n", "test"}, 24 | {"test\n", []byte("test")}, 25 | {"5\n", int(5)}, 26 | {"5\n", int8(5)}, 27 | {"5\n", int16(5)}, 28 | {"5\n", int32(5)}, 29 | {"5\n", int64(5)}, 30 | {"5\n", uint(5)}, 31 | {"5\n", uint8(5)}, 32 | {"5\n", uint16(5)}, 33 | {"5\n", uint32(5)}, 34 | {"5\n", uint64(5)}, 35 | {"5.1\n", float32(5.1)}, 36 | {"5.1\n", float64(5.1)}, 37 | {"true\n", true}, 38 | {"test\n", unmarshaler{S: "test"}}, 39 | {"test\n", unmarshaler{S: "test"}}, // Test cached unmarshaler. 40 | {"test\n", &unmarshaler{S: "test"}}, 41 | } 42 | 43 | dec := encoding.GetDecoder("text/plain") 44 | if dec == nil { 45 | t.Fatal("decoder not found") 46 | } 47 | 48 | for _, test := range tests { 49 | ctx := httptest.NewRequest(fasthttp.MethodGet, "/", test.in, nil) 50 | v := reflect.New(reflect.TypeOf(test.want)).Interface() 51 | if err := dec(ctx, v); err != nil { 52 | t.Error(err) 53 | } else { 54 | if diff := cmp.Diff(test.want, reflect.ValueOf(v).Elem().Interface()); diff != "" { 55 | t.Errorf("%T: %s", test.want, diff) 56 | } 57 | } 58 | } 59 | } 60 | 61 | func TestDecodeError(t *testing.T) { 62 | tests := []struct { 63 | in string 64 | val any 65 | want error 66 | }{ 67 | {"test\n", &struct{}{}, don.ErrUnsupportedMediaType}, 68 | {"test\n", ptr(0), strconv.ErrSyntax}, 69 | {"test\n", &unmarshaler{Err: io.EOF}, io.EOF}, 70 | } 71 | 72 | dec := encoding.GetDecoder("text/plain") 73 | if dec == nil { 74 | t.Fatal("decoder not found") 75 | } 76 | 77 | for _, test := range tests { 78 | ctx := httptest.NewRequest(fasthttp.MethodGet, "/", test.in, nil) 79 | if err := dec(ctx, test.val); err == nil { 80 | t.Error("should return error") 81 | } else if !errors.Is(err, test.want) { 82 | t.Errorf("should return error %q, got %q", test.want, err) 83 | } 84 | } 85 | } 86 | 87 | func ptr[T any](v T) *T { 88 | return &v 89 | } 90 | 91 | type unmarshaler struct { 92 | S string 93 | Err error 94 | } 95 | 96 | func (m *unmarshaler) UnmarshalText(text []byte) error { 97 | if m.Err != nil { 98 | return m.Err 99 | } 100 | m.S = string(text) 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /encoding/text/encode.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "encoding" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/abemedia/go-don" 9 | "github.com/abemedia/go-don/internal/byteconv" 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | //nolint:cyclop,funlen 14 | func encode(ctx *fasthttp.RequestCtx, v any) error { 15 | if v == nil { 16 | return nil 17 | } 18 | 19 | var ( 20 | b []byte 21 | err error 22 | ) 23 | 24 | switch v := v.(type) { 25 | case string: 26 | b = byteconv.Atob(v) 27 | case []byte: 28 | b = v 29 | case int: 30 | b = strconv.AppendInt(ctx.Response.Body(), int64(v), 10) 31 | case int8: 32 | b = strconv.AppendInt(ctx.Response.Body(), int64(v), 10) 33 | case int16: 34 | b = strconv.AppendInt(ctx.Response.Body(), int64(v), 10) 35 | case int32: 36 | b = strconv.AppendInt(ctx.Response.Body(), int64(v), 10) 37 | case int64: 38 | b = strconv.AppendInt(ctx.Response.Body(), v, 10) 39 | case uint: 40 | b = strconv.AppendUint(ctx.Response.Body(), uint64(v), 10) 41 | case uint8: 42 | b = strconv.AppendUint(ctx.Response.Body(), uint64(v), 10) 43 | case uint16: 44 | b = strconv.AppendUint(ctx.Response.Body(), uint64(v), 10) 45 | case uint32: 46 | b = strconv.AppendUint(ctx.Response.Body(), uint64(v), 10) 47 | case uint64: 48 | b = strconv.AppendUint(ctx.Response.Body(), v, 10) 49 | case float32: 50 | b = strconv.AppendFloat(ctx.Response.Body(), float64(v), 'f', -1, 32) 51 | case float64: 52 | b = strconv.AppendFloat(ctx.Response.Body(), v, 'f', -1, 64) 53 | case bool: 54 | b = strconv.AppendBool(ctx.Response.Body(), v) 55 | case encoding.TextMarshaler: 56 | b, err = v.MarshalText() 57 | case error: 58 | b = byteconv.Atob(v.Error()) 59 | case fmt.Stringer: 60 | b = byteconv.Atob(v.String()) 61 | default: 62 | return don.ErrNotAcceptable 63 | } 64 | 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if len(b) > 0 { 70 | ctx.Response.SetBodyRaw(b) 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /encoding/text/encode_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "testing" 7 | 8 | "github.com/abemedia/go-don" 9 | "github.com/abemedia/go-don/encoding" 10 | "github.com/abemedia/go-don/pkg/httptest" 11 | "github.com/google/go-cmp/cmp" 12 | "github.com/valyala/fasthttp" 13 | ) 14 | 15 | func TestEncode(t *testing.T) { 16 | tests := []struct { 17 | in any 18 | want string 19 | }{ 20 | {"test", "test"}, 21 | {[]byte("test"), "test"}, 22 | {int(5), "5"}, 23 | {int8(5), "5"}, 24 | {int16(5), "5"}, 25 | {int32(5), "5"}, 26 | {int64(5), "5"}, 27 | {uint(5), "5"}, 28 | {uint8(5), "5"}, 29 | {uint16(5), "5"}, 30 | {uint32(5), "5"}, 31 | {uint64(5), "5"}, 32 | {float32(5.1), "5.1"}, 33 | {float64(5.1), "5.1"}, 34 | {true, "true"}, 35 | {errors.New("test"), "test"}, 36 | {marshaler{s: "test"}, "test"}, 37 | {&marshaler{s: "test"}, "test"}, 38 | {stringer{}, "test"}, 39 | {&stringer{}, "test"}, 40 | } 41 | 42 | enc := encoding.GetEncoder("text/plain") 43 | if enc == nil { 44 | t.Fatal("encoder not found") 45 | } 46 | 47 | for _, test := range tests { 48 | ctx := httptest.NewRequest(fasthttp.MethodGet, "/", "", nil) 49 | if err := enc(ctx, test.in); err != nil { 50 | t.Error(err) 51 | } else { 52 | if diff := cmp.Diff(test.want, string(ctx.Response.Body())); diff != "" { 53 | t.Errorf("%T: %s", test.in, diff) 54 | } 55 | } 56 | } 57 | } 58 | 59 | func TestEncodeError(t *testing.T) { 60 | tests := []struct { 61 | in any 62 | want error 63 | }{ 64 | {&struct{}{}, don.ErrNotAcceptable}, 65 | {marshaler{err: io.EOF}, io.EOF}, 66 | } 67 | 68 | enc := encoding.GetEncoder("text/plain") 69 | if enc == nil { 70 | t.Fatal("encoder not found") 71 | } 72 | 73 | for _, test := range tests { 74 | ctx := httptest.NewRequest(fasthttp.MethodGet, "/", "", nil) 75 | if err := enc(ctx, test.in); err == nil { 76 | t.Error("should return error") 77 | } else if !errors.Is(err, test.want) { 78 | t.Errorf("should return error %q, got %q", test.want, err) 79 | } 80 | } 81 | } 82 | 83 | type marshaler struct { 84 | s string 85 | err error 86 | } 87 | 88 | func (m marshaler) MarshalText() ([]byte, error) { 89 | if m.err != nil { 90 | return nil, m.err 91 | } 92 | return []byte(m.s), nil 93 | } 94 | 95 | type stringer struct{} 96 | 97 | func (m stringer) String() string { 98 | return "test" 99 | } 100 | -------------------------------------------------------------------------------- /encoding/text/text.go: -------------------------------------------------------------------------------- 1 | // Package text provides encoding and decoding of plain text data. 2 | package text 3 | 4 | import ( 5 | "github.com/abemedia/go-don/encoding" 6 | ) 7 | 8 | func init() { 9 | mediaType := "text/plain" 10 | 11 | encoding.RegisterDecoder(decode, mediaType) 12 | encoding.RegisterEncoder(encode, mediaType) 13 | } 14 | -------------------------------------------------------------------------------- /encoding/text/text_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/internal/test" 7 | ) 8 | 9 | var opt = test.EncodingOptions[string]{ 10 | Mime: "text/plain", 11 | Raw: "foo", 12 | Parsed: "foo", 13 | } 14 | 15 | func TestText(t *testing.T) { 16 | test.Encoding(t, opt) 17 | } 18 | 19 | func BenchmarkText(b *testing.B) { 20 | test.BenchmarkEncoding(b, opt) 21 | } 22 | -------------------------------------------------------------------------------- /encoding/toml/toml.go: -------------------------------------------------------------------------------- 1 | // Package toml provides encoding and decoding of TOML data. 2 | package toml 3 | 4 | import ( 5 | "github.com/abemedia/go-don/encoding" 6 | "github.com/pelletier/go-toml" 7 | ) 8 | 9 | func init() { 10 | mediaType := "application/toml" 11 | 12 | encoding.RegisterDecoder(toml.Unmarshal, mediaType) 13 | encoding.RegisterEncoder(toml.Marshal, mediaType) 14 | } 15 | -------------------------------------------------------------------------------- /encoding/toml/toml_test.go: -------------------------------------------------------------------------------- 1 | package toml_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/internal/test" 7 | ) 8 | 9 | type item struct { 10 | Foo string `toml:"foo"` 11 | } 12 | 13 | var opt = test.EncodingOptions[item]{ 14 | Mime: "application/toml", 15 | Raw: `foo = "bar"` + "\n", 16 | Parsed: item{Foo: "bar"}, 17 | } 18 | 19 | func TestTOML(t *testing.T) { 20 | test.Encoding(t, opt) 21 | } 22 | 23 | func BenchmarkTOML(b *testing.B) { 24 | test.BenchmarkEncoding(b, opt) 25 | } 26 | -------------------------------------------------------------------------------- /encoding/xml/xml.go: -------------------------------------------------------------------------------- 1 | // Package xml provides encoding and decoding of XML data. 2 | package xml 3 | 4 | import ( 5 | "encoding/xml" 6 | 7 | "github.com/abemedia/go-don/encoding" 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | func decodeXML(ctx *fasthttp.RequestCtx, v any) error { 12 | return xml.NewDecoder(ctx.RequestBodyStream()).Decode(v) 13 | } 14 | 15 | func encodeXML(ctx *fasthttp.RequestCtx, v any) error { 16 | return xml.NewEncoder(ctx).Encode(v) 17 | } 18 | 19 | func init() { 20 | mediaType := "application/xml" 21 | alias := "text/xml" 22 | 23 | encoding.RegisterDecoder(decodeXML, mediaType, alias) 24 | encoding.RegisterEncoder(encodeXML, mediaType, alias) 25 | } 26 | -------------------------------------------------------------------------------- /encoding/xml/xml_test.go: -------------------------------------------------------------------------------- 1 | package xml_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/internal/test" 7 | ) 8 | 9 | type item struct { 10 | Foo string `xml:"foo"` 11 | } 12 | 13 | var opt = test.EncodingOptions[item]{ 14 | Mime: "application/xml", 15 | Raw: "bar", 16 | Parsed: item{Foo: "bar"}, 17 | } 18 | 19 | func TestXML(t *testing.T) { 20 | test.Encoding(t, opt) 21 | } 22 | 23 | func BenchmarkXML(b *testing.B) { 24 | test.BenchmarkEncoding(b, opt) 25 | } 26 | -------------------------------------------------------------------------------- /encoding/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | // Package yaml provides encoding and decoding of YAML data. 2 | package yaml 3 | 4 | import ( 5 | "github.com/abemedia/go-don/encoding" 6 | "github.com/valyala/fasthttp" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func decodeYAML(ctx *fasthttp.RequestCtx, v any) error { 11 | return yaml.NewDecoder(ctx.RequestBodyStream()).Decode(v) 12 | } 13 | 14 | func encodeYAML(ctx *fasthttp.RequestCtx, v any) error { 15 | return yaml.NewEncoder(ctx).Encode(v) 16 | } 17 | 18 | func init() { 19 | mediaType := "application/yaml" 20 | aliases := []string{"text/yaml", "application/x-yaml", "text/x-yaml", "text/vnd.yaml"} 21 | 22 | encoding.RegisterDecoder(decodeYAML, mediaType, aliases...) 23 | encoding.RegisterEncoder(encodeYAML, mediaType, aliases...) 24 | } 25 | -------------------------------------------------------------------------------- /encoding/yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don/internal/test" 7 | ) 8 | 9 | type item struct { 10 | Foo string `yaml:"foo"` 11 | } 12 | 13 | var opt = test.EncodingOptions[item]{ 14 | Mime: "application/yaml", 15 | Raw: "foo: bar\n", 16 | Parsed: item{Foo: "bar"}, 17 | } 18 | 19 | func TestYAML(t *testing.T) { 20 | test.Encoding(t, opt) 21 | } 22 | 23 | func BenchmarkYAML(b *testing.B) { 24 | test.BenchmarkEncoding(b, opt) 25 | } 26 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package don 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding" 7 | "encoding/xml" 8 | "errors" 9 | "strconv" 10 | 11 | "github.com/abemedia/go-don/internal/byteconv" 12 | "github.com/goccy/go-json" 13 | "github.com/valyala/fasthttp" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | func E(err error) fasthttp.RequestHandler { 18 | h := H(func(context.Context, any) (any, error) { return nil, err }) 19 | return func(ctx *fasthttp.RequestCtx) { h(ctx, nil) } 20 | } 21 | 22 | type HTTPError struct { 23 | err error 24 | code int 25 | } 26 | 27 | func Error(err error, code int) *HTTPError { 28 | return &HTTPError{err, code} 29 | } 30 | 31 | func (e *HTTPError) Error() string { 32 | return e.err.Error() 33 | } 34 | 35 | func (e *HTTPError) Is(err error) bool { 36 | return errors.Is(e.err, err) || errors.Is(StatusError(e.code), err) 37 | } 38 | 39 | func (e *HTTPError) Unwrap() error { 40 | return e.err 41 | } 42 | 43 | func (e *HTTPError) StatusCode() int { 44 | if e.code != 0 { 45 | return e.code 46 | } 47 | 48 | var sc StatusCoder 49 | if errors.As(e.err, &sc) { 50 | return sc.StatusCode() 51 | } 52 | 53 | return fasthttp.StatusInternalServerError 54 | } 55 | 56 | func (e *HTTPError) MarshalText() ([]byte, error) { 57 | var m encoding.TextMarshaler 58 | if errors.As(e.err, &m) { 59 | return m.MarshalText() 60 | } 61 | 62 | return byteconv.Atob(e.Error()), nil 63 | } 64 | 65 | func (e *HTTPError) MarshalJSON() ([]byte, error) { 66 | var m json.Marshaler 67 | if errors.As(e.err, &m) { 68 | return m.MarshalJSON() 69 | } 70 | 71 | var buf bytes.Buffer 72 | buf.WriteString(`{"message":`) 73 | buf.WriteString(strconv.Quote(e.Error())) 74 | buf.WriteRune('}') 75 | 76 | return buf.Bytes(), nil 77 | } 78 | 79 | func (e *HTTPError) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { 80 | var m xml.Marshaler 81 | if errors.As(e.err, &m) { 82 | return m.MarshalXML(enc, start) 83 | } 84 | 85 | start = xml.StartElement{Name: xml.Name{Local: "message"}} 86 | return enc.EncodeElement(e.Error(), start) 87 | } 88 | 89 | func (e *HTTPError) MarshalYAML() (any, error) { 90 | var m yaml.Marshaler 91 | if errors.As(e.err, &m) { 92 | return m.MarshalYAML() 93 | } 94 | 95 | return map[string]string{"message": e.Error()}, nil 96 | } 97 | 98 | var ( 99 | _ error = (*HTTPError)(nil) 100 | _ encoding.TextMarshaler = (*HTTPError)(nil) 101 | _ json.Marshaler = (*HTTPError)(nil) 102 | _ xml.Marshaler = (*HTTPError)(nil) 103 | _ yaml.Marshaler = (*HTTPError)(nil) 104 | ) 105 | 106 | // StatusError creates an error from an HTTP status code. 107 | type StatusError int 108 | 109 | const ( 110 | ErrBadRequest = StatusError(fasthttp.StatusBadRequest) 111 | ErrUnauthorized = StatusError(fasthttp.StatusUnauthorized) 112 | ErrForbidden = StatusError(fasthttp.StatusForbidden) 113 | ErrNotFound = StatusError(fasthttp.StatusNotFound) 114 | ErrMethodNotAllowed = StatusError(fasthttp.StatusMethodNotAllowed) 115 | ErrNotAcceptable = StatusError(fasthttp.StatusNotAcceptable) 116 | ErrConflict = StatusError(fasthttp.StatusConflict) 117 | ErrUnsupportedMediaType = StatusError(fasthttp.StatusUnsupportedMediaType) 118 | ErrInternalServerError = StatusError(fasthttp.StatusInternalServerError) 119 | ) 120 | 121 | func (e StatusError) Error() string { 122 | return fasthttp.StatusMessage(int(e)) 123 | } 124 | 125 | func (e StatusError) StatusCode() int { 126 | return int(e) 127 | } 128 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package don_test 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/abemedia/go-don" 10 | _ "github.com/abemedia/go-don/encoding/json" 11 | _ "github.com/abemedia/go-don/encoding/text" 12 | _ "github.com/abemedia/go-don/encoding/xml" 13 | _ "github.com/abemedia/go-don/encoding/yaml" 14 | "github.com/goccy/go-json" 15 | "github.com/google/go-cmp/cmp" 16 | "github.com/valyala/fasthttp" 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | func TestError_Is(t *testing.T) { 21 | if !errors.Is(don.Error(errTest, 0), errTest) { 22 | t.Error("should match wrapped error") 23 | } 24 | if !errors.Is(don.Error(errTest, fasthttp.StatusBadRequest), don.ErrBadRequest) { 25 | t.Error("should match status error") 26 | } 27 | } 28 | 29 | func TestError_Unwrap(t *testing.T) { 30 | if errors.Unwrap(don.Error(errTest, 0)) != errTest { //nolint:errorlint 31 | t.Error("should unwrap wrapped error") 32 | } 33 | } 34 | 35 | func TestError_StatusCode(t *testing.T) { 36 | if don.Error(don.ErrBadRequest, 0).StatusCode() != fasthttp.StatusBadRequest { 37 | t.Error("should respect wrapped error's status code") 38 | } 39 | if don.Error(don.ErrBadRequest, fasthttp.StatusConflict).StatusCode() != fasthttp.StatusConflict { 40 | t.Error("should ignore wrapped error's status code if explicitly set") 41 | } 42 | } 43 | 44 | func TestError_MarshalText(t *testing.T) { 45 | b, _ := don.Error(errTest, 0).MarshalText() 46 | if diff := cmp.Diff("test", string(b)); diff != "" { 47 | t.Error(diff) 48 | } 49 | b, _ = don.Error(&testError{}, 0).MarshalText() 50 | if diff := cmp.Diff("custom", string(b)); diff != "" { 51 | t.Error(diff) 52 | } 53 | } 54 | 55 | func TestError_MarshalJSON(t *testing.T) { 56 | b, _ := json.Marshal(don.Error(errTest, 0)) 57 | if diff := cmp.Diff(`{"message":"test"}`, string(b)); diff != "" { 58 | t.Error(diff) 59 | } 60 | b, _ = json.Marshal(don.Error(&testError{}, 0)) 61 | if diff := cmp.Diff(`{"custom":"test"}`, string(b)); diff != "" { 62 | t.Error(diff) 63 | } 64 | } 65 | 66 | func TestError_MarshalXML(t *testing.T) { 67 | b, _ := xml.Marshal(don.Error(errTest, 0)) 68 | if diff := cmp.Diff("test", string(b)); diff != "" { 69 | t.Error(diff) 70 | } 71 | b, _ = xml.Marshal(don.Error(&testError{}, 0)) 72 | if diff := cmp.Diff("test", string(b)); diff != "" { 73 | t.Error(diff) 74 | } 75 | } 76 | 77 | func TestError_MarshalYAML(t *testing.T) { 78 | b, _ := yaml.Marshal(don.Error(errTest, 0)) 79 | if diff := cmp.Diff("message: test\n", string(b)); diff != "" { 80 | t.Error(diff) 81 | } 82 | b, _ = yaml.Marshal(don.Error(&testError{}, 0)) 83 | if diff := cmp.Diff("custom: test\n", string(b)); diff != "" { 84 | t.Error(diff) 85 | } 86 | } 87 | 88 | var errTest = errors.New("test") 89 | 90 | type testError struct{} 91 | 92 | func (e *testError) Error() string { 93 | return "test" 94 | } 95 | 96 | func (e *testError) MarshalText() ([]byte, error) { 97 | return []byte("custom"), nil 98 | } 99 | 100 | func (e *testError) MarshalJSON() ([]byte, error) { 101 | return []byte(fmt.Sprintf(`{"custom":%q}`, e.Error())), nil 102 | } 103 | 104 | func (e *testError) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { 105 | start.Name = xml.Name{Local: "custom"} 106 | return enc.EncodeElement(e.Error(), start) 107 | } 108 | 109 | func (e *testError) MarshalYAML() (any, error) { 110 | return map[string]string{"custom": e.Error()}, nil 111 | } 112 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/abemedia/go-don" 10 | _ "github.com/abemedia/go-don/encoding/json" 11 | _ "github.com/abemedia/go-don/encoding/text" 12 | _ "github.com/abemedia/go-don/encoding/yaml" 13 | ) 14 | 15 | // Returns 204 - No Content. 16 | func Empty(context.Context, any) (any, error) { 17 | return nil, nil 18 | } 19 | 20 | func Ping(context.Context, any) (string, error) { 21 | return "pong", nil 22 | } 23 | 24 | type GreetRequest struct { 25 | Name string `json:"name"` // Get name from JSON body. 26 | Age int `header:"X-User-Age"` // Get age from HTTP header. 27 | } 28 | 29 | type GreetResponse struct { 30 | Greeting string `json:"data"` 31 | } 32 | 33 | // Set a custom HTTP response code. 34 | func (gr *GreetResponse) StatusCode() int { 35 | return http.StatusTeapot 36 | } 37 | 38 | // Add custom headers to the response. 39 | func (gr *GreetResponse) Header() http.Header { 40 | header := http.Header{} 41 | header.Set("Foo", "bar") 42 | return header 43 | } 44 | 45 | func Greet(_ context.Context, req *GreetRequest) (*GreetResponse, error) { 46 | if req.Name == "" { 47 | return nil, don.ErrBadRequest 48 | } 49 | 50 | res := &GreetResponse{ 51 | Greeting: fmt.Sprintf("Hello %s, you're %d years old.", req.Name, req.Age), 52 | } 53 | 54 | return res, nil 55 | } 56 | 57 | func main() { 58 | r := don.New(nil) 59 | r.Get("/", don.H(Empty)) 60 | 61 | g := r.Group("/api") 62 | g.Get("/ping", don.H(Ping)) 63 | g.Post("/greet", don.H(Greet)) 64 | 65 | log.Fatal(r.ListenAndServe(":8080")) 66 | } 67 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package don 2 | 3 | var NewNilCheck = newNilCheck 4 | 5 | func NewRequestPool[T any](v T) pool[T] { 6 | return newRequestPool(v) 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abemedia/go-don 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/abemedia/httprouter v0.0.0-20230505023925-232e0e5a4b1b 7 | github.com/goccy/go-json v0.10.2 8 | github.com/google/go-cmp v0.6.0 9 | github.com/pelletier/go-toml v1.9.5 10 | github.com/valyala/fasthttp v1.52.0 11 | github.com/vmihailenco/msgpack/v5 v5.4.1 12 | google.golang.org/protobuf v1.33.0 13 | gopkg.in/yaml.v3 v3.0.1 14 | ) 15 | 16 | require ( 17 | github.com/abemedia/fasthttpfs v0.0.0-20220405193636-731805b0c723 // indirect 18 | github.com/andybalholm/brotli v1.1.0 // indirect 19 | github.com/klauspost/compress v1.17.6 // indirect 20 | github.com/valyala/bytebufferpool v1.0.0 // indirect 21 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/abemedia/fasthttpfs v0.0.0-20220321013016-a7e6ad30856d/go.mod h1:Q7fRPwbRn+E/hqQEU2ZnMbp9juhSZBcSl3/aQrr+apQ= 2 | github.com/abemedia/fasthttpfs v0.0.0-20220405193636-731805b0c723 h1:gq2jKsEsHvR+R0InqnbxLQ5/L2bRTfXieNLAMhQir3I= 3 | github.com/abemedia/fasthttpfs v0.0.0-20220405193636-731805b0c723/go.mod h1:Q7fRPwbRn+E/hqQEU2ZnMbp9juhSZBcSl3/aQrr+apQ= 4 | github.com/abemedia/httprouter v0.0.0-20230505023925-232e0e5a4b1b h1:/0hlaEP0jHdeE/SxFO0fT6AdHUtaQp0rbOkxydJ2Bsc= 5 | github.com/abemedia/httprouter v0.0.0-20230505023925-232e0e5a4b1b/go.mod h1:jMATPO/ttg245+tl+jaLzgZs03RS9coyM87qNPzk594= 6 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 7 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 8 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 9 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 10 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 11 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 12 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 16 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 17 | github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 18 | github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= 19 | github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 20 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 21 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 24 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 25 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 26 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 27 | github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 28 | github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= 29 | github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= 30 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 31 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 32 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 33 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 34 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 35 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 38 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 39 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 40 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 41 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 44 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 45 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 46 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 47 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 48 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 49 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 51 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 63 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 64 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 65 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 68 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 69 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 70 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 71 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 72 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 73 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 74 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 75 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 76 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 77 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 78 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 82 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 83 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package don 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/abemedia/go-don/internal/byteconv" 8 | "github.com/abemedia/httprouter" 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | type group struct { 13 | r *API 14 | prefix string 15 | } 16 | 17 | func (g *group) Get(path string, handle httprouter.Handle) { 18 | g.Handle(http.MethodGet, path, handle) 19 | } 20 | 21 | func (g *group) Post(path string, handle httprouter.Handle) { 22 | g.Handle(http.MethodPost, path, handle) 23 | } 24 | 25 | func (g *group) Put(path string, handle httprouter.Handle) { 26 | g.Handle(http.MethodPut, path, handle) 27 | } 28 | 29 | func (g *group) Patch(path string, handle httprouter.Handle) { 30 | g.Handle(http.MethodPatch, path, handle) 31 | } 32 | 33 | func (g *group) Delete(path string, handle httprouter.Handle) { 34 | g.Handle(http.MethodDelete, path, handle) 35 | } 36 | 37 | func (g *group) Handle(method, path string, handle httprouter.Handle) { 38 | g.r.Handle(method, g.prefix+path, handle) 39 | } 40 | 41 | func (g *group) Handler(method, path string, handle http.Handler) { 42 | g.r.Handler(method, g.prefix+path, handle) 43 | } 44 | 45 | func (g *group) HandleFunc(method, path string, handle http.HandlerFunc) { 46 | g.Handler(method, path, handle) 47 | } 48 | 49 | func (g *group) Group(path string) Router { 50 | return &group{prefix: g.prefix + path, r: g.r} 51 | } 52 | 53 | func (g *group) Use(mw ...Middleware) { 54 | g.r.Use(func(next fasthttp.RequestHandler) fasthttp.RequestHandler { 55 | mwNext := next 56 | for _, fn := range mw { 57 | mwNext = fn(mwNext) 58 | } 59 | 60 | return func(ctx *fasthttp.RequestCtx) { 61 | // Only use the middleware if path belongs to group. 62 | if strings.HasPrefix(byteconv.Btoa(ctx.Path()), g.prefix) { 63 | mwNext(ctx) 64 | } else { 65 | next(ctx) 66 | } 67 | } 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | package don_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/abemedia/go-don" 8 | _ "github.com/abemedia/go-don/encoding/text" 9 | "github.com/abemedia/go-don/internal/test" 10 | "github.com/abemedia/httprouter" 11 | "github.com/valyala/fasthttp" 12 | ) 13 | 14 | func TestGroup(t *testing.T) { 15 | api := don.New(nil) 16 | group := api.Group("/group") 17 | 18 | test.Router(t, group, api.RequestHandler(), "/group") 19 | test.Router(t, group.Group("/sub"), api.RequestHandler(), "/group/sub") 20 | test.Router(t, group.Group("sub"), api.RequestHandler(), "/groupsub") 21 | } 22 | 23 | func TestGroup_Use(t *testing.T) { 24 | mwCalled := false 25 | 26 | api := don.New(nil) 27 | api.Get("/", func(*fasthttp.RequestCtx, httprouter.Params) {}) 28 | 29 | group := api.Group("/group") 30 | group.Get("/foo", func(*fasthttp.RequestCtx, httprouter.Params) {}) 31 | group.Use(func(next fasthttp.RequestHandler) fasthttp.RequestHandler { 32 | return func(ctx *fasthttp.RequestCtx) { 33 | if strings.HasPrefix(string(ctx.Path()), "/group/") { 34 | mwCalled = true 35 | } else { 36 | t.Error("middleware called outside of group") 37 | } 38 | } 39 | }) 40 | 41 | h := api.RequestHandler() 42 | 43 | urls := []string{"/", "/group/foo"} 44 | for _, url := range urls { 45 | ctx := &fasthttp.RequestCtx{} 46 | ctx.Request.Header.SetMethod(fasthttp.MethodGet) 47 | ctx.Request.SetRequestURI(url) 48 | 49 | h(ctx) 50 | 51 | if ctx.Response.StatusCode() >= 300 { 52 | t.Errorf("expected success status got %d", ctx.Response.Header.StatusCode()) 53 | } 54 | } 55 | 56 | if !mwCalled { 57 | t.Error("group middleware wasn't called") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package don 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net/http" 7 | 8 | "github.com/abemedia/go-don/encoding" 9 | "github.com/abemedia/go-don/internal/byteconv" 10 | "github.com/abemedia/httprouter" 11 | "github.com/valyala/fasthttp" 12 | ) 13 | 14 | // StatusCoder allows you to customise the HTTP response code. 15 | type StatusCoder interface { 16 | StatusCode() int 17 | } 18 | 19 | // Headerer allows you to customise the HTTP headers. 20 | type Headerer interface { 21 | Header() http.Header 22 | } 23 | 24 | // Handle is the type for your handlers. 25 | type Handle[T, O any] func(ctx context.Context, request T) (O, error) 26 | 27 | // H wraps your handler function with the Go generics magic. 28 | func H[T, O any](handle Handle[T, O]) httprouter.Handle { 29 | pool := newRequestPool(*new(T)) 30 | decodeRequest := newRequestDecoder(*new(T)) 31 | isNil := newNilCheck(*new(O)) 32 | 33 | return func(ctx *fasthttp.RequestCtx, p httprouter.Params) { 34 | contentType := getMediaType(ctx.Request.Header.Peek(fasthttp.HeaderAccept)) 35 | 36 | enc := encoding.GetEncoder(contentType) 37 | if enc == nil { 38 | handleError(ctx, ErrNotAcceptable) 39 | return 40 | } 41 | 42 | var res any 43 | 44 | req := pool.Get() 45 | err := decodeRequest(req, ctx, p) 46 | if err != nil { 47 | res = Error(err, getStatusCode(err, http.StatusBadRequest)) 48 | } else { 49 | res, err = handle(ctx, *req) 50 | if err != nil { 51 | res = Error(err, 0) 52 | } 53 | } 54 | pool.Put(req) 55 | 56 | ctx.SetContentType(contentType + "; charset=utf-8") 57 | 58 | if h, ok := res.(Headerer); ok { 59 | for k, v := range h.Header() { 60 | ctx.Response.Header.Set(k, v[0]) 61 | } 62 | } 63 | 64 | if sc, ok := res.(StatusCoder); ok { 65 | ctx.SetStatusCode(sc.StatusCode()) 66 | } 67 | 68 | if err == nil && isNil(res) { 69 | res = nil 70 | ctx.Response.Header.SetContentLength(-3) 71 | } 72 | 73 | if err = enc(ctx, res); err != nil { 74 | handleError(ctx, err) 75 | } 76 | } 77 | } 78 | 79 | func handleError(ctx *fasthttp.RequestCtx, err error) { 80 | code := getStatusCode(err, http.StatusInternalServerError) 81 | if code < http.StatusInternalServerError { 82 | ctx.Error(err.Error(), code) 83 | return 84 | } 85 | ctx.Error(fasthttp.StatusMessage(code), code) 86 | ctx.Logger().Printf("%v", err) 87 | } 88 | 89 | func getMediaType(b []byte) string { 90 | index := bytes.IndexRune(b, ';') 91 | if index > 0 { 92 | b = b[:index] 93 | } 94 | 95 | return byteconv.Btoa(bytes.TrimSpace(b)) 96 | } 97 | 98 | func getStatusCode(i any, fallback int) int { 99 | if sc, ok := i.(StatusCoder); ok { 100 | return sc.StatusCode() 101 | } 102 | return fallback 103 | } 104 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package don_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/abemedia/go-don" 11 | _ "github.com/abemedia/go-don/encoding/json" 12 | _ "github.com/abemedia/go-don/encoding/text" 13 | "github.com/abemedia/go-don/pkg/httptest" 14 | "github.com/abemedia/httprouter" 15 | "github.com/google/go-cmp/cmp" 16 | "github.com/valyala/fasthttp" 17 | ) 18 | 19 | func TestHandlerRequest(t *testing.T) { 20 | type request struct { 21 | Path string `path:"path"` 22 | Header string `header:"Header"` 23 | Query string `query:"query"` 24 | JSON string `json:"json"` 25 | } 26 | 27 | var got request 28 | 29 | want := request{ 30 | Path: "path", 31 | Header: "header", 32 | Query: "query", 33 | JSON: "json", 34 | } 35 | 36 | api := don.New(&don.Config{DefaultEncoding: "application/json"}) 37 | 38 | api.Post("/:path", don.H(func(ctx context.Context, req request) (any, error) { 39 | got = req 40 | return nil, nil 41 | })) 42 | 43 | api.RequestHandler()(httptest.NewRequest( 44 | fasthttp.MethodPost, 45 | "/path?query=query", 46 | `{"json":"json"}`, 47 | map[string]string{"header": "header"}, 48 | )) 49 | 50 | if diff := cmp.Diff(want, got); diff != "" { 51 | t.Error(diff) 52 | } 53 | } 54 | 55 | func TestHandlerResponse(t *testing.T) { 56 | type request struct { 57 | url string 58 | body string 59 | header map[string]string 60 | } 61 | 62 | type response struct { 63 | Code int 64 | Body string 65 | Header map[string]string 66 | } 67 | 68 | tests := []struct { 69 | message string 70 | want response 71 | config *don.Config 72 | route string 73 | handler httprouter.Handle 74 | request request 75 | }{ 76 | { 77 | message: "should return no content", 78 | want: response{ 79 | Code: fasthttp.StatusNoContent, 80 | Body: "", 81 | Header: map[string]string{ 82 | "Content-Type": "text/plain; charset=utf-8", 83 | }, 84 | }, 85 | handler: don.H(func(ctx context.Context, req any) (any, error) { 86 | return nil, nil 87 | }), 88 | }, 89 | { 90 | message: "should return null", 91 | want: response{ 92 | Code: fasthttp.StatusOK, 93 | Body: "null", 94 | Header: map[string]string{ 95 | "Content-Type": "application/json; charset=utf-8", 96 | }, 97 | }, 98 | config: &don.Config{DefaultEncoding: "application/json", DisableNoContent: true}, 99 | handler: don.H(func(ctx context.Context, req any) (any, error) { 100 | return nil, nil 101 | }), 102 | }, 103 | { 104 | message: "should set response headers", 105 | want: response{ 106 | Code: fasthttp.StatusOK, 107 | Header: map[string]string{ 108 | "Content-Type": "text/plain; charset=utf-8", 109 | "Foo": "bar", 110 | }, 111 | }, 112 | handler: don.H(func(ctx context.Context, req any) (any, error) { 113 | return &headerer{}, nil 114 | }), 115 | }, 116 | { 117 | message: "should return error on unprocessable request", 118 | want: response{ 119 | Code: fasthttp.StatusUnsupportedMediaType, 120 | Body: "Unsupported Media Type", 121 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 122 | }, 123 | handler: don.H(func(ctx context.Context, req struct{ Hello string }) (any, error) { 124 | return nil, nil 125 | }), 126 | request: request{body: `{"foo":"bar"}`}, 127 | }, 128 | { 129 | message: "should return error on unacceptable", 130 | want: response{ 131 | Code: fasthttp.StatusNotAcceptable, 132 | Body: "Not Acceptable", 133 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 134 | }, 135 | handler: don.H(func(ctx context.Context, req any) ([]string, error) { 136 | return []string{"foo", "bar"}, nil 137 | }), 138 | request: request{header: map[string]string{"Accept": "text/plain; charset=utf-8"}}, 139 | }, 140 | { 141 | message: "should return error on unsupported accept", 142 | want: response{ 143 | Code: fasthttp.StatusNotAcceptable, 144 | Body: "Not Acceptable", 145 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 146 | }, 147 | handler: don.H(func(ctx context.Context, req any) (any, error) { 148 | return nil, nil 149 | }), 150 | request: request{header: map[string]string{"Accept": "application/msword"}}, 151 | }, 152 | { 153 | message: "should return error on unsupported content type", 154 | want: response{ 155 | Code: fasthttp.StatusUnsupportedMediaType, 156 | Body: "Unsupported Media Type", 157 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 158 | }, 159 | handler: don.H(func(ctx context.Context, req any) (any, error) { 160 | return nil, nil 161 | }), 162 | request: request{ 163 | body: `foo`, 164 | header: map[string]string{"Content-Type": "application/msword"}, 165 | }, 166 | }, 167 | { 168 | message: "should return error on invalid query", 169 | want: response{ 170 | Code: fasthttp.StatusBadRequest, 171 | Body: "strconv.ParseInt: parsing \"foo\": invalid syntax", 172 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 173 | }, 174 | handler: don.H(func(ctx context.Context, req struct { 175 | Test int `query:"test"` 176 | }, 177 | ) (any, error) { 178 | return req, nil 179 | }), 180 | request: request{url: "/?test=foo"}, 181 | }, 182 | { 183 | message: "should return error on invalid header", 184 | want: response{ 185 | Code: fasthttp.StatusBadRequest, 186 | Body: "strconv.ParseInt: parsing \"foo\": invalid syntax", 187 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 188 | }, 189 | handler: don.H(func(ctx context.Context, req struct { 190 | Test int `header:"Test"` 191 | }, 192 | ) (any, error) { 193 | return req, nil 194 | }), 195 | request: request{header: map[string]string{"Test": "foo"}}, 196 | }, 197 | { 198 | message: "should return error on invalid path element", 199 | want: response{ 200 | Code: fasthttp.StatusNotFound, 201 | Body: "Not Found", 202 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 203 | }, 204 | handler: don.H(func(ctx context.Context, req struct { 205 | Test int `path:"test"` 206 | }, 207 | ) (any, error) { 208 | return req, nil 209 | }), 210 | route: "/:test", 211 | request: request{url: "/foo"}, 212 | }, 213 | { 214 | message: "should return error on invalid body", 215 | want: response{ 216 | Code: fasthttp.StatusBadRequest, 217 | Body: "strconv.Atoi: parsing \"foo\": invalid syntax", 218 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 219 | }, 220 | handler: don.H(func(ctx context.Context, req int) (any, error) { 221 | return req, nil 222 | }), 223 | request: request{body: "foo"}, 224 | }, 225 | { 226 | message: "should return internal server error", 227 | want: response{ 228 | Code: fasthttp.StatusInternalServerError, 229 | Body: "test", 230 | Header: map[string]string{"Content-Type": "text/plain; charset=utf-8"}, 231 | }, 232 | handler: don.H(func(ctx context.Context, req any) (any, error) { 233 | return nil, errors.New("test") 234 | }), 235 | }, 236 | } 237 | 238 | for _, test := range tests { 239 | ctx := httptest.NewRequest(fasthttp.MethodPost, test.request.url, test.request.body, test.request.header) 240 | 241 | api := don.New(test.config) 242 | api.Post("/"+strings.TrimPrefix(test.route, "/"), test.handler) 243 | api.RequestHandler()(ctx) 244 | 245 | res := response{ctx.Response.StatusCode(), string(ctx.Response.Body()), map[string]string{}} 246 | ctx.Response.Header.VisitAll(func(key, value []byte) { res.Header[string(key)] = string(value) }) 247 | 248 | if diff := cmp.Diff(test.want, res); diff != "" { 249 | t.Errorf("%s:\n%s", test.message, diff) 250 | } 251 | } 252 | } 253 | 254 | type headerer struct{} 255 | 256 | func (h *headerer) String() string { 257 | return "" 258 | } 259 | 260 | func (h *headerer) Header() http.Header { 261 | return http.Header{"Foo": []string{"bar"}} 262 | } 263 | 264 | func BenchmarkHandler(b *testing.B) { 265 | type request struct { 266 | Path string `path:"path"` 267 | Header string `header:"Header"` 268 | Query string `query:"query"` 269 | } 270 | 271 | header := map[string]string{"Header": "header", "Accept": "text/plain"} 272 | ctx := httptest.NewRequest("POST", "/path?query=query", "", header) 273 | p := httprouter.Params{{Key: "path", Value: "path"}} 274 | 275 | b.Run("Request", func(b *testing.B) { 276 | h := don.H(func(ctx context.Context, req request) (any, error) { return nil, nil }) 277 | for i := 0; i < b.N; i++ { 278 | h(ctx, p) 279 | } 280 | }) 281 | 282 | b.Run("RequestPointer", func(b *testing.B) { 283 | h := don.H(func(ctx context.Context, req *request) (any, error) { return nil, nil }) 284 | for i := 0; i < b.N; i++ { 285 | h(ctx, p) 286 | } 287 | }) 288 | } 289 | -------------------------------------------------------------------------------- /internal/byteconv/byteconv.go: -------------------------------------------------------------------------------- 1 | // Package byteconv provides fast and efficient conversion functions for byte slices and strings. 2 | package byteconv 3 | 4 | import ( 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | // Btoa returns a string from a byte slice without memory allocation. 10 | func Btoa(b []byte) string { 11 | return *(*string)(unsafe.Pointer(&b)) 12 | } 13 | 14 | // Atob returns a byte slice from a string without memory allocation. 15 | func Atob(s string) []byte { 16 | sp := unsafe.Pointer(&s) 17 | b := *(*[]byte)(sp) 18 | (*reflect.SliceHeader)(unsafe.Pointer(&b)).Cap = (*reflect.StringHeader)(sp).Len 19 | return b 20 | } 21 | -------------------------------------------------------------------------------- /internal/byteconv/byteconv_test.go: -------------------------------------------------------------------------------- 1 | package byteconv_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/abemedia/go-don/internal/byteconv" 8 | ) 9 | 10 | func TestBtoa(t *testing.T) { 11 | if byteconv.Btoa([]byte("test")) != "test" { 12 | t.Error("should be equal") 13 | } 14 | } 15 | 16 | func TestAtob(t *testing.T) { 17 | if !bytes.Equal(byteconv.Atob("test"), []byte("test")) { 18 | t.Error("should be equal") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/test/encoding.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/abemedia/go-don" 12 | "github.com/abemedia/go-don/encoding" 13 | _ "github.com/abemedia/go-don/encoding/text" // default encoding 14 | "github.com/abemedia/go-don/pkg/httptest" 15 | "github.com/google/go-cmp/cmp" 16 | "github.com/google/go-cmp/cmp/cmpopts" 17 | ) 18 | 19 | type EncodingOptions[T any] struct { 20 | Mime string 21 | Parsed T 22 | Raw string 23 | } 24 | 25 | func Encoding[T any](t *testing.T, opt EncodingOptions[T]) { 26 | t.Helper() 27 | t.Run("Decode", func(t *testing.T) { 28 | t.Helper() 29 | Decode(t, opt) 30 | }) 31 | t.Run("Encode", func(t *testing.T) { 32 | t.Helper() 33 | Encode(t, opt) 34 | }) 35 | } 36 | 37 | func Decode[T any](t *testing.T, opt EncodingOptions[T]) { 38 | t.Helper() 39 | 40 | var diff string 41 | 42 | api := don.New(nil) 43 | api.Post("/", don.H(func(ctx context.Context, req T) (any, error) { 44 | diff = cmp.Diff(opt.Parsed, req, ignoreUnexported[T]()) 45 | return nil, nil 46 | })) 47 | 48 | ctx := httptest.NewRequest(http.MethodPost, "/", opt.Raw, map[string]string{"Content-Type": opt.Mime}) 49 | api.RequestHandler()(ctx) 50 | 51 | if diff != "" { 52 | t.Error(diff) 53 | } 54 | 55 | if ctx.Response.StatusCode() != http.StatusNoContent { 56 | t.Errorf("expected success status: %v", &ctx.Response) 57 | } 58 | } 59 | 60 | func Encode[T any](t *testing.T, opt EncodingOptions[T]) { 61 | t.Helper() 62 | 63 | api := don.New(nil) 64 | api.Post("/", don.H(func(ctx context.Context, req any) (T, error) { 65 | return opt.Parsed, nil 66 | })) 67 | 68 | ctx := httptest.NewRequest(http.MethodPost, "/", "", map[string]string{"Accept": opt.Mime}) 69 | api.RequestHandler()(ctx) 70 | 71 | if diff := cmp.Diff(opt.Raw, string(ctx.Response.Body()), ignoreUnexported[T]()); diff != "" { 72 | t.Error(diff) 73 | } 74 | 75 | if ctx.Response.StatusCode() != http.StatusOK { 76 | t.Errorf("expected success status: %v", &ctx.Response) 77 | } 78 | } 79 | 80 | func ignoreUnexported[T any]() cmp.Option { 81 | t := reflect.TypeOf(*new(T)) 82 | for t.Kind() == reflect.Pointer { 83 | t = t.Elem() 84 | } 85 | if t.Kind() != reflect.Struct { 86 | return nil 87 | } 88 | return cmpopts.IgnoreUnexported(reflect.New(t).Elem().Interface()) 89 | } 90 | 91 | func BenchmarkEncoding[T any](b *testing.B, opt EncodingOptions[T]) { 92 | b.Run("Decode", func(b *testing.B) { 93 | b.Helper() 94 | BenchmarkDecode(b, opt) 95 | }) 96 | b.Run("Encode", func(b *testing.B) { 97 | b.Helper() 98 | BenchmarkEncode(b, opt) 99 | }) 100 | } 101 | 102 | func BenchmarkDecode[T any](b *testing.B, opt EncodingOptions[T]) { 103 | b.Helper() 104 | 105 | dec := encoding.GetDecoder(opt.Mime) 106 | if dec == nil { 107 | b.Fatal("decoder not found") 108 | } 109 | 110 | rd := strings.NewReader(opt.Raw) 111 | ctx := httptest.NewRequest("POST", "/", "", nil) 112 | ctx.Request.SetBodyStream(rd, len(opt.Raw)) 113 | 114 | v := new(T) 115 | if val := reflect.ValueOf(v).Elem(); val.Kind() == reflect.Pointer { 116 | val.Set(reflect.New(val.Type().Elem())) 117 | } 118 | 119 | for i := 0; i < b.N; i++ { 120 | rd.Seek(0, io.SeekStart) //nolint:errcheck 121 | dec(ctx, v) //nolint:errcheck 122 | } 123 | } 124 | 125 | func BenchmarkEncode[T any](b *testing.B, opt EncodingOptions[T]) { 126 | b.Helper() 127 | 128 | enc := encoding.GetEncoder(opt.Mime) 129 | if enc == nil { 130 | b.Fatal("encoder not found") 131 | } 132 | 133 | ctx := httptest.NewRequest("POST", "/", "", nil) 134 | 135 | for i := 0; i < b.N; i++ { 136 | ctx.Response.ResetBody() 137 | enc(ctx, opt.Parsed) //nolint:errcheck 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /internal/test/router.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/abemedia/go-don" 9 | "github.com/abemedia/go-don/pkg/httptest" 10 | "github.com/abemedia/httprouter" 11 | "github.com/valyala/fasthttp" 12 | ) 13 | 14 | func Router(t *testing.T, r don.Router, handler fasthttp.RequestHandler, basePath string) { 15 | t.Helper() 16 | 17 | tests := []struct { 18 | desc string 19 | method string 20 | fn func(path string, handle httprouter.Handle) 21 | }{ 22 | {"Get", fasthttp.MethodGet, r.Get}, 23 | {"Post", fasthttp.MethodPost, r.Post}, 24 | {"Put", fasthttp.MethodPut, r.Put}, 25 | {"Patch", fasthttp.MethodPatch, r.Patch}, 26 | {"Delete", fasthttp.MethodDelete, r.Delete}, 27 | {"Handle", fasthttp.MethodGet, func(path string, handle httprouter.Handle) { 28 | r.Handle(fasthttp.MethodGet, path, handle) 29 | }}, 30 | {"Handler", fasthttp.MethodGet, func(path string, handle httprouter.Handle) { 31 | r.Handler(fasthttp.MethodGet, path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | _, _ = w.Write([]byte("Handler")) 33 | })) 34 | }}, 35 | {"HandleFunc", fasthttp.MethodGet, func(path string, handle httprouter.Handle) { 36 | r.HandleFunc(fasthttp.MethodGet, path, func(w http.ResponseWriter, r *http.Request) { 37 | _, _ = w.Write([]byte("HandleFunc")) 38 | }) 39 | }}, 40 | } 41 | 42 | for _, test := range tests { 43 | path := "/" + strings.ToLower(test.desc) 44 | 45 | test.fn(path, func(ctx *fasthttp.RequestCtx, p httprouter.Params) { 46 | _, _ = ctx.WriteString(test.desc) 47 | }) 48 | 49 | ctx := httptest.NewRequest(test.method, basePath+path, "", nil) 50 | handler(ctx) 51 | 52 | if code := ctx.Response.StatusCode(); code != fasthttp.StatusOK { 53 | t.Errorf("%s request should return success status: %s", test.desc, fasthttp.StatusMessage(code)) 54 | } 55 | 56 | if string(ctx.Response.Body()) != test.desc { 57 | t.Errorf("%s request should reach handler", test.desc) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /nilcheck.go: -------------------------------------------------------------------------------- 1 | package don 2 | 3 | import "reflect" 4 | 5 | func newNilCheck(zero any) func(v any) bool { 6 | typ := reflect.TypeOf(zero) 7 | 8 | // Return true for nil interface. 9 | if typ == nil { 10 | return func(v any) bool { return v == nil } 11 | } 12 | 13 | switch typ.Kind() { 14 | case reflect.String, reflect.Ptr, reflect.Interface: 15 | // Return true for empty string and nil pointer. 16 | return func(v any) bool { return v == zero } 17 | case reflect.Map: 18 | // Return true for and nil map. 19 | return func(v any) bool { return dataOf(v) == nil } 20 | case reflect.Slice: 21 | // Return true for nil slice. 22 | return func(v any) bool { return (*reflect.SliceHeader)(dataOf(v)).Data == 0 } 23 | default: 24 | // Return false for all others. 25 | return func(any) bool { return false } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nilcheck_test.go: -------------------------------------------------------------------------------- 1 | package don_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/abemedia/go-don" 7 | ) 8 | 9 | func TestNilCheck(t *testing.T) { 10 | tests := []struct { 11 | typ any 12 | data any 13 | message string 14 | expected bool 15 | }{ 16 | { 17 | message: "nil interface", 18 | typ: nil, 19 | data: nil, 20 | expected: true, 21 | }, 22 | { 23 | message: "empty string", 24 | typ: "", 25 | data: "", 26 | expected: true, 27 | }, 28 | { 29 | message: "nil struct", 30 | typ: (*struct{})(nil), 31 | data: (*struct{})(nil), 32 | expected: true, 33 | }, 34 | { 35 | message: "zero struct", 36 | typ: struct{}{}, 37 | data: struct{}{}, 38 | expected: false, 39 | }, 40 | { 41 | message: "nil map", 42 | typ: (map[string]string)(nil), 43 | data: (map[string]string)(nil), 44 | expected: true, 45 | }, 46 | { 47 | message: "zero map", 48 | typ: (map[string]string)(nil), 49 | data: map[string]string{}, 50 | expected: false, 51 | }, 52 | { 53 | message: "non-zero map", 54 | typ: (map[string]string)(nil), 55 | data: map[string]string{"foo": "bar"}, 56 | expected: false, 57 | }, 58 | { 59 | message: "nil slice", 60 | typ: ([]string)(nil), 61 | data: ([]string)(nil), 62 | expected: true, 63 | }, 64 | { 65 | message: "zero slice", 66 | typ: ([]string)(nil), 67 | data: []string{}, 68 | expected: false, 69 | }, 70 | { 71 | message: "non-zero slice", 72 | typ: ([]string)(nil), 73 | data: []string{"aa"}, 74 | expected: false, 75 | }, 76 | { 77 | message: "boolean", 78 | typ: false, 79 | data: false, 80 | expected: false, 81 | }, 82 | { 83 | message: "integer", 84 | typ: 0, 85 | data: 0, 86 | expected: false, 87 | }, 88 | { 89 | message: "non-zero slice pointer", 90 | typ: (*[]string)(nil), 91 | data: func() any { 92 | m := []string{"aa"} 93 | return &m 94 | }(), 95 | expected: false, 96 | }, 97 | } 98 | 99 | for _, test := range tests { 100 | isNil := don.NewNilCheck(test.typ) 101 | if isNil(test.data) != test.expected { 102 | t.Errorf("%s should be %t", test.message, test.expected) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /pkg/httptest/request.go: -------------------------------------------------------------------------------- 1 | // Package httptest provides utilities for testing fasthttp handlers. 2 | package httptest 3 | 4 | import ( 5 | "strings" 6 | 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | // NewRequest returns a new [fasthttp.RequestCtx] with the given method, url, body and header. 11 | func NewRequest(method, url, body string, header map[string]string) *fasthttp.RequestCtx { 12 | ctx := &fasthttp.RequestCtx{} 13 | ctx.Request.Header.SetMethod(method) 14 | ctx.Request.SetRequestURI(url) 15 | 16 | for k, v := range header { 17 | ctx.Request.Header.Set(k, v) 18 | } 19 | 20 | ctx.Request.SetBodyStream(strings.NewReader(body), len(body)) 21 | 22 | return ctx 23 | } 24 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package don 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | ) 7 | 8 | type pool[T any] interface { 9 | Get() *T 10 | Put(x *T) 11 | } 12 | 13 | type requestPool[T any] struct { 14 | pool sync.Pool 15 | reset func(*T) 16 | } 17 | 18 | func newRequestPool[T any](zero T) pool[T] { 19 | typ := reflect.TypeOf(zero) 20 | if typ == nil { 21 | return &fakePool[T]{&zero} 22 | } 23 | 24 | p := &requestPool[T]{} 25 | 26 | if typ.Kind() != reflect.Pointer { 27 | p.pool.New = func() any { 28 | return new(T) 29 | } 30 | p.reset = func(v *T) { 31 | *v = zero 32 | } 33 | } else { 34 | rtype := dataOf(typ) 35 | elem := typ.Elem() 36 | elemrtype := dataOf(elem) 37 | zero := dataOf(reflect.New(elem).Elem().Interface()) 38 | 39 | p.pool.New = func() any { 40 | v := packEface(rtype, unsafe_New(elemrtype)).(T) //nolint:forcetypeassert 41 | return &v 42 | } 43 | p.reset = func(v *T) { 44 | typedmemmove(elemrtype, dataOf(*v), zero) 45 | } 46 | } 47 | 48 | return p 49 | } 50 | 51 | func (p *requestPool[T]) Get() *T { 52 | return p.pool.Get().(*T) //nolint:forcetypeassert 53 | } 54 | 55 | func (p *requestPool[T]) Put(v *T) { 56 | p.reset(v) 57 | p.pool.Put(v) 58 | } 59 | 60 | type fakePool[T any] struct{ v *T } 61 | 62 | func (p *fakePool[T]) Get() *T { return p.v } 63 | 64 | func (p *fakePool[T]) Put(*T) {} 65 | -------------------------------------------------------------------------------- /pool_test.go: -------------------------------------------------------------------------------- 1 | package don_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/abemedia/go-don" 8 | ) 9 | 10 | func TestRequestPool(t *testing.T) { 11 | type item struct { 12 | String string 13 | Pointer *string 14 | } 15 | 16 | t.Run("Nil", func(t *testing.T) { 17 | var zero any 18 | pool := don.NewRequestPool(zero) 19 | 20 | pool.Put(pool.Get()) 21 | 22 | if !reflect.DeepEqual(&zero, pool.Get()) { 23 | t.Fatal("should be zero value") 24 | } 25 | }) 26 | 27 | t.Run("Struct", func(t *testing.T) { 28 | zero := item{} 29 | pool := don.NewRequestPool(zero) 30 | 31 | for i := 0; i < 100; i++ { 32 | v := pool.Get() 33 | v.String = "test" 34 | v.Pointer = &v.String 35 | pool.Put(v) 36 | } 37 | 38 | for i := 0; i < 100; i++ { 39 | if !reflect.DeepEqual(&zero, pool.Get()) { 40 | t.Fatal("should be zero value") 41 | } 42 | } 43 | }) 44 | 45 | t.Run("Pointer", func(t *testing.T) { 46 | zero := &item{} 47 | pool := don.NewRequestPool(zero) 48 | 49 | for i := 0; i < 100; i++ { 50 | p := pool.Get() 51 | v := *p 52 | v.String = "test" 53 | v.Pointer = &v.String 54 | pool.Put(p) 55 | } 56 | 57 | for i := 0; i < 100; i++ { 58 | if !reflect.DeepEqual(&zero, pool.Get()) { 59 | t.Fatal("should be zero value") 60 | } 61 | } 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package don 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/abemedia/go-don/decoder" 7 | "github.com/abemedia/go-don/encoding" 8 | "github.com/abemedia/httprouter" 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | type requestDecoder[V any] func(v *V, ctx *fasthttp.RequestCtx, p httprouter.Params) error 13 | 14 | func newRequestDecoder[V any](v V) requestDecoder[V] { 15 | path, _ := decoder.NewCached(v, "path") 16 | query, _ := decoder.NewCached(v, "query") 17 | header, _ := decoder.NewCached(v, "header") 18 | 19 | if path == nil && query == nil && header == nil { 20 | return decodeBody[V]() 21 | } 22 | 23 | return decodeRequest(path, query, header) 24 | } 25 | 26 | func decodeRequest[V any](path, query, header *decoder.CachedDecoder[V]) requestDecoder[V] { 27 | body := decodeBody[V]() 28 | return func(v *V, ctx *fasthttp.RequestCtx, p httprouter.Params) error { 29 | if err := body(v, ctx, nil); err != nil { 30 | return err 31 | } 32 | 33 | val := reflect.ValueOf(v).Elem() 34 | 35 | if path != nil && len(p) > 0 { 36 | if err := path.DecodeValue((decoder.Params)(p), val); err != nil { 37 | return ErrNotFound 38 | } 39 | } 40 | 41 | if query != nil { 42 | if q := ctx.Request.URI().QueryArgs(); q.Len() > 0 { 43 | if err := query.DecodeValue((*decoder.Args)(q), val); err != nil { 44 | return err 45 | } 46 | } 47 | } 48 | 49 | if header != nil { 50 | if err := header.DecodeValue((*decoder.Header)(&ctx.Request.Header), val); err != nil { 51 | return err 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | } 58 | 59 | func decodeBody[V any]() requestDecoder[V] { 60 | return func(v *V, ctx *fasthttp.RequestCtx, _ httprouter.Params) error { 61 | if ctx.Request.Header.ContentLength() == 0 || ctx.IsGet() || ctx.IsHead() { 62 | return nil 63 | } 64 | 65 | dec := encoding.GetDecoder(getMediaType(ctx.Request.Header.ContentType())) 66 | if dec == nil { 67 | return ErrUnsupportedMediaType 68 | } 69 | 70 | return dec(ctx, v) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scripts/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git stash --keep-index --include-untracked --quiet 4 | 5 | exitCode=0 6 | 7 | go mod tidy || exitCode=$? 8 | golangci-lint run || exitCode=$? 9 | go test ./... || exitCode=$? 10 | 11 | if [ $exitCode -eq 0 ]; then 12 | git add . 13 | else 14 | git stash --keep-index --include-untracked --quiet && git stash drop --quiet 15 | fi 16 | 17 | git stash pop --quiet 18 | 19 | exit $exitCode 20 | -------------------------------------------------------------------------------- /unsafe.go: -------------------------------------------------------------------------------- 1 | package don 2 | 3 | import "unsafe" 4 | 5 | //go:linkname unsafe_New reflect.unsafe_New 6 | func unsafe_New(unsafe.Pointer) unsafe.Pointer //nolint:revive 7 | 8 | //go:linkname typedmemmove reflect.typedmemmove 9 | func typedmemmove(t, dst, src unsafe.Pointer) 10 | 11 | // emptyInterface is the header for an interface{} value. 12 | type emptyInterface struct { 13 | typ unsafe.Pointer 14 | ptr unsafe.Pointer 15 | } 16 | 17 | func dataOf(v any) unsafe.Pointer { 18 | return (*emptyInterface)(unsafe.Pointer(&v)).ptr 19 | } 20 | 21 | func packEface(typ, ptr unsafe.Pointer) any { 22 | var i any 23 | e := (*emptyInterface)(unsafe.Pointer(&i)) 24 | e.typ = typ 25 | e.ptr = ptr 26 | return i 27 | } 28 | --------------------------------------------------------------------------------