├── .cligen.yml ├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── dummy │ └── main.go ├── examples ├── README.md └── docker │ ├── Dockerfile │ ├── README.md │ └── openapi.yml ├── go.mod ├── go.sum ├── internal ├── api │ ├── api.go │ ├── api_test.go │ ├── build.go │ ├── build_test.go │ ├── find.go │ └── find_test.go ├── config │ ├── config.go │ ├── config_test.go │ ├── logger.go │ └── server.go ├── exitcode │ └── exitcode.go ├── logger │ ├── logger.go │ └── logger_test.go ├── middleware │ ├── logging.go │ └── logging_test.go ├── parse │ ├── parse.go │ ├── parse_test.go │ └── testdata │ │ ├── api.raml │ │ ├── empty-openapi.yml │ │ ├── openapi │ │ ├── openapi.yml │ │ ├── openapi3.yml │ │ └── schema.graphql ├── pkg │ ├── logger │ │ └── logger.go │ └── pathfmt │ │ ├── pathfmt.go │ │ └── pathfmt_test.go ├── read │ ├── read.go │ ├── read_test.go │ └── testdata │ │ └── openapi3.yml └── server │ ├── handler.go │ ├── handler_test.go │ ├── server.go │ └── server_test.go └── test ├── dummy_test.go └── testdata ├── cases.yml └── openapi.yml /.cligen.yml: -------------------------------------------------------------------------------- 1 | name: "Dummy" 2 | commands: 3 | - name: "server" 4 | alias: "s" 5 | description: "run mock server" 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | 8 | [*.go] 9 | indent_style = tab 10 | indent_size = 4 11 | 12 | [Makefile] 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### :heavy_check_mark: Checklist: 2 | - [ ] Have only one commit (if not, squash them into one commit) 3 | - [ ] Tests for the changes have been added (for features / bug fixes / refactoring) 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: / 6 | schedule: 7 | interval: daily 8 | reviewers: 9 | - "neotoolkit/team" 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | reviewers: 15 | - "neotoolkit/team" 16 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: 3 | release: 4 | types: [published] 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: ${{ github.event.repository.name }} 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | packages: write 15 | contents: read 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v2 23 | 24 | - name: Log in to registry 25 | uses: docker/login-action@v2 26 | with: 27 | registry: ${{ env.REGISTRY }} 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Set version 32 | id: set-version 33 | run: | 34 | VERSION=${GITHUB_REF/refs\/tags\//} 35 | 36 | echo ::set-output name=VERSION::${VERSION} 37 | 38 | - name: Build and push 39 | id: docker_build 40 | uses: docker/build-push-action@v3 41 | with: 42 | push: true 43 | tags: ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ steps.set-version.outputs.VERSION }} 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | uses: neotoolkit/.github/.github/workflows/build.yml@main 9 | dependency: 10 | uses: neotoolkit/.github/.github/workflows/dependency.yml@main 11 | lint: 12 | uses: neotoolkit/.github/.github/workflows/lint.yml@main 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories 15 | vendor/ 16 | 17 | # IDEA settings 18 | .idea 19 | 20 | # Pact logs 21 | test/log/pact.log 22 | 23 | # macOS 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - bodyclose 5 | - deadcode 6 | - dupl 7 | - errcheck 8 | - gochecknoglobals 9 | - gocritic 10 | - gocyclo 11 | - gosimple 12 | - govet 13 | - ineffassign 14 | - makezero 15 | - misspell 16 | - nilerr 17 | - prealloc 18 | - revive 19 | - staticcheck 20 | - structcheck 21 | - stylecheck 22 | - testpackage 23 | - typecheck 24 | - unparam 25 | - unused 26 | - varcheck 27 | - wsl 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | RUN go install github.com/neotoolkit/dummy/cmd/dummy@latest 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 neotoolkit 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all tidy fmt imports lint test 2 | 3 | .PHONY: all 4 | all: tidy fmt imports lint test 5 | 6 | .PHONY: tidy 7 | tidy: 8 | go mod tidy 9 | 10 | .PHONY: fmt 11 | fmt: 12 | go fmt ./... 13 | 14 | .PHONY: imports 15 | imports: 16 | goimports -local github.com/neotoolkit/dummy/ -w -l ./ 17 | 18 | .PHONY: lint 19 | lint: 20 | golangci-lint run 21 | 22 | .PHONY: test 23 | test: 24 | go test ./... 25 | cover: 26 | go test -coverprofile=coverage.out && go tool cover -html=coverage.out 27 | 28 | .PHONY: local 29 | local: 30 | go run ./cmd/dummy -port=8080 server examples/docker/openapi3.yml 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dummy 2 | 3 | [![CI-img]][CI-url] 4 | [![pkg-img]][pkg-url] 5 | [![reportcard-img]][reportcard-url] 6 | [![coverage-img]][coverage-url] 7 | [![version-img]][version-url] 8 | 9 | dummy 10 | 11 | Run mock server based off an API contract with one command 12 | 13 | [Dummy](https://evrone.com/dummy?utm_source=github&utm_campaign=dummy) is created & supported by [Evrone](https://evrone.com?utm_source=github&utm_campaign=dummy) 14 | 15 | ## Features 16 | - Supports `OpenAPI 3.x` 17 | 18 | ## Installation 19 | ```shell 20 | go install github.com/neotoolkit/dummy/cmd/dummy@latest 21 | ``` 22 | 23 | ## Usage 24 | Dummy can help you run mock server based off an API contract, which helps people see how your API will work before you even have it built. Run it locally with the `dummy s` command to run your API on a HTTP server you can interact with. 25 | ```shell 26 | dummy s openapi.yml 27 | ``` 28 | ```shell 29 | dummy s https://raw.githubusercontent.com/neotoolkit/dummy/main/examples/docker/openapi.yml 30 | ``` 31 | More usage [examples](examples) 32 | 33 | ## Documentation 34 | See [these docs][pkg-url]. 35 | 36 | ## License 37 | [MIT License](LICENSE). 38 | 39 | ## Sponsors 40 |

41 | 42 | Sponsored by Evrone 44 | 45 |

46 | 47 | [CI-img]: https://github.com/neotoolkit/dummy/workflows/CI/badge.svg 48 | [CI-url]: https://github.com/neotoolkit/dummy/actions 49 | [pkg-img]: https://pkg.go.dev/badge/neotoolkit/dummy 50 | [pkg-url]: https://pkg.go.dev/github.com/neotoolkit/dummy 51 | [reportcard-img]: https://goreportcard.com/badge/neotoolkit/dummy 52 | [reportcard-url]: https://goreportcard.com/report/neotoolkit/dummy 53 | [coverage-img]: https://codecov.io/gh/neotoolkit/dummy/branch/main/graph/badge.svg 54 | [coverage-url]: https://codecov.io/gh/neotoolkit/dummy 55 | [version-img]: https://img.shields.io/github/v/release/neotoolkit/dummy 56 | [version-url]: https://github.com/neotoolkit/dummy/releases 57 | -------------------------------------------------------------------------------- /cmd/dummy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/cristalhq/acmd" 15 | 16 | "github.com/neotoolkit/dummy/internal/config" 17 | "github.com/neotoolkit/dummy/internal/logger" 18 | "github.com/neotoolkit/dummy/internal/parse" 19 | "github.com/neotoolkit/dummy/internal/server" 20 | ) 21 | 22 | const version = "0.2.1" 23 | 24 | func main() { 25 | err := run() 26 | if err != nil { 27 | fmt.Fprintf(os.Stderr, "server run error: %v\n", err) 28 | } 29 | } 30 | 31 | func run() error { 32 | cmds := []acmd.Command{ 33 | { 34 | Name: "server", 35 | Alias: "s", 36 | Description: "run mock server", 37 | Do: func(ctx context.Context, args []string) error { 38 | cfg := config.NewConfig() 39 | 40 | cfg.Server.Path = args[0] 41 | 42 | fs := flag.NewFlagSet("dummy", flag.ContinueOnError) 43 | fs.StringVar(&cfg.Server.Port, "port", "8080", "") 44 | fs.StringVar(&cfg.Logger.Level, "logger-level", "info", "") 45 | if err := fs.Parse(args[1:]); err != nil { 46 | return err 47 | } 48 | 49 | api, err := parse.Parse(cfg.Server.Path) 50 | if err != nil { 51 | return fmt.Errorf("specification parse error: %w", err) 52 | } 53 | 54 | l := logger.NewLogger(cfg.Logger.Level) 55 | h := server.NewHandlers(api, l) 56 | s := server.NewServer(cfg.Server, l, h) 57 | 58 | go func() { 59 | if err := s.Run(); !errors.Is(err, http.ErrServerClosed) { 60 | l.Errorf("run server error: %v", err) 61 | } 62 | }() 63 | 64 | interrupt := make(chan os.Signal, 1) 65 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 66 | 67 | x := <-interrupt 68 | l.Infof("received `%v`", x) 69 | 70 | const timeout = 5 * time.Second 71 | 72 | ctx, cancelFunc := context.WithTimeout(ctx, timeout) 73 | defer cancelFunc() 74 | 75 | return s.Stop(ctx) 76 | }, 77 | }, 78 | } 79 | 80 | r := acmd.RunnerOf(cmds, acmd.Config{ 81 | AppName: "Dummy", 82 | Version: version, 83 | }) 84 | 85 | return r.Run() 86 | } 87 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | ```shell 3 | dummy s openapi.yml 4 | ``` 5 | ```shell 6 | dummy server openapi.yml 7 | ``` 8 | ```shell 9 | dummy s openapi.json 10 | ``` 11 | ```shell 12 | dummy s https://raw.githubusercontent.com/neotoolkit/dummy/main/examples/docker/openapi.yml 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | RUN go install github.com/neotoolkit/dummy/cmd/dummy@latest 4 | 5 | COPY ./openapi3.yml . 6 | 7 | WORKDIR . 8 | 9 | CMD ["dummy", "s", "openapi.yml", "-port=8080"] 10 | -------------------------------------------------------------------------------- /examples/docker/README.md: -------------------------------------------------------------------------------- 1 | :one: 2 | ```bash 3 | docker build -t dummy . 4 | ``` 5 | :two: 6 | ```bash 7 | docker run -p 8080:8080 dummy 8 | ``` 9 | :white_check_mark: -------------------------------------------------------------------------------- /examples/docker/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | 3 | info: 4 | title: Users dummy API 5 | version: 0.1.0 6 | 7 | paths: 8 | /users: 9 | post: 10 | requestBody: 11 | required: true 12 | content: 13 | application/json: 14 | schema: 15 | $ref: "#/components/schemas/User" 16 | 17 | responses: 18 | '201': 19 | description: '' 20 | content: 21 | application/json: 22 | schema: 23 | $ref: '#/components/schemas/User' 24 | 25 | get: 26 | responses: 27 | '200': 28 | description: '' 29 | content: 30 | application/json: 31 | schema: 32 | type: array 33 | items: 34 | $ref: '#/components/schemas/User' 35 | example: 36 | - id: e1afccea-5168-4735-84d4-cb96f6fb5d25 37 | firstName: Elon 38 | lastName: Musk 39 | - id: 472063cc-4c83-11ec-81d3-0242ac130003 40 | firstName: Sergey 41 | lastName: Brin 42 | 43 | /users/{userId}: 44 | get: 45 | parameters: 46 | - in: path 47 | name: userId 48 | description: '' 49 | required: true 50 | schema: 51 | type: string 52 | 53 | responses: 54 | '200': 55 | description: '' 56 | content: 57 | application/json: 58 | schema: 59 | $ref: '#/components/schemas/User' 60 | 61 | components: 62 | schemas: 63 | User: 64 | type: object 65 | required: 66 | - id 67 | - firstName 68 | - lastName 69 | properties: 70 | id: 71 | type: string 72 | format: uuid 73 | firstName: 74 | type: string 75 | lastName: 76 | type: string 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/neotoolkit/dummy 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cristalhq/acmd v0.8.0 7 | github.com/goccy/go-yaml v1.9.5 8 | github.com/lamoda/gonkey v1.18.2 9 | github.com/neotoolkit/faker v0.9.1 10 | github.com/neotoolkit/openapi v0.10.0 11 | github.com/stretchr/testify v1.8.0 12 | go.uber.org/zap v1.21.0 13 | ) 14 | 15 | require ( 16 | github.com/Masterminds/goutils v1.1.1 // indirect 17 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 18 | github.com/Masterminds/sprig/v3 v3.2.2 // indirect 19 | github.com/aerospike/aerospike-client-go/v5 v5.8.0 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/fatih/color v1.13.0 // indirect 22 | github.com/google/uuid v1.3.0 // indirect 23 | github.com/huandu/xstrings v1.3.2 // indirect 24 | github.com/imdario/mergo v0.3.13 // indirect 25 | github.com/joho/godotenv v1.4.0 // indirect 26 | github.com/kylelemons/godebug v1.1.0 // indirect 27 | github.com/lib/pq v1.10.4 // indirect 28 | github.com/mattn/go-colorable v0.1.12 // indirect 29 | github.com/mattn/go-isatty v0.0.14 // indirect 30 | github.com/mitchellh/copystructure v1.2.0 // indirect 31 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 32 | github.com/pkg/errors v0.9.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/shopspring/decimal v1.2.0 // indirect 35 | github.com/spf13/cast v1.3.1 // indirect 36 | github.com/tidwall/gjson v1.13.0 // indirect 37 | github.com/tidwall/match v1.1.1 // indirect 38 | github.com/tidwall/pretty v1.2.0 // indirect 39 | github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect 40 | go.uber.org/atomic v1.7.0 // indirect 41 | go.uber.org/multierr v1.6.0 // indirect 42 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect 43 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 44 | golang.org/x/sys v0.0.0-20220207234003-57398862261d // indirect 45 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 46 | gopkg.in/yaml.v2 v2.4.0 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 2 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 3 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 4 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 5 | github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= 6 | github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= 7 | github.com/aerospike/aerospike-client-go/v5 v5.8.0 h1:EUV2wG80yIenQqOyUlf5NfyhagPIwoeL09MJIE+xILE= 8 | github.com/aerospike/aerospike-client-go/v5 v5.8.0/go.mod h1:rJ/KpmClE7kiBPfvAPrGw9WuNOiz8v2uKbQaUyYPXtI= 9 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 10 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 11 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 12 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 13 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 14 | github.com/cristalhq/acmd v0.8.0 h1:0NFc0ixjHMJ1kJyoBZoklcSgw4PGBpD6XRuMv733xEg= 15 | github.com/cristalhq/acmd v0.8.0/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 20 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 21 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 22 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 23 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 24 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 25 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 26 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 27 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 28 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 29 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 30 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 31 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 32 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 33 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 34 | github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0= 35 | github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 38 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 39 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 40 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 41 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 42 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 43 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 44 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 45 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 46 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 47 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 49 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 50 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 52 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 53 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 54 | github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 55 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 56 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 57 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 58 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 59 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 60 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 61 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 62 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 63 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 64 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 65 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 68 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 69 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 70 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 71 | github.com/lamoda/gonkey v1.18.2 h1:DXMa/WTx9LjJPP2C3xD6n6inZP1X0fhzbdNqnw0m5v4= 72 | github.com/lamoda/gonkey v1.18.2/go.mod h1:w0jZz5DPVWmYcnFy8Yq5RZatYmhmHNUxsZcJezurw9E= 73 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 74 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 75 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 76 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 77 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 78 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 79 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 80 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 81 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 82 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 83 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 84 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 85 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 86 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 87 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 88 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 89 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 90 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 91 | github.com/neotoolkit/faker v0.9.1 h1:Z5bvSQoqm9usI2Q6X5/7aeUhhI81geTcy2VP5Qdaay8= 92 | github.com/neotoolkit/faker v0.9.1/go.mod h1:xo8gPYed0+xG7RQHV3wwKMyE4i0wkMMjNoyxjihgWno= 93 | github.com/neotoolkit/openapi v0.10.0 h1:kg7hrBQc3CxkwVkrWp0MXzqxWMcCQGuQZDWKBJLeZY4= 94 | github.com/neotoolkit/openapi v0.10.0/go.mod h1:/Yh4psCpGJAbsMCmp2W8dkL+JQjBYChxtAeeCDNoCsc= 95 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 96 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 97 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 98 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 99 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 100 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 101 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 102 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 103 | github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 104 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 105 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 106 | github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= 107 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 108 | github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= 109 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 110 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 111 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 112 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 113 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 114 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 115 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 116 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 117 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 118 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 119 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 120 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 121 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 122 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 123 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 124 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 125 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 126 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 127 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 128 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 129 | github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M= 130 | github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 131 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 132 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 133 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 134 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 135 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 136 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 137 | github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= 138 | github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= 139 | github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 140 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 141 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 142 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 143 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 144 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 145 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 146 | go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= 147 | go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= 148 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 149 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 150 | golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 151 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 152 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= 153 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 154 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 155 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 156 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 157 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 158 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 159 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 160 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 161 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 162 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 163 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 164 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 165 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 166 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 167 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= 168 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 169 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 173 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 174 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 176 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 177 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 192 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 193 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 194 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 195 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 196 | golang.org/x/sys v0.0.0-20220207234003-57398862261d h1:Bm7BNOQt2Qv7ZqysjeLjgCBanX+88Z/OtdvsrEv1Djc= 197 | golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 198 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 199 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 200 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 201 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 202 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 203 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 204 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 205 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 206 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 207 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 208 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 209 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 210 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 211 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 212 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 213 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 214 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 215 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 216 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 217 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 218 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 219 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 220 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 221 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 222 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 223 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 224 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= 225 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= 226 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 227 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 228 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 229 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 230 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 231 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 232 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 233 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 234 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 235 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 236 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 237 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 238 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 239 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 240 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 241 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 242 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 243 | -------------------------------------------------------------------------------- /internal/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // API -. 4 | type API struct { 5 | Operations []Operation 6 | } 7 | 8 | // Operation -. 9 | type Operation struct { 10 | Method string 11 | Path string 12 | Body map[string]FieldType 13 | Responses []Response 14 | } 15 | 16 | // FieldType -. 17 | type FieldType struct { 18 | Required bool 19 | Type string 20 | } 21 | 22 | // Response -. 23 | type Response struct { 24 | StatusCode int 25 | MediaType string 26 | Schema Schema 27 | Example any 28 | Examples map[string]any 29 | } 30 | 31 | // ExampleValue -. 32 | func (r Response) ExampleValue(key string) any { 33 | if nil == r.Schema { 34 | return nil 35 | } 36 | 37 | example, ok := r.Examples[key] 38 | if ok { 39 | return example 40 | } 41 | 42 | if r.Example != nil { 43 | return r.Example 44 | } 45 | 46 | return r.Schema.ExampleValue() 47 | } 48 | 49 | // Schema -. 50 | type Schema interface { 51 | ExampleValue() any 52 | } 53 | 54 | // BooleanSchema -. 55 | type BooleanSchema struct { 56 | Example bool 57 | } 58 | 59 | // ExampleValue -. 60 | func (b BooleanSchema) ExampleValue() any { 61 | return b.Example 62 | } 63 | 64 | // IntSchema -. 65 | type IntSchema struct { 66 | Example int64 67 | } 68 | 69 | // ExampleValue -. 70 | func (i IntSchema) ExampleValue() any { 71 | return i.Example 72 | } 73 | 74 | // FloatSchema -. 75 | type FloatSchema struct { 76 | Example float64 77 | } 78 | 79 | // ExampleValue -. 80 | func (f FloatSchema) ExampleValue() any { 81 | return f.Example 82 | } 83 | 84 | // StringSchema -. 85 | type StringSchema struct { 86 | Example string 87 | } 88 | 89 | // ExampleValue -. 90 | func (s StringSchema) ExampleValue() any { 91 | return s.Example 92 | } 93 | 94 | // ArraySchema -. 95 | type ArraySchema struct { 96 | Type Schema 97 | Example []interface{} 98 | } 99 | 100 | // ExampleValue -. 101 | func (a ArraySchema) ExampleValue() any { 102 | if len(a.Example) > 0 { 103 | return a.Example 104 | } 105 | 106 | return []any{a.Type.ExampleValue()} 107 | } 108 | 109 | // ObjectSchema -. 110 | type ObjectSchema struct { 111 | Properties map[string]Schema 112 | Example map[string]any 113 | } 114 | 115 | // ExampleValue -. 116 | func (o ObjectSchema) ExampleValue() any { 117 | if len(o.Example) > 0 { 118 | return o.Example 119 | } 120 | 121 | example := make(map[string]any, len(o.Properties)) 122 | 123 | for key, propSchema := range o.Properties { 124 | example[key] = propSchema.ExampleValue() 125 | } 126 | 127 | return example 128 | } 129 | 130 | // FakerSchema -. 131 | type FakerSchema struct { 132 | Example any 133 | } 134 | 135 | // ExampleValue -. 136 | func (f FakerSchema) ExampleValue() any { 137 | return f.Example 138 | } 139 | -------------------------------------------------------------------------------- /internal/api/api_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/neotoolkit/dummy/internal/api" 9 | ) 10 | 11 | func TestSchema_ExampleValue(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | schema api.Schema 15 | want any 16 | }{ 17 | { 18 | name: "boolean: default", 19 | schema: api.BooleanSchema{}, 20 | want: false, 21 | }, 22 | { 23 | name: "boolean: with example", 24 | schema: api.BooleanSchema{Example: true}, 25 | want: true, 26 | }, 27 | { 28 | name: "int: default", 29 | schema: api.IntSchema{}, 30 | want: int64(0), 31 | }, 32 | { 33 | name: "int: with example", 34 | schema: api.IntSchema{Example: 42}, 35 | want: int64(42), 36 | }, 37 | { 38 | name: "float: default", 39 | schema: api.FloatSchema{}, 40 | want: 0.0, 41 | }, 42 | { 43 | name: "float: with example", 44 | schema: api.FloatSchema{Example: 4.2}, 45 | want: 4.2, 46 | }, 47 | { 48 | name: "string: default", 49 | schema: api.StringSchema{}, 50 | want: "", 51 | }, 52 | { 53 | name: "string: with example", 54 | schema: api.StringSchema{Example: "John"}, 55 | want: "John", 56 | }, 57 | { 58 | name: "array: default", 59 | schema: api.ArraySchema{Type: api.StringSchema{}}, 60 | want: []any{""}, 61 | }, 62 | { 63 | name: "array: with int example", 64 | schema: api.ArraySchema{Example: []any{4, 2}}, 65 | want: []any{4, 2}, 66 | }, 67 | { 68 | name: "array: with string example", 69 | schema: api.ArraySchema{Example: []any{"4", "2"}}, 70 | want: []any{"4", "2"}, 71 | }, 72 | { 73 | name: "object: default", 74 | schema: api.ObjectSchema{}, 75 | want: map[string]any{}, 76 | }, 77 | { 78 | name: "object: with example", 79 | schema: api.ObjectSchema{Example: map[string]any{"a": "4", "b": "2"}}, 80 | want: map[string]any{"a": "4", "b": "2"}, 81 | }, 82 | } 83 | 84 | for _, tc := range tests { 85 | t.Run(tc.name, func(t *testing.T) { 86 | got := tc.schema.ExampleValue() 87 | 88 | require.Equal(t, tc.want, got) 89 | }) 90 | } 91 | } 92 | 93 | func TestResponse_ExampleValue(t *testing.T) { 94 | tests := []struct { 95 | name string 96 | response api.Response 97 | key string 98 | want any 99 | }{ 100 | { 101 | name: "boolean examples", 102 | response: api.Response{ 103 | Schema: api.BooleanSchema{}, 104 | Examples: map[string]any{"first": false, "second": true}, 105 | }, 106 | key: "first", 107 | want: false, 108 | }, 109 | { 110 | name: "int examples", 111 | response: api.Response{ 112 | Schema: api.IntSchema{}, 113 | Examples: map[string]any{"first": int64(4), "second": int64(2)}, 114 | }, 115 | key: "first", 116 | want: int64(4), 117 | }, 118 | { 119 | name: "float examples", 120 | response: api.Response{ 121 | Schema: api.FloatSchema{}, 122 | Examples: map[string]any{"first": 4.0, "second": 2.0}, 123 | }, 124 | key: "first", 125 | want: 4.0, 126 | }, 127 | { 128 | name: "string examples", 129 | response: api.Response{ 130 | Schema: api.StringSchema{}, 131 | Examples: map[string]any{"first": "abc", "second": "xyz"}, 132 | }, 133 | key: "first", 134 | want: "abc", 135 | }, 136 | { 137 | name: "array examples", 138 | response: api.Response{ 139 | Schema: api.ArraySchema{}, 140 | Examples: map[string]any{"first": []int64{4, 2}, "second": []int64{0, 0}}, 141 | }, 142 | key: "first", 143 | want: []int64{4, 2}, 144 | }, 145 | { 146 | name: "object examples", 147 | response: api.Response{ 148 | Schema: api.ObjectSchema{}, 149 | Examples: map[string]any{"first": map[string]any{"first": "abc"}, "second": map[string]any{"first": "xyz"}}, 150 | }, 151 | key: "first", 152 | want: map[string]interface{}{"first": "abc"}, 153 | }, 154 | { 155 | name: "use schema example", 156 | response: api.Response{ 157 | Examples: map[string]any{"first": map[string]any{"first": "abc"}, "second": map[string]any{"first": "xyz"}}, 158 | Schema: api.ObjectSchema{ 159 | Example: map[string]any{"first": "schema variant"}, 160 | }, 161 | }, 162 | key: "third", 163 | want: map[string]any{"first": "schema variant"}, 164 | }, 165 | { 166 | name: "nil schema", 167 | response: api.Response{ 168 | Schema: nil, 169 | Examples: map[string]any{"first": false, "second": true}, 170 | }, 171 | key: "first", 172 | want: nil, 173 | }, 174 | { 175 | name: "example", 176 | response: api.Response{ 177 | Schema: api.ObjectSchema{}, 178 | Example: map[string]any{"first": "abc", "second": "def"}, 179 | }, 180 | key: "", 181 | want: map[string]any{"first": "abc", "second": "def"}, 182 | }, 183 | { 184 | name: "object properties", 185 | response: api.Response{ 186 | Schema: api.ObjectSchema{ 187 | Properties: map[string]api.Schema{ 188 | "key": api.ObjectSchema{}, 189 | }, 190 | }, 191 | }, 192 | key: "", 193 | want: map[string]any{"key": map[string]any{}}, 194 | }, 195 | { 196 | name: "faker schema", 197 | response: api.Response{ 198 | Schema: api.FakerSchema{}, 199 | }, 200 | key: "", 201 | want: nil, 202 | }, 203 | } 204 | 205 | for _, tc := range tests { 206 | t.Run(tc.name, func(t *testing.T) { 207 | got := tc.response.ExampleValue(tc.key) 208 | 209 | require.Equal(t, tc.want, got) 210 | }) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /internal/api/build.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/neotoolkit/faker" 10 | "github.com/neotoolkit/openapi" 11 | 12 | "github.com/neotoolkit/dummy/internal/pkg/pathfmt" 13 | ) 14 | 15 | // SchemaTypeError -. 16 | type SchemaTypeError struct { 17 | SchemaType string 18 | } 19 | 20 | func (e *SchemaTypeError) Error() string { 21 | return "unknown type " + e.SchemaType 22 | } 23 | 24 | // ErrEmptyItems -. 25 | var ErrEmptyItems = errors.New("empty items in array") 26 | 27 | // ArrayExampleError -. 28 | type ArrayExampleError struct { 29 | Data any 30 | } 31 | 32 | func (e *ArrayExampleError) Error() string { 33 | return fmt.Sprintf("unpredicted type for example %T", e.Data) 34 | } 35 | 36 | func ParseArrayExample(data any) ([]any, error) { 37 | if nil == data { 38 | return []any{}, nil 39 | } 40 | 41 | d, ok := data.([]any) 42 | if ok { 43 | res := make([]any, len(d)) 44 | for k, v := range d { 45 | res[k] = v.(map[string]any) 46 | } 47 | 48 | return res, nil 49 | } 50 | 51 | return nil, &ArrayExampleError{Data: data} 52 | } 53 | 54 | // ObjectExampleError -. 55 | type ObjectExampleError struct { 56 | Data any 57 | } 58 | 59 | // Error -. 60 | func (e *ObjectExampleError) Error() string { 61 | return fmt.Sprintf("unpredicted type for example %T", e.Data) 62 | } 63 | 64 | func ParseObjectExample(data any) (map[string]any, error) { 65 | if nil == data { 66 | return map[string]any{}, nil 67 | } 68 | 69 | d, ok := data.(map[string]any) 70 | if ok { 71 | return d, nil 72 | } 73 | 74 | return nil, &ObjectExampleError{Data: data} 75 | } 76 | 77 | type Builder struct { 78 | OpenAPI openapi.OpenAPI 79 | Operations []Operation 80 | } 81 | 82 | // Build -. 83 | func (b *Builder) Build() (API, error) { 84 | for path, method := range b.OpenAPI.Paths { 85 | if err := b.Add(path, http.MethodGet, method.Get); err != nil { 86 | return API{}, err 87 | } 88 | 89 | if err := b.Add(path, http.MethodPost, method.Post); err != nil { 90 | return API{}, err 91 | } 92 | 93 | if err := b.Add(path, http.MethodPut, method.Put); err != nil { 94 | return API{}, err 95 | } 96 | 97 | if err := b.Add(path, http.MethodPatch, method.Patch); err != nil { 98 | return API{}, err 99 | } 100 | 101 | if err := b.Add(path, http.MethodDelete, method.Delete); err != nil { 102 | return API{}, err 103 | } 104 | } 105 | 106 | return API{Operations: b.Operations}, nil 107 | } 108 | 109 | // Add -. 110 | func (b *Builder) Add(path, method string, o *openapi.Operation) error { 111 | if o != nil { 112 | p := pathfmt.RemoveTrailingSlash(path) 113 | 114 | operation, err := b.Set(p, method, o) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | b.Operations = append(b.Operations, operation) 120 | } 121 | 122 | return nil 123 | } 124 | 125 | // Set -. 126 | func (b *Builder) Set(path, method string, o *openapi.Operation) (Operation, error) { 127 | operation := Operation{ 128 | Method: method, 129 | Path: path, 130 | } 131 | 132 | if nil == o { 133 | return operation, nil 134 | } 135 | 136 | body, ok := o.RequestBody.Content["application/json"] 137 | if ok || len(o.RequestBody.Ref) > 0 { 138 | var s openapi.Schema 139 | 140 | switch { 141 | case len(o.RequestBody.Ref) > 0: 142 | requestBody, err := b.OpenAPI.LookupRequestBodyByReference(o.RequestBody.Ref) 143 | if err != nil { 144 | return Operation{}, fmt.Errorf("resolve reference: %w", err) 145 | } 146 | 147 | body, ok := requestBody.Content["application/json"] 148 | if ok { 149 | s = body.Schema 150 | } 151 | case body.Schema.IsRef(): 152 | schema, err := b.OpenAPI.LookupSchemaByReference(body.Schema.Ref) 153 | if err != nil { 154 | return Operation{}, fmt.Errorf("resolve reference: %w", err) 155 | } 156 | 157 | s = schema 158 | default: 159 | s = body.Schema 160 | } 161 | 162 | operation.Body = make(map[string]FieldType, len(s.Properties)) 163 | 164 | for _, v := range s.Required { 165 | operation.Body[v] = FieldType{Required: true} 166 | } 167 | 168 | for k, v := range s.Properties { 169 | operation.Body[k] = FieldType{ 170 | Required: operation.Body[k].Required, 171 | Type: v.Type, 172 | } 173 | } 174 | } 175 | 176 | for code, resp := range o.Responses { 177 | statusCode, err := strconv.Atoi(code) 178 | if err != nil { 179 | return Operation{}, err 180 | } 181 | 182 | if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { 183 | continue 184 | } 185 | 186 | content, ok := resp.Content["application/json"] 187 | if !ok { 188 | operation.Responses = append(operation.Responses, Response{ 189 | StatusCode: statusCode, 190 | }) 191 | 192 | continue 193 | } 194 | 195 | example := openapi.ExampleToResponse(content.Example) 196 | 197 | examples := make(map[string]any, len(content.Examples)+1) 198 | 199 | if len(content.Examples) > 0 { 200 | for key, e := range content.Examples { 201 | examples[key] = openapi.ExampleToResponse(e.Value) 202 | } 203 | 204 | examples[""] = openapi.ExampleToResponse(content.Examples[content.Examples.GetKeys()[0]].Value) 205 | } 206 | 207 | schema, err := b.convertSchema(content.Schema) 208 | if err != nil { 209 | return Operation{}, err 210 | } 211 | 212 | operation.Responses = append(operation.Responses, Response{ 213 | StatusCode: statusCode, 214 | MediaType: "application/json", 215 | Schema: schema, 216 | Example: example, 217 | Examples: examples, 218 | }) 219 | } 220 | 221 | return operation, nil 222 | } 223 | 224 | func (b *Builder) convertSchema(s openapi.Schema) (Schema, error) { 225 | if s.IsRef() { 226 | schema, err := b.OpenAPI.LookupSchemaByReference(s.Ref) 227 | if err != nil { 228 | return nil, fmt.Errorf("resolve reference: %w", err) 229 | } 230 | 231 | s = schema 232 | } 233 | 234 | f, err := faker.Faker(s.Faker) 235 | if s.Faker != "" && err == nil { 236 | return FakerSchema{Example: f}, nil 237 | } 238 | 239 | switch s.Type { 240 | case "boolean": 241 | val, _ := s.Example.(bool) 242 | return BooleanSchema{Example: val}, nil 243 | case "integer": 244 | val, _ := s.Example.(int64) 245 | return IntSchema{Example: val}, nil 246 | case "number": 247 | val, _ := s.Example.(float64) 248 | return FloatSchema{Example: val}, nil 249 | case "string": 250 | val, _ := s.Example.(string) 251 | return StringSchema{Example: val}, nil 252 | case "array": 253 | if nil == s.Items { 254 | return nil, ErrEmptyItems 255 | } 256 | 257 | itemsSchema, err := b.convertSchema(*s.Items) 258 | if err != nil { 259 | return nil, err 260 | } 261 | 262 | arrExample, err := ParseArrayExample(s.Example) 263 | if err != nil { 264 | return nil, err 265 | } 266 | 267 | return ArraySchema{ 268 | Type: itemsSchema, 269 | Example: arrExample, 270 | }, nil 271 | case "object": 272 | obj := ObjectSchema{Properties: make(map[string]Schema, len(s.Properties))} 273 | 274 | for key, prop := range s.Properties { 275 | propSchema, err := b.convertSchema(*prop) 276 | if err != nil { 277 | return nil, err 278 | } 279 | 280 | obj.Properties[key] = propSchema 281 | } 282 | 283 | objExample, err := ParseObjectExample(s.Example) 284 | if err != nil { 285 | return nil, err 286 | } 287 | 288 | obj.Example = objExample 289 | 290 | return obj, nil 291 | default: 292 | return nil, &SchemaTypeError{SchemaType: s.Type} 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /internal/api/build_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/neotoolkit/openapi" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/neotoolkit/dummy/internal/api" 14 | ) 15 | 16 | func TestSchemaTypeError(t *testing.T) { 17 | got := &api.SchemaTypeError{ 18 | SchemaType: "", 19 | } 20 | 21 | require.Equal(t, got.Error(), "unknown type ") 22 | } 23 | 24 | func TestArrayExampleError(t *testing.T) { 25 | got := &api.ArrayExampleError{ 26 | Data: "", 27 | } 28 | 29 | require.Equal(t, got.Error(), "unpredicted type for example string") 30 | } 31 | 32 | func TestParseArrayExample(t *testing.T) { 33 | tests := []struct { 34 | name string 35 | data any 36 | want []any 37 | err error 38 | }{ 39 | { 40 | name: "nil data", 41 | data: nil, 42 | want: []any{}, 43 | err: nil, 44 | }, 45 | { 46 | name: "array", 47 | data: []any{ 48 | map[string]any{ 49 | "key": "value", 50 | }, 51 | }, 52 | want: []any{ 53 | map[string]any{ 54 | "key": "value", 55 | }, 56 | }, 57 | err: nil, 58 | }, 59 | { 60 | name: "not array", 61 | data: "string", 62 | want: nil, 63 | err: &api.ArrayExampleError{Data: "string"}, 64 | }, 65 | } 66 | 67 | for _, tc := range tests { 68 | t.Run(tc.name, func(t *testing.T) { 69 | res, err := api.ParseArrayExample(tc.data) 70 | if err != nil { 71 | require.EqualError(t, err, tc.err.Error()) 72 | } 73 | 74 | require.Equal(t, tc.want, res) 75 | }) 76 | } 77 | } 78 | 79 | func TestObjectExampleError(t *testing.T) { 80 | got := &api.ObjectExampleError{ 81 | Data: "", 82 | } 83 | 84 | require.Equal(t, got.Error(), "unpredicted type for example string") 85 | } 86 | 87 | func TestParseObjectExample(t *testing.T) { 88 | tests := []struct { 89 | name string 90 | data any 91 | want map[string]any 92 | err error 93 | }{ 94 | { 95 | name: "nil data", 96 | data: nil, 97 | want: map[string]any{}, 98 | err: nil, 99 | }, 100 | { 101 | name: "object", 102 | data: map[string]any{}, 103 | want: map[string]any{}, 104 | err: nil, 105 | }, 106 | { 107 | name: "not object", 108 | data: "string", 109 | want: nil, 110 | err: &api.ObjectExampleError{Data: "string"}, 111 | }, 112 | } 113 | 114 | for _, tc := range tests { 115 | t.Run(tc.name, func(t *testing.T) { 116 | res, err := api.ParseObjectExample(tc.data) 117 | if err != nil { 118 | require.EqualError(t, err, tc.err.Error()) 119 | } 120 | 121 | require.Equal(t, tc.want, res) 122 | }) 123 | } 124 | } 125 | 126 | func TestBuilder_Build(t *testing.T) { 127 | tests := []struct { 128 | name string 129 | builder api.Builder 130 | want api.API 131 | err error 132 | }{ 133 | { 134 | name: "", 135 | builder: api.Builder{}, 136 | want: api.API{}, 137 | err: nil, 138 | }, 139 | { 140 | name: "GET", 141 | builder: api.Builder{ 142 | OpenAPI: openapi.OpenAPI{ 143 | Paths: map[string]*openapi.Path{ 144 | "test": { 145 | Get: &openapi.Operation{}, 146 | }, 147 | }, 148 | }, 149 | }, 150 | want: api.API{ 151 | Operations: []api.Operation{ 152 | { 153 | Method: "GET", 154 | Path: "test", 155 | Body: map[string]api.FieldType(nil), 156 | Responses: []api.Response(nil), 157 | }, 158 | }, 159 | }, 160 | err: nil, 161 | }, 162 | { 163 | name: "Wrong status code in GET", 164 | builder: api.Builder{ 165 | OpenAPI: openapi.OpenAPI{ 166 | Paths: map[string]*openapi.Path{ 167 | "test": { 168 | Get: &openapi.Operation{ 169 | Responses: map[string]*openapi.Response{ 170 | "Wrong status code": nil, 171 | }, 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, 177 | want: api.API{}, 178 | err: &strconv.NumError{ 179 | Func: "Atoi", 180 | Num: "Wrong status code", 181 | Err: strconv.ErrSyntax, 182 | }, 183 | }, 184 | { 185 | name: "POST", 186 | builder: api.Builder{ 187 | OpenAPI: openapi.OpenAPI{ 188 | Paths: map[string]*openapi.Path{ 189 | "test": { 190 | Post: &openapi.Operation{}, 191 | }, 192 | }, 193 | }, 194 | }, 195 | want: api.API{ 196 | Operations: []api.Operation{ 197 | { 198 | Method: "POST", 199 | Path: "test", 200 | Body: map[string]api.FieldType(nil), 201 | Responses: []api.Response(nil), 202 | }, 203 | }, 204 | }, 205 | err: nil, 206 | }, 207 | { 208 | name: "Wrong schema reference in POST", 209 | builder: api.Builder{ 210 | OpenAPI: openapi.OpenAPI{ 211 | Paths: map[string]*openapi.Path{ 212 | "test": { 213 | Post: &openapi.Operation{ 214 | RequestBody: openapi.RequestBody{ 215 | Content: map[string]*openapi.MediaType{ 216 | "application/json": { 217 | Schema: openapi.Schema{ 218 | Ref: "wrong schema reference", 219 | }, 220 | }, 221 | }, 222 | }, 223 | }, 224 | }, 225 | }, 226 | }, 227 | }, 228 | want: api.API{}, 229 | err: fmt.Errorf("resolve reference: %w", &openapi.SchemaError{Ref: "wrong schema reference"}), 230 | }, 231 | { 232 | name: "PUT", 233 | builder: api.Builder{ 234 | OpenAPI: openapi.OpenAPI{ 235 | Paths: map[string]*openapi.Path{ 236 | "test": { 237 | Put: &openapi.Operation{}, 238 | }, 239 | }, 240 | }, 241 | }, 242 | want: api.API{ 243 | Operations: []api.Operation{ 244 | { 245 | Method: "PUT", 246 | Path: "test", 247 | Body: map[string]api.FieldType(nil), 248 | Responses: []api.Response(nil), 249 | }, 250 | }, 251 | }, 252 | err: nil, 253 | }, 254 | { 255 | name: "Wrong schema reference in PUT", 256 | builder: api.Builder{ 257 | OpenAPI: openapi.OpenAPI{ 258 | Paths: map[string]*openapi.Path{ 259 | "test": { 260 | Put: &openapi.Operation{ 261 | RequestBody: openapi.RequestBody{ 262 | Content: map[string]*openapi.MediaType{ 263 | "application/json": { 264 | Schema: openapi.Schema{ 265 | Ref: "wrong schema reference", 266 | }, 267 | }, 268 | }, 269 | }, 270 | }, 271 | }, 272 | }, 273 | }, 274 | }, 275 | want: api.API{}, 276 | err: fmt.Errorf("resolve reference: %w", &openapi.SchemaError{Ref: "wrong schema reference"}), 277 | }, 278 | { 279 | name: "PATCH", 280 | builder: api.Builder{ 281 | OpenAPI: openapi.OpenAPI{ 282 | Paths: map[string]*openapi.Path{ 283 | "test": { 284 | Patch: &openapi.Operation{}, 285 | }, 286 | }, 287 | }, 288 | }, 289 | want: api.API{ 290 | Operations: []api.Operation{ 291 | { 292 | Method: "PATCH", 293 | Path: "test", 294 | Body: map[string]api.FieldType(nil), 295 | Responses: []api.Response(nil), 296 | }, 297 | }, 298 | }, 299 | err: nil, 300 | }, 301 | { 302 | name: "Wrong schema reference in PATCH", 303 | builder: api.Builder{ 304 | OpenAPI: openapi.OpenAPI{ 305 | Paths: map[string]*openapi.Path{ 306 | "test": { 307 | Patch: &openapi.Operation{ 308 | RequestBody: openapi.RequestBody{ 309 | Content: map[string]*openapi.MediaType{ 310 | "application/json": { 311 | Schema: openapi.Schema{ 312 | Ref: "wrong schema reference", 313 | }, 314 | }, 315 | }, 316 | }, 317 | }, 318 | }, 319 | }, 320 | }, 321 | }, 322 | want: api.API{}, 323 | err: fmt.Errorf("resolve reference: %w", &openapi.SchemaError{Ref: "wrong schema reference"}), 324 | }, 325 | { 326 | name: "DELETE", 327 | builder: api.Builder{ 328 | OpenAPI: openapi.OpenAPI{ 329 | Paths: map[string]*openapi.Path{ 330 | "test": { 331 | Delete: &openapi.Operation{}, 332 | }, 333 | }, 334 | }, 335 | }, 336 | want: api.API{ 337 | Operations: []api.Operation{ 338 | { 339 | Method: "DELETE", 340 | Path: "test", 341 | Body: map[string]api.FieldType(nil), 342 | Responses: []api.Response(nil), 343 | }, 344 | }, 345 | }, 346 | err: nil, 347 | }, 348 | { 349 | name: "Wrong status code in DELETE", 350 | builder: api.Builder{ 351 | OpenAPI: openapi.OpenAPI{ 352 | Paths: map[string]*openapi.Path{ 353 | "test": { 354 | Delete: &openapi.Operation{ 355 | Responses: map[string]*openapi.Response{ 356 | "Wrong status code": nil, 357 | }, 358 | }, 359 | }, 360 | }, 361 | }, 362 | }, 363 | want: api.API{}, 364 | err: &strconv.NumError{ 365 | Func: "Atoi", 366 | Num: "Wrong status code", 367 | Err: strconv.ErrSyntax, 368 | }, 369 | }, 370 | } 371 | 372 | for _, tc := range tests { 373 | t.Run(tc.name, func(t *testing.T) { 374 | b := tc.builder 375 | 376 | res, err := b.Build() 377 | if err != nil { 378 | require.EqualError(t, err, tc.err.Error()) 379 | } 380 | 381 | require.Equal(t, tc.want, res) 382 | }) 383 | } 384 | } 385 | 386 | func TestBuilder_Add(t *testing.T) { 387 | tests := []struct { 388 | name string 389 | builder api.Builder 390 | path string 391 | method string 392 | operation *openapi.Operation 393 | err error 394 | }{ 395 | { 396 | name: "", 397 | builder: api.Builder{}, 398 | path: "", 399 | method: "", 400 | operation: nil, 401 | err: nil, 402 | }, 403 | { 404 | name: "", 405 | builder: api.Builder{}, 406 | path: "", 407 | method: "", 408 | operation: &openapi.Operation{ 409 | RequestBody: openapi.RequestBody{ 410 | Content: map[string]*openapi.MediaType{ 411 | "application/json": { 412 | Schema: openapi.Schema{ 413 | Ref: "wrong schema reference", 414 | }, 415 | }, 416 | }, 417 | }, 418 | }, 419 | err: fmt.Errorf("resolve reference: %w", &openapi.SchemaError{Ref: "wrong schema reference"}), 420 | }, 421 | { 422 | name: "", 423 | builder: api.Builder{}, 424 | path: "", 425 | method: "", 426 | operation: &openapi.Operation{}, 427 | err: nil, 428 | }, 429 | } 430 | 431 | for _, tc := range tests { 432 | t.Run(tc.name, func(t *testing.T) { 433 | b := tc.builder 434 | 435 | err := b.Add(tc.path, tc.method, tc.operation) 436 | if err != nil { 437 | require.EqualError(t, err, tc.err.Error()) 438 | } 439 | }) 440 | } 441 | } 442 | 443 | func TestBuilder_Set(t *testing.T) { 444 | tests := []struct { 445 | name string 446 | builder api.Builder 447 | path string 448 | method string 449 | operation *openapi.Operation 450 | want api.Operation 451 | err error 452 | }{ 453 | { 454 | name: "", 455 | builder: api.Builder{}, 456 | path: "", 457 | method: "", 458 | operation: nil, 459 | want: api.Operation{}, 460 | err: nil, 461 | }, 462 | { 463 | name: "", 464 | builder: api.Builder{}, 465 | path: "", 466 | method: "", 467 | operation: &openapi.Operation{ 468 | RequestBody: openapi.RequestBody{ 469 | Content: map[string]*openapi.MediaType{ 470 | "application/json": { 471 | Schema: openapi.Schema{ 472 | Ref: "wrong schema reference", 473 | }, 474 | }, 475 | }, 476 | }, 477 | }, 478 | want: api.Operation{}, 479 | err: fmt.Errorf("resolve reference: %w", &openapi.SchemaError{Ref: "wrong schema reference"}), 480 | }, 481 | { 482 | name: "", 483 | builder: api.Builder{}, 484 | path: "", 485 | method: "", 486 | operation: &openapi.Operation{ 487 | RequestBody: openapi.RequestBody{ 488 | Content: map[string]*openapi.MediaType{ 489 | "application/json": { 490 | Schema: openapi.Schema{ 491 | Required: []string{"field"}, 492 | Properties: map[string]*openapi.Schema{ 493 | "prop": {}, 494 | }, 495 | }, 496 | }, 497 | }, 498 | }, 499 | }, 500 | want: api.Operation{ 501 | Body: map[string]api.FieldType{ 502 | "field": { 503 | Required: true, 504 | Type: "", 505 | }, 506 | "prop": { 507 | Required: false, 508 | Type: "", 509 | }, 510 | }, 511 | }, 512 | err: nil, 513 | }, 514 | { 515 | name: "", 516 | builder: api.Builder{}, 517 | path: "", 518 | method: "", 519 | operation: &openapi.Operation{ 520 | Responses: openapi.Responses{ 521 | "200": {}, 522 | }, 523 | }, 524 | want: api.Operation{ 525 | Responses: []api.Response{ 526 | { 527 | StatusCode: http.StatusOK, 528 | }, 529 | }, 530 | }, 531 | err: nil, 532 | }, 533 | { 534 | name: "", 535 | builder: api.Builder{}, 536 | path: "", 537 | method: "", 538 | operation: &openapi.Operation{ 539 | Responses: openapi.Responses{ 540 | "200": { 541 | Content: map[string]*openapi.MediaType{ 542 | "application/json": { 543 | Example: "example", 544 | }, 545 | }, 546 | }, 547 | }, 548 | }, 549 | want: api.Operation{}, 550 | err: &api.SchemaTypeError{SchemaType: ""}, 551 | }, 552 | { 553 | name: "", 554 | builder: api.Builder{}, 555 | path: "", 556 | method: "", 557 | operation: &openapi.Operation{ 558 | RequestBody: openapi.RequestBody{ 559 | Ref: "request-body", 560 | }, 561 | }, 562 | want: api.Operation{}, 563 | err: errors.New("resolve reference: unknown request body request-body"), 564 | }, 565 | } 566 | 567 | for _, tc := range tests { 568 | t.Run(tc.name, func(t *testing.T) { 569 | b := tc.builder 570 | 571 | got, err := b.Set(tc.path, tc.method, tc.operation) 572 | if err != nil { 573 | require.EqualError(t, err, tc.err.Error()) 574 | } 575 | 576 | require.Equal(t, tc.want, got) 577 | }) 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /internal/api/find.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | // FindResponseError -. 12 | type FindResponseError struct { 13 | Method string 14 | Path string 15 | } 16 | 17 | // Error -. 18 | func (e *FindResponseError) Error() string { 19 | return "not specified operation: " + e.Method + " " + e.Path 20 | } 21 | 22 | // FindResponseParams -. 23 | type FindResponseParams struct { 24 | Path string 25 | Method string 26 | Body io.ReadCloser 27 | MediaType string 28 | } 29 | 30 | // ErrEmptyRequireField -. 31 | var ErrEmptyRequireField = errors.New("empty require field") 32 | 33 | // FindResponse -. 34 | func (a API) FindResponse(params FindResponseParams) (Response, error) { 35 | operation, ok := a.findOperation(params) 36 | if !ok { 37 | return Response{}, &FindResponseError{ 38 | Method: params.Method, 39 | Path: params.Path, 40 | } 41 | } 42 | 43 | switch params.Method { 44 | case http.MethodPost, http.MethodPut, http.MethodPatch: 45 | var body map[string]any 46 | 47 | err := json.NewDecoder(params.Body).Decode(&body) 48 | if err != nil { 49 | return Response{}, err 50 | } 51 | 52 | for k, v := range operation.Body { 53 | _, ok := body[k] 54 | if !ok && v.Required { 55 | return Response{}, ErrEmptyRequireField 56 | } 57 | } 58 | } 59 | 60 | response, ok := operation.findOperationResponse(params) 61 | if !ok { 62 | return operation.Responses[0], nil 63 | } 64 | 65 | return response, nil 66 | } 67 | 68 | func (a API) findOperation(params FindResponseParams) (Operation, bool) { 69 | for _, op := range a.Operations { 70 | if !IsPathMatchTemplate(params.Path, op.Path) { 71 | continue 72 | } 73 | 74 | if params.Method != op.Method { 75 | continue 76 | } 77 | 78 | return op, true 79 | } 80 | 81 | return Operation{}, false 82 | } 83 | 84 | func (o Operation) findOperationResponse(params FindResponseParams) (Response, bool) { 85 | for _, r := range o.Responses { 86 | if r.MediaType != params.MediaType { 87 | continue 88 | } 89 | 90 | return r, true 91 | } 92 | 93 | return Response{}, false 94 | } 95 | 96 | // IsPathMatchTemplate returns true if path matches template 97 | func IsPathMatchTemplate(path, pathTemplate string) bool { 98 | pathSegments := strings.Split(path, "/") 99 | templateSegments := strings.Split(pathTemplate, "/") 100 | 101 | if len(pathSegments) != len(templateSegments) { 102 | return false 103 | } 104 | 105 | for i := 0; i < len(pathSegments); i++ { 106 | if strings.HasPrefix(templateSegments[i], "{") && strings.HasSuffix(templateSegments[i], "}") { 107 | continue 108 | } 109 | 110 | if pathSegments[i] != templateSegments[i] { 111 | return false 112 | } 113 | } 114 | 115 | return true 116 | } 117 | -------------------------------------------------------------------------------- /internal/api/find_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/neotoolkit/dummy/internal/api" 12 | ) 13 | 14 | type ResponseParamsBody struct { 15 | Body any 16 | IsBodyBroken bool 17 | } 18 | 19 | func (params ResponseParamsBody) Read(p []byte) (n int, err error) { 20 | jsonData, err := json.Marshal(params.Body) 21 | if params.IsBodyBroken || err != nil { 22 | jsonData = []byte{1, 2, 3} 23 | } 24 | 25 | readerResult, readerError := bytes.NewReader(jsonData).Read(p) 26 | 27 | return readerResult, readerError 28 | } 29 | 30 | func (ResponseParamsBody) Close() error { 31 | return nil 32 | } 33 | 34 | func TestIsPathMatchTemplate(t *testing.T) { 35 | tests := []struct { 36 | name string 37 | path string 38 | param string 39 | want bool 40 | }{ 41 | { 42 | name: "", 43 | path: "", 44 | param: "", 45 | want: true, 46 | }, 47 | { 48 | name: "", 49 | path: "/path", 50 | param: "/path", 51 | want: true, 52 | }, 53 | { 54 | name: "", 55 | path: "/path/1", 56 | param: "/path/{1}", 57 | want: true, 58 | }, 59 | { 60 | name: "", 61 | path: "/path/1/path/2", 62 | param: "/path/{1}/path/{2}", 63 | want: true, 64 | }, 65 | { 66 | name: "", 67 | path: "/path/1/path1/2", 68 | param: "/path/{1}/path/{2}", 69 | want: false, 70 | }, 71 | { 72 | name: "", 73 | path: "/path/1/path/2", 74 | param: "/path/{1}/path1/{2}", 75 | want: false, 76 | }, 77 | { 78 | name: "", 79 | path: "/path/1/path/2/path", 80 | param: "/path/{1}/path/{2}", 81 | want: false, 82 | }, 83 | } 84 | 85 | for _, tc := range tests { 86 | t.Run(tc.name, func(t *testing.T) { 87 | got := api.IsPathMatchTemplate(tc.path, tc.param) 88 | 89 | require.Equal(t, tc.want, got) 90 | }) 91 | } 92 | } 93 | 94 | func TestFindResponseError(t *testing.T) { 95 | got := &api.FindResponseError{ 96 | Method: "test method", 97 | Path: "test path", 98 | } 99 | 100 | require.Equal(t, got.Error(), "not specified operation: test method test path") 101 | } 102 | 103 | func TestFindResponse(t *testing.T) { 104 | bodyFixedPath := map[string]api.FieldType{ 105 | "param1": { 106 | Required: true, 107 | Type: "string", 108 | }, 109 | "param2": { 110 | Required: false, 111 | Type: "string", 112 | }, 113 | } 114 | 115 | responseFixedPathJSON := api.Response{ 116 | StatusCode: 200, 117 | MediaType: "application/json", 118 | } 119 | responseFixedPathZip := api.Response{ 120 | StatusCode: 200, 121 | MediaType: "application/zip", 122 | } 123 | 124 | operationFixedPath := api.Operation{ 125 | Method: "POST", 126 | Path: "some/fixed/path", 127 | Body: bodyFixedPath, 128 | Responses: []api.Response{ 129 | responseFixedPathJSON, 130 | responseFixedPathZip, 131 | }, 132 | } 133 | 134 | a := api.API{ 135 | Operations: []api.Operation{ 136 | operationFixedPath, 137 | }, 138 | } 139 | 140 | mismatchByOperationPathError := api.FindResponseError{ 141 | Method: "POST", 142 | Path: "some/other/path", 143 | } 144 | mismatchByOperationMethodError := api.FindResponseError{ 145 | Method: "GET", 146 | Path: "some/fixed/path", 147 | } 148 | bodyWithoutRequiredParamError := errors.New("empty require field") 149 | 150 | bodyWithoutRequiredParam := map[string]any{ 151 | "param3": "qwe", 152 | "param2": "rty", 153 | } 154 | bodyWithoutOptionalParam := map[string]any{ 155 | "param3": "qwe", 156 | "param1": "rty", 157 | } 158 | bodyWithAllParam := map[string]any{ 159 | "param1": "qwe", 160 | "param2": "rty", 161 | } 162 | 163 | emptyResponse := api.Response{} 164 | 165 | tests := []struct { 166 | name string 167 | path string 168 | method string 169 | body any 170 | wantFirst api.Response 171 | wantSecond any 172 | responseMediaType string 173 | }{ 174 | { 175 | name: "Mismatch by operation path", 176 | path: "some/other/path", 177 | method: "POST", 178 | body: bodyFixedPath, 179 | wantFirst: emptyResponse, 180 | wantSecond: &mismatchByOperationPathError, 181 | }, 182 | { 183 | name: "Mismatch by operation method", 184 | path: "some/fixed/path", 185 | method: "GET", 186 | body: bodyFixedPath, 187 | wantFirst: emptyResponse, 188 | wantSecond: &mismatchByOperationMethodError, 189 | }, 190 | { 191 | name: "Mismatch by body without required params", 192 | path: "some/fixed/path", 193 | method: "POST", 194 | body: bodyWithoutRequiredParam, 195 | wantFirst: emptyResponse, 196 | wantSecond: bodyWithoutRequiredParamError, 197 | }, 198 | { 199 | name: "Match by body without optional params", 200 | path: "some/fixed/path", 201 | method: "POST", 202 | body: bodyWithoutOptionalParam, 203 | wantFirst: responseFixedPathJSON, 204 | wantSecond: nil, 205 | }, 206 | { 207 | name: "Match by response without selected media type", 208 | path: "some/fixed/path", 209 | method: "POST", 210 | body: bodyWithAllParam, 211 | wantFirst: responseFixedPathJSON, 212 | wantSecond: nil, 213 | }, 214 | { 215 | name: "Match by all criteria", 216 | path: "some/fixed/path", 217 | method: "POST", 218 | body: bodyWithAllParam, 219 | wantFirst: responseFixedPathZip, 220 | wantSecond: nil, 221 | responseMediaType: "application/zip", 222 | }, 223 | } 224 | 225 | for _, tc := range tests { 226 | t.Run(tc.name, func(t *testing.T) { 227 | bodyReader := ResponseParamsBody{Body: tc.body} 228 | mediaType := "application/xml" 229 | 230 | if tc.responseMediaType != "" { 231 | mediaType = tc.responseMediaType 232 | } 233 | params := api.FindResponseParams{ 234 | Path: tc.path, 235 | Method: tc.method, 236 | Body: bodyReader, 237 | MediaType: mediaType, 238 | } 239 | firstResult, secondResult := a.FindResponse(params) 240 | 241 | require.Equal(t, tc.wantFirst, firstResult) 242 | require.Equal(t, tc.wantSecond, secondResult) 243 | }) 244 | } 245 | 246 | t.Run("Broken json in body reader", func(t *testing.T) { 247 | params := api.FindResponseParams{ 248 | Path: "some/fixed/path", 249 | Method: "POST", 250 | Body: ResponseParamsBody{Body: bodyWithAllParam, IsBodyBroken: true}, 251 | MediaType: "application/xml", 252 | } 253 | 254 | _, err := a.FindResponse(params) 255 | require.Error(t, err) 256 | }) 257 | } 258 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Config is struct for Config 4 | type Config struct { 5 | Server Server 6 | Logger Logger 7 | } 8 | 9 | // NewConfig returns a new instance of Config instance 10 | func NewConfig() *Config { 11 | return &Config{} 12 | } 13 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/neotoolkit/dummy/internal/config" 9 | ) 10 | 11 | func TestNewConfig(t *testing.T) { 12 | conf := config.NewConfig() 13 | 14 | require.IsType(t, &config.Config{}, conf) 15 | } 16 | -------------------------------------------------------------------------------- /internal/config/logger.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Logger is struct for Logger 4 | type Logger struct { 5 | Level string 6 | } 7 | -------------------------------------------------------------------------------- /internal/config/server.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Server is struct for Server 4 | type Server struct { 5 | // Path to OpenAPI specification 6 | Path string 7 | Port string 8 | } 9 | -------------------------------------------------------------------------------- /internal/exitcode/exitcode.go: -------------------------------------------------------------------------------- 1 | package exitcode 2 | 3 | const ( 4 | // Success -. 5 | Success = iota 6 | // Failure -. 7 | Failure 8 | ) 9 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | type Logger struct { 12 | logger *zap.SugaredLogger 13 | } 14 | 15 | func NewLogger(level string) *Logger { 16 | encoderCfg := zapcore.EncoderConfig{ 17 | MessageKey: "msg", 18 | LevelKey: "level", 19 | TimeKey: "time", 20 | NameKey: "logger", 21 | EncodeLevel: zapcore.LowercaseLevelEncoder, 22 | EncodeTime: zapcore.ISO8601TimeEncoder, 23 | EncodeDuration: zapcore.StringDurationEncoder, 24 | } 25 | encoder := zapcore.NewJSONEncoder(encoderCfg) 26 | l := encodeLevel(level) 27 | core := zapcore.NewCore(encoder, os.Stdout, l) 28 | logger := zap.New(core) 29 | 30 | return &Logger{logger: logger.Sugar()} 31 | } 32 | 33 | func (l *Logger) Debug(msg string) { 34 | l.logger.Debug(msg) 35 | } 36 | 37 | func (l *Logger) Info(msg string) { 38 | l.logger.Info(msg) 39 | } 40 | 41 | func (l *Logger) Infof(msg string, args ...any) { 42 | l.logger.Infof(msg, args) 43 | } 44 | 45 | func (l *Logger) Infow(msg string, w ...any) { 46 | l.logger.Infow(msg, w...) 47 | } 48 | 49 | func (l *Logger) Warn(msg string) { 50 | l.logger.Warn(msg) 51 | } 52 | 53 | func (l *Logger) Error(msg string) { 54 | l.logger.Error(msg) 55 | } 56 | 57 | func (l *Logger) Errorf(msg string, args ...any) { 58 | l.logger.Errorf(msg, args) 59 | } 60 | 61 | func (l *Logger) Errorw(msg string, w ...any) { 62 | l.logger.Errorw(msg, w...) 63 | } 64 | 65 | func encodeLevel(level string) zapcore.Level { 66 | switch strings.ToLower(level) { 67 | case "debug": 68 | return zap.DebugLevel 69 | case "info": 70 | return zap.InfoLevel 71 | case "warn": 72 | return zap.WarnLevel 73 | case "error": 74 | return zap.ErrorLevel 75 | default: 76 | return zap.InfoLevel 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/neotoolkit/dummy/internal/logger" 9 | ) 10 | 11 | func TestNewLogger(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | level string 15 | }{ 16 | { 17 | name: "", 18 | level: "", 19 | }, 20 | { 21 | name: "Logger with INFO level", 22 | level: "INFO", 23 | }, 24 | { 25 | name: "Logger with DEBUG level", 26 | level: "DEBUG", 27 | }, 28 | } 29 | 30 | for _, tc := range tests { 31 | t.Run(tc.name, func(t *testing.T) { 32 | got := logger.NewLogger(tc.level) 33 | 34 | require.IsType(t, &logger.Logger{}, got) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/middleware/logging.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/neotoolkit/dummy/internal/pkg/logger" 8 | ) 9 | 10 | type responseWriter struct { 11 | http.ResponseWriter 12 | status int 13 | } 14 | 15 | func wrapResponseWriter(w http.ResponseWriter) *responseWriter { 16 | return &responseWriter{ResponseWriter: w} 17 | } 18 | 19 | func (rw *responseWriter) Status() int { 20 | return rw.status 21 | } 22 | 23 | func (rw *responseWriter) WriteHeader(code int) { 24 | rw.status = code 25 | rw.ResponseWriter.WriteHeader(code) 26 | } 27 | 28 | // Logging -. 29 | func Logging(next http.Handler, logger logger.Logger) http.Handler { 30 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | logger.Infow("request", 32 | "path", r.URL.Path, 33 | "method", r.Method, 34 | "header", r.Header, 35 | "body", r.Body, 36 | ) 37 | 38 | wrapped := wrapResponseWriter(w) 39 | 40 | start := time.Now() 41 | 42 | next.ServeHTTP(wrapped, r) 43 | 44 | logger.Infow("response", 45 | "status-code", wrapped.Status(), 46 | "header", wrapped.Header(), 47 | "duration", time.Since(start), 48 | ) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /internal/middleware/logging_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "github.com/neotoolkit/dummy/internal/logger" 5 | "github.com/neotoolkit/dummy/internal/middleware" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestLogging(t *testing.T) { 12 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | w.WriteHeader(http.StatusOK) 14 | }) 15 | 16 | l := logger.NewLogger("DEBUG") 17 | 18 | handler := middleware.Logging(next, l) 19 | 20 | req := httptest.NewRequest("GET", "https://", nil) 21 | 22 | handler.ServeHTTP(httptest.NewRecorder(), req) 23 | } 24 | -------------------------------------------------------------------------------- /internal/parse/parse.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "errors" 5 | "path/filepath" 6 | 7 | "github.com/goccy/go-yaml" 8 | "github.com/neotoolkit/openapi" 9 | 10 | "github.com/neotoolkit/dummy/internal/api" 11 | "github.com/neotoolkit/dummy/internal/read" 12 | ) 13 | 14 | type SpecType string 15 | 16 | const ( 17 | OpenAPI SpecType = "OpenAPI" 18 | GraphQL SpecType = "GraphQL" 19 | Unknown SpecType = "Unknown" 20 | ) 21 | 22 | var ErrEmptySpecTypePath = errors.New("empty spec type path") 23 | 24 | // SpecTypeError -. 25 | type SpecTypeError struct { 26 | Path string 27 | } 28 | 29 | // Error -. 30 | func (e *SpecTypeError) Error() string { 31 | return "specification type not implemented, path: " + e.Path 32 | } 33 | 34 | // SpecFileError -. 35 | type SpecFileError struct { 36 | Path string 37 | } 38 | 39 | // Error -. 40 | func (e *SpecFileError) Error() string { 41 | return e.Path + " without format" 42 | } 43 | 44 | // Parse -. 45 | func Parse(path string) (api.API, error) { 46 | file, err := read.Read(path) 47 | if err != nil { 48 | return api.API{}, err 49 | } 50 | 51 | specType, err := GetSpecType(path) 52 | if err != nil { 53 | return api.API{}, err 54 | } 55 | 56 | switch specType { 57 | case OpenAPI: 58 | oapi, err := openapi.Parse(file) 59 | if err != nil { 60 | return api.API{}, err 61 | } 62 | 63 | b := &api.Builder{ 64 | OpenAPI: oapi, 65 | } 66 | 67 | return b.Build() 68 | case GraphQL: 69 | return api.API{}, nil 70 | } 71 | 72 | return api.API{}, nil 73 | } 74 | 75 | // GetSpecType returns specification type for path 76 | func GetSpecType(path string) (SpecType, error) { 77 | if len(path) == 0 { 78 | return Unknown, ErrEmptySpecTypePath 79 | } 80 | 81 | p := filepath.Ext(path) 82 | 83 | if len(p) == 0 { 84 | return Unknown, &SpecFileError{Path: path} 85 | } 86 | 87 | file, err := read.Read(path) 88 | if err != nil { 89 | return Unknown, err 90 | } 91 | 92 | switch p { 93 | case ".yml", ".yaml": 94 | var oapi openapi.OpenAPI 95 | 96 | if err := yaml.Unmarshal(file, &oapi); err != nil || len(oapi.OpenAPI) == 0 { 97 | return Unknown, &SpecTypeError{Path: path} 98 | } 99 | 100 | return OpenAPI, nil 101 | case ".graphql": 102 | return GraphQL, nil 103 | default: 104 | return Unknown, &SpecFileError{Path: path} 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/parse/parse_test.go: -------------------------------------------------------------------------------- 1 | package parse_test 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "sort" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/neotoolkit/dummy/internal/api" 12 | "github.com/neotoolkit/dummy/internal/parse" 13 | ) 14 | 15 | func TestSpecTypeError(t *testing.T) { 16 | got := &parse.SpecTypeError{ 17 | Path: "test", 18 | } 19 | 20 | require.Equal(t, got.Error(), "specification type not implemented, path: test") 21 | } 22 | 23 | func TestSpecFileError(t *testing.T) { 24 | got := &parse.SpecFileError{ 25 | Path: "test", 26 | } 27 | 28 | require.Equal(t, got.Error(), "test without format") 29 | } 30 | 31 | func TestParse(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | path string 35 | want api.API 36 | err error 37 | }{ 38 | { 39 | name: "empty path", 40 | path: "", 41 | want: api.API{}, 42 | err: &fs.PathError{ 43 | Op: "open", 44 | Path: "", 45 | Err: errors.New("no such file or directory"), 46 | }, 47 | }, 48 | { 49 | name: "file without format", 50 | path: "./testdata/openapi", 51 | want: api.API{}, 52 | err: &parse.SpecFileError{ 53 | Path: "./testdata/openapi", 54 | }, 55 | }, 56 | { 57 | name: "graphql", 58 | path: "./testdata/schema.graphql", 59 | want: api.API{}, 60 | err: nil, 61 | }, 62 | { 63 | name: "", 64 | path: "./testdata/empty-openapi.yml", 65 | want: api.API{}, 66 | err: &parse.SpecTypeError{ 67 | Path: "./testdata/empty-openapi.yml", 68 | }, 69 | }, 70 | { 71 | name: "", 72 | path: "./testdata/openapi.yml", 73 | want: api.API{}, 74 | err: nil, 75 | }, 76 | { 77 | name: "unknown format", 78 | path: "./testdata/api.raml", 79 | want: api.API{}, 80 | err: &parse.SpecFileError{ 81 | Path: "./testdata/api.raml", 82 | }, 83 | }, 84 | } 85 | 86 | for _, tc := range tests { 87 | t.Run(tc.name, func(t *testing.T) { 88 | got, err := parse.Parse(tc.path) 89 | if err != nil { 90 | require.EqualError(t, err, tc.err.Error()) 91 | } 92 | require.Equal(t, tc.want, got) 93 | }) 94 | } 95 | } 96 | 97 | func TestParse_YAML(t *testing.T) { 98 | expected := api.API{ 99 | Operations: []api.Operation{ 100 | { 101 | Method: "POST", 102 | Path: "/users", 103 | Body: map[string]api.FieldType{ 104 | "id": { 105 | Required: true, 106 | Type: "string", 107 | }, 108 | "firstName": { 109 | Required: true, 110 | Type: "string", 111 | }, 112 | "lastName": { 113 | Required: true, 114 | Type: "string", 115 | }, 116 | }, 117 | Responses: []api.Response{ 118 | { 119 | StatusCode: 201, 120 | MediaType: "application/json", 121 | Schema: api.ObjectSchema{ 122 | Properties: map[string]api.Schema{ 123 | "id": api.StringSchema{Example: "380ed0b7-eb21-4ad4-acd0-efa90cf69c6a"}, 124 | "firstName": api.StringSchema{Example: "Larry"}, 125 | "lastName": api.StringSchema{Example: "Page"}, 126 | }, 127 | Example: map[string]any{}, 128 | }, 129 | Examples: map[string]any{}, 130 | }, 131 | }, 132 | }, 133 | { 134 | Method: "GET", 135 | Path: "/users", 136 | Responses: []api.Response{ 137 | { 138 | StatusCode: 200, 139 | MediaType: "application/json", 140 | Schema: api.ArraySchema{ 141 | Type: api.ObjectSchema{ 142 | Properties: map[string]api.Schema{ 143 | "id": api.StringSchema{Example: "380ed0b7-eb21-4ad4-acd0-efa90cf69c6a"}, 144 | "firstName": api.StringSchema{Example: "Larry"}, 145 | "lastName": api.StringSchema{Example: "Page"}, 146 | }, 147 | Example: map[string]any{}, 148 | }, 149 | Example: []any{}, 150 | }, 151 | Example: []map[string]any{ 152 | { 153 | "id": "e1afccea-5168-4735-84d4-cb96f6fb5d25", 154 | "firstName": "Elon", 155 | "lastName": "Musk", 156 | }, 157 | { 158 | "id": "472063cc-4c83-11ec-81d3-0242ac130003", 159 | "firstName": "Sergey", 160 | "lastName": "Brin", 161 | }, 162 | }, 163 | Examples: map[string]any{}, 164 | }, 165 | }, 166 | }, 167 | { 168 | Method: "GET", 169 | Path: "/users/{userId}", 170 | Responses: []api.Response{ 171 | { 172 | StatusCode: 200, 173 | MediaType: "application/json", 174 | Schema: api.ObjectSchema{ 175 | Properties: map[string]api.Schema{ 176 | "id": api.StringSchema{Example: "380ed0b7-eb21-4ad4-acd0-efa90cf69c6a"}, 177 | "firstName": api.StringSchema{Example: "Larry"}, 178 | "lastName": api.StringSchema{Example: "Page"}, 179 | }, 180 | Example: map[string]any{}, 181 | }, 182 | Examples: map[string]any{}, 183 | }, 184 | }, 185 | }, 186 | }, 187 | } 188 | 189 | openapi, err := parse.Parse("testdata/openapi3.yml") 190 | 191 | require.NoError(t, err) 192 | require.Equalf(t, testable(t, expected), testable(t, openapi), `parsed schema from "testdata/openapi3.yml"`) 193 | } 194 | 195 | func testable(t *testing.T, api api.API) api.API { 196 | t.Helper() 197 | 198 | sort.Slice(api.Operations, func(i, j int) bool { 199 | a, b := api.Operations[i], api.Operations[j] 200 | 201 | if a.Method > b.Method { 202 | return false 203 | } 204 | 205 | if a.Method < b.Method { 206 | return true 207 | } 208 | 209 | return a.Path < b.Path 210 | }) 211 | 212 | return api 213 | } 214 | 215 | func TestGetSpecType(t *testing.T) { 216 | tests := []struct { 217 | name string 218 | path string 219 | want parse.SpecType 220 | err error 221 | }{ 222 | { 223 | name: "", 224 | path: "", 225 | want: parse.Unknown, 226 | err: parse.ErrEmptySpecTypePath, 227 | }, 228 | { 229 | name: "", 230 | path: "./testdata/openapi3", 231 | want: parse.Unknown, 232 | err: &parse.SpecFileError{ 233 | Path: "./testdata/openapi3", 234 | }, 235 | }, 236 | { 237 | name: "", 238 | path: "./testdata/openapi3.yml", 239 | want: parse.OpenAPI, 240 | err: nil, 241 | }, 242 | { 243 | name: "", 244 | path: "./testdata/api.raml", 245 | want: parse.Unknown, 246 | err: &parse.SpecFileError{ 247 | Path: "./testdata/api.raml", 248 | }, 249 | }, 250 | { 251 | name: "", 252 | path: "./testdata/schema.graphql", 253 | want: parse.GraphQL, 254 | err: nil, 255 | }, 256 | } 257 | 258 | for _, tc := range tests { 259 | t.Run(tc.name, func(t *testing.T) { 260 | got, err := parse.GetSpecType(tc.path) 261 | if err != nil { 262 | require.EqualError(t, err, tc.err.Error()) 263 | } 264 | require.Equal(t, tc.want, got) 265 | }) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /internal/parse/testdata/api.raml: -------------------------------------------------------------------------------- 1 | #%RAML 1.0 2 | --- 3 | -------------------------------------------------------------------------------- /internal/parse/testdata/empty-openapi.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neotoolkit/dummy/5a873434ccc69ba574404ace33f26e703e3b7600/internal/parse/testdata/empty-openapi.yml -------------------------------------------------------------------------------- /internal/parse/testdata/openapi: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Users dummy API 4 | version: 0.1.0 5 | paths: 6 | /users: 7 | post: 8 | requestBody: 9 | required: true 10 | content: 11 | application/json: 12 | schema: 13 | $ref: "#/components/schemas/User" 14 | responses: 15 | '201': 16 | description: '' 17 | content: 18 | application/json: 19 | schema: 20 | $ref: '#/components/schemas/User' 21 | get: 22 | responses: 23 | '200': 24 | description: '' 25 | content: 26 | application/json: 27 | schema: 28 | type: array 29 | items: 30 | $ref: '#/components/schemas/User' 31 | example: 32 | - id: e1afccea-5168-4735-84d4-cb96f6fb5d25 33 | firstName: Elon 34 | lastName: Musk 35 | - id: 472063cc-4c83-11ec-81d3-0242ac130003 36 | firstName: Sergey 37 | lastName: Brin 38 | 39 | /users/{userId}: 40 | get: 41 | parameters: 42 | - in: path 43 | name: userId 44 | description: '' 45 | required: true 46 | schema: 47 | type: string 48 | responses: 49 | '200': 50 | description: '' 51 | content: 52 | application/json: 53 | schema: 54 | $ref: '#/components/schemas/User' 55 | 56 | components: 57 | schemas: 58 | User: 59 | type: object 60 | required: 61 | - id 62 | - firstName 63 | - lastName 64 | properties: 65 | id: 66 | type: string 67 | format: uuid 68 | example: 380ed0b7-eb21-4ad4-acd0-efa90cf69c6a 69 | firstName: 70 | type: string 71 | example: Larry 72 | lastName: 73 | type: string 74 | example: Page 75 | -------------------------------------------------------------------------------- /internal/parse/testdata/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Users dummy API 4 | version: 0.1.0 5 | -------------------------------------------------------------------------------- /internal/parse/testdata/openapi3.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Users dummy API 4 | version: 0.1.0 5 | paths: 6 | /users: 7 | post: 8 | requestBody: 9 | required: true 10 | content: 11 | application/json: 12 | schema: 13 | $ref: "#/components/schemas/User" 14 | responses: 15 | '201': 16 | description: '' 17 | content: 18 | application/json: 19 | schema: 20 | $ref: '#/components/schemas/User' 21 | get: 22 | responses: 23 | '200': 24 | description: '' 25 | content: 26 | application/json: 27 | schema: 28 | type: array 29 | items: 30 | $ref: '#/components/schemas/User' 31 | example: 32 | - id: e1afccea-5168-4735-84d4-cb96f6fb5d25 33 | firstName: Elon 34 | lastName: Musk 35 | - id: 472063cc-4c83-11ec-81d3-0242ac130003 36 | firstName: Sergey 37 | lastName: Brin 38 | 39 | /users/{userId}: 40 | get: 41 | parameters: 42 | - in: path 43 | name: userId 44 | description: '' 45 | required: true 46 | schema: 47 | type: string 48 | responses: 49 | '200': 50 | description: '' 51 | content: 52 | application/json: 53 | schema: 54 | $ref: '#/components/schemas/User' 55 | 56 | components: 57 | schemas: 58 | User: 59 | type: object 60 | required: 61 | - id 62 | - firstName 63 | - lastName 64 | properties: 65 | id: 66 | type: string 67 | format: uuid 68 | example: 380ed0b7-eb21-4ad4-acd0-efa90cf69c6a 69 | firstName: 70 | type: string 71 | example: Larry 72 | lastName: 73 | type: string 74 | example: Page 75 | -------------------------------------------------------------------------------- /internal/parse/testdata/schema.graphql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neotoolkit/dummy/5a873434ccc69ba574404ace33f26e703e3b7600/internal/parse/testdata/schema.graphql -------------------------------------------------------------------------------- /internal/pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | type Logger interface { 4 | Debug(msg string) 5 | Info(msg string) 6 | Infof(msg string, args ...any) 7 | Infow(msg string, w ...any) 8 | Warn(msg string) 9 | Error(msg string) 10 | Errorf(msg string, args ...any) 11 | Errorw(msg string, w ...any) 12 | } 13 | -------------------------------------------------------------------------------- /internal/pkg/pathfmt/pathfmt.go: -------------------------------------------------------------------------------- 1 | package pathfmt 2 | 3 | import "strings" 4 | 5 | // RemoveTrailingSlash returns path without trailing slash 6 | func RemoveTrailingSlash(path string) string { 7 | if len(path) > 0 && path[len(path)-1] == '/' { 8 | return path[0 : len(path)-1] 9 | } 10 | 11 | return path 12 | } 13 | 14 | // RemoveFragment - clear url from reference in path 15 | func RemoveFragment(path string) string { 16 | return RemoveTrailingSlash(strings.Split(path, "#")[0]) 17 | } 18 | -------------------------------------------------------------------------------- /internal/pkg/pathfmt/pathfmt_test.go: -------------------------------------------------------------------------------- 1 | package pathfmt_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/neotoolkit/dummy/internal/pkg/pathfmt" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRemoveTrailingSlash(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | path string 14 | want string 15 | }{ 16 | { 17 | name: "", 18 | path: "", 19 | want: "", 20 | }, 21 | { 22 | name: "", 23 | path: "/", 24 | want: "", 25 | }, 26 | { 27 | name: "", 28 | path: "path/", 29 | want: "path", 30 | }, 31 | } 32 | 33 | for _, tc := range tests { 34 | t.Run(tc.name, func(t *testing.T) { 35 | got := pathfmt.RemoveTrailingSlash(tc.path) 36 | 37 | require.Equal(t, tc.want, got) 38 | }) 39 | } 40 | } 41 | 42 | func TestRemoveFragment(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | path string 46 | want string 47 | }{ 48 | { 49 | name: "", 50 | path: "", 51 | want: "", 52 | }, 53 | { 54 | name: "", 55 | path: "/", 56 | want: "", 57 | }, 58 | { 59 | name: "", 60 | path: "/#", 61 | want: "", 62 | }, 63 | } 64 | 65 | for _, tc := range tests { 66 | t.Run(tc.name, func(t *testing.T) { 67 | got := pathfmt.RemoveFragment(tc.path) 68 | 69 | require.Equal(t, tc.want, got) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/read/read.go: -------------------------------------------------------------------------------- 1 | package read 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Read -. 10 | func Read(path string) ([]byte, error) { 11 | if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { 12 | return url(path) 13 | } 14 | 15 | return file(path) 16 | } 17 | 18 | func url(url string) ([]byte, error) { 19 | resp, err := http.Get(url) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer resp.Body.Close() 24 | 25 | body, err := ioutil.ReadAll(resp.Body) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return body, nil 31 | } 32 | 33 | func file(path string) ([]byte, error) { 34 | file, err := ioutil.ReadFile(path) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return file, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/read/read_test.go: -------------------------------------------------------------------------------- 1 | package read_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/fs" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/neotoolkit/dummy/internal/read" 15 | ) 16 | 17 | func TestRead(t *testing.T) { 18 | mux := http.NewServeMux() 19 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 20 | w.Header().Set("Content-Type", "application/json") 21 | fmt.Fprintln(w, `{ 22 | "openapi": "3.0.3", 23 | "info": { 24 | "title": "Test dummy API", 25 | "version": "0.1.0" 26 | }, 27 | "paths": { 28 | "/healthz": { 29 | "get": { 30 | "responses": { 31 | "201": { 32 | "description": "" 33 | } 34 | } 35 | } 36 | } 37 | } 38 | }`) 39 | }) 40 | mux.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) { 41 | w.Header().Add("Content-Length", "50") 42 | // return less bytes, which will result in an "unexpected EOF" from ioutil.ReadAll() 43 | fmt.Fprintln(w, []byte("a")) 44 | }) 45 | 46 | ts := httptest.NewServer(mux) 47 | defer ts.Close() 48 | 49 | tests := []struct { 50 | name string 51 | path string 52 | err error 53 | }{ 54 | { 55 | name: "empty URL", 56 | path: "https://", 57 | err: &url.Error{ 58 | Op: "Get", 59 | URL: "https:", 60 | Err: errors.New("http: no Host in request URL"), 61 | }, 62 | }, 63 | { 64 | name: "read from URL error", 65 | path: ts.URL + "/error", 66 | err: errors.New("unexpected EOF"), 67 | }, 68 | { 69 | name: "read from URL", 70 | path: ts.URL, 71 | err: nil, 72 | }, 73 | { 74 | name: "read from file", 75 | path: "./testdata/openapi3.yml", 76 | err: nil, 77 | }, 78 | { 79 | name: "empty file", 80 | path: "", 81 | err: &fs.PathError{ 82 | Op: "open", 83 | Path: "", 84 | Err: errors.New("no such file or directory"), 85 | }, 86 | }, 87 | } 88 | 89 | for _, tc := range tests { 90 | t.Run(tc.name, func(t *testing.T) { 91 | got, err := read.Read(tc.path) 92 | if err != nil { 93 | require.EqualError(t, err, tc.err.Error()) 94 | } 95 | 96 | require.IsType(t, []byte{}, got) 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/read/testdata/openapi3.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | 3 | info: 4 | title: Users dummy API 5 | version: 0.1.0 6 | 7 | paths: 8 | /users: 9 | post: 10 | requestBody: 11 | required: true 12 | content: 13 | application/json: 14 | schema: 15 | $ref: "#/components/schemas/User" 16 | responses: 17 | '201': 18 | description: '' 19 | content: 20 | application/json: 21 | schema: 22 | $ref: '#/components/schemas/User' 23 | example: 24 | id: e1afccea-5168-4735-84d4-cb96f6fb5d25 25 | firstName: Elon 26 | lastName: Musk 27 | 28 | components: 29 | schemas: 30 | User: 31 | type: object 32 | required: 33 | - id 34 | - firstName 35 | - lastName 36 | properties: 37 | id: 38 | type: string 39 | format: uuid 40 | firstName: 41 | type: string 42 | lastName: 43 | type: string 44 | -------------------------------------------------------------------------------- /internal/server/handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/neotoolkit/dummy/internal/api" 10 | "github.com/neotoolkit/dummy/internal/logger" 11 | "github.com/neotoolkit/dummy/internal/pkg/pathfmt" 12 | ) 13 | 14 | // Handlers -. 15 | type Handlers struct { 16 | API api.API 17 | Logger *logger.Logger 18 | } 19 | 20 | // NewHandlers returns a new instance of Handlers 21 | func NewHandlers(api api.API, l *logger.Logger) Handlers { 22 | return Handlers{ 23 | API: api, 24 | Logger: l, 25 | } 26 | } 27 | 28 | // Handler -. 29 | func (s *Server) Handler(w http.ResponseWriter, r *http.Request) { 30 | if setStatusCode(w, r.Header.Get("X-Set-Status-Code")) { 31 | return 32 | } 33 | 34 | w.Header().Set("Content-Type", "application/json") 35 | 36 | p := pathfmt.RemoveFragment(r.URL.Path) 37 | 38 | response, ok, err := s.Handlers.Get(p, r.Method, r.Body) 39 | if ok { 40 | if _, ok := err.(*json.SyntaxError); ok || errors.Is(err, api.ErrEmptyRequireField) || err == io.EOF { 41 | w.WriteHeader(http.StatusBadRequest) 42 | 43 | return 44 | } 45 | 46 | w.WriteHeader(response.StatusCode) 47 | resp := response.ExampleValue(r.Header.Get("X-Example")) 48 | 49 | if nil == resp { 50 | return 51 | } 52 | 53 | bytes, err := json.Marshal(resp) 54 | if err != nil { 55 | s.Logger.Errorf("serialize response error: %v", err) 56 | } 57 | 58 | if _, err := w.Write(bytes); err != nil { 59 | s.Logger.Errorf("write response error: %v", err) 60 | } 61 | 62 | return 63 | } 64 | 65 | w.WriteHeader(http.StatusNotFound) 66 | } 67 | 68 | // Get -. 69 | func (h Handlers) Get(path, method string, body io.ReadCloser) (api.Response, bool, error) { 70 | response, err := h.API.FindResponse(api.FindResponseParams{ 71 | Path: path, 72 | Method: method, 73 | Body: body, 74 | }) 75 | if err != nil { 76 | if errors.Is(err, api.ErrEmptyRequireField) { 77 | return api.Response{}, true, err 78 | } 79 | 80 | if err == io.EOF { 81 | return api.Response{}, true, err 82 | } 83 | 84 | if _, ok := err.(*json.SyntaxError); ok { 85 | return api.Response{}, true, err 86 | } 87 | 88 | return api.Response{}, false, err 89 | } 90 | 91 | return response, true, nil 92 | } 93 | 94 | func setStatusCode(w http.ResponseWriter, statusCode string) bool { 95 | switch statusCode { 96 | case "500": 97 | w.WriteHeader(http.StatusInternalServerError) 98 | 99 | return true 100 | default: 101 | return false 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /internal/server/handler_test.go: -------------------------------------------------------------------------------- 1 | package server_test 2 | 3 | import ( 4 | "github.com/neotoolkit/dummy/internal/api" 5 | "github.com/neotoolkit/dummy/internal/logger" 6 | "github.com/neotoolkit/dummy/internal/server" 7 | "github.com/stretchr/testify/require" 8 | "testing" 9 | ) 10 | 11 | func TestNewHandlers(t *testing.T) { 12 | h := server.NewHandlers(api.API{}, &logger.Logger{}) 13 | 14 | require.IsType(t, server.Handlers{}, h) 15 | } 16 | 17 | func TestNewHandlers_Get(t *testing.T) { 18 | h := server.NewHandlers(api.API{}, &logger.Logger{}) 19 | 20 | resp, ok, err := h.Get("", "", nil) 21 | 22 | require.Equal(t, api.Response{}, resp) 23 | require.Equal(t, false, ok) 24 | require.NotNil(t, err) 25 | } 26 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/neotoolkit/dummy/internal/config" 8 | "github.com/neotoolkit/dummy/internal/middleware" 9 | "github.com/neotoolkit/dummy/internal/pkg/logger" 10 | ) 11 | 12 | // Server is struct for Server 13 | type Server struct { 14 | Config config.Server 15 | Server *http.Server 16 | Logger logger.Logger 17 | Handlers Handlers 18 | } 19 | 20 | // NewServer returns a new instance of Server instance 21 | func NewServer(config config.Server, l logger.Logger, h Handlers) *Server { 22 | return &Server{ 23 | Config: config, 24 | Logger: l, 25 | Handlers: h, 26 | } 27 | } 28 | 29 | // Run -. 30 | func (s *Server) Run() error { 31 | mux := http.NewServeMux() 32 | 33 | mux.HandleFunc("/", s.Handler) 34 | 35 | handler := middleware.Logging(mux, s.Logger) 36 | 37 | s.Server = &http.Server{ 38 | Addr: ":" + s.Config.Port, 39 | Handler: handler, 40 | } 41 | 42 | s.Logger.Infof("Running mock server on %s port", s.Config.Port) 43 | 44 | err := s.Server.ListenAndServe() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (s *Server) Stop(ctx context.Context) error { 53 | return s.Server.Shutdown(ctx) 54 | } 55 | -------------------------------------------------------------------------------- /internal/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server_test 2 | 3 | import ( 4 | "github.com/neotoolkit/dummy/internal/config" 5 | "github.com/neotoolkit/dummy/internal/logger" 6 | "github.com/neotoolkit/dummy/internal/server" 7 | "github.com/stretchr/testify/require" 8 | "testing" 9 | ) 10 | 11 | func TestNewServer(t *testing.T) { 12 | s := server.NewServer(config.Server{}, &logger.Logger{}, server.Handlers{}) 13 | 14 | require.IsType(t, &server.Server{}, s) 15 | } 16 | -------------------------------------------------------------------------------- /test/dummy_test.go: -------------------------------------------------------------------------------- 1 | package test_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/lamoda/gonkey/runner" 9 | 10 | "github.com/neotoolkit/dummy/internal/config" 11 | "github.com/neotoolkit/dummy/internal/logger" 12 | "github.com/neotoolkit/dummy/internal/parse" 13 | "github.com/neotoolkit/dummy/internal/server" 14 | ) 15 | 16 | func TestDummy(t *testing.T) { 17 | api, err := parse.Parse("./testdata/openapi.yml") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | s := new(server.Server) 23 | conf := config.NewConfig() 24 | s.Config = conf.Server 25 | s.Handlers.API = api 26 | s.Logger = logger.NewLogger(conf.Logger.Level) 27 | 28 | mux := http.NewServeMux() 29 | mux.HandleFunc("/", s.Handler) 30 | newServer := httptest.NewServer(mux) 31 | 32 | runner.RunWithTesting(t, &runner.RunWithTestingParams{ 33 | Server: newServer, 34 | TestsDir: "./testdata/cases.yml", 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /test/testdata/cases.yml: -------------------------------------------------------------------------------- 1 | - name: Create user. Bad request. Empty lastName 2 | method: POST 3 | path: /users 4 | 5 | request: | 6 | { 7 | "firstName": "Elon" 8 | } 9 | 10 | response: 11 | 400: | 12 | 13 | - name: Create user. Bad request. Empty firstName 14 | method: POST 15 | path: /users 16 | 17 | request: | 18 | { 19 | "lastName": "Musk" 20 | } 21 | 22 | response: 23 | 400: | 24 | 25 | - name: Create user 26 | method: POST 27 | path: /users 28 | 29 | request: | 30 | { 31 | "firstName": "Elon", 32 | "lastName": "Musk" 33 | } 34 | 35 | response: 36 | 201: | 37 | { 38 | "id":"e1afccea-5168-4735-84d4-cb96f6fb5d25", 39 | "firstName":"Elon", 40 | "lastName":"Musk" 41 | } 42 | 43 | - name: Get users 44 | method: GET 45 | path: /users 46 | 47 | response: 48 | 200: | 49 | [ 50 | { 51 | "id": "e1afccea-5168-4735-84d4-cb96f6fb5d25", 52 | "firstName": "Elon", 53 | "lastName": "Musk" 54 | }, 55 | { 56 | "id":"472063cc-4c83-11ec-81d3-0242ac130003", 57 | "firstName":"Sergey", 58 | "lastName":"Brin" 59 | } 60 | ] 61 | 62 | - name: Get user by ID 63 | method: GET 64 | path: /users/e1afccea-5168-4735-84d4-cb96f6fb5d25 65 | 66 | response: 67 | 200: | 68 | { 69 | "id":"e1afccea-5168-4735-84d4-cb96f6fb5d25", 70 | "firstName":"Elon", 71 | "lastName":"Musk" 72 | } 73 | 74 | - name: Update user. Bad request. Empty lastName 75 | method: PUT 76 | path: /users/e1afccea-5168-4735-84d4-cb96f6fb5d25 77 | 78 | request: | 79 | { 80 | "firstName": "Elon" 81 | } 82 | 83 | response: 84 | 400: | 85 | 86 | - name: Update user. Bad request. Empty firstName 87 | method: PUT 88 | path: /users/e1afccea-5168-4735-84d4-cb96f6fb5d25 89 | 90 | request: | 91 | { 92 | "lastName": "Musk" 93 | } 94 | 95 | response: 96 | 400: | 97 | 98 | - name: Update user 99 | method: PUT 100 | path: /users/e1afccea-5168-4735-84d4-cb96f6fb5d25 101 | 102 | request: | 103 | { 104 | "firstName": "Elon", 105 | "lastName": "Musk" 106 | } 107 | 108 | response: 109 | 200: | 110 | { 111 | "id":"e1afccea-5168-4735-84d4-cb96f6fb5d25", 112 | "firstName":"Elon", 113 | "lastName":"Musk" 114 | } 115 | 116 | - name: Update user. Bad request. Empty lastName 117 | method: PATCH 118 | path: /users/e1afccea-5168-4735-84d4-cb96f6fb5d25 119 | 120 | request: | 121 | { 122 | "firstName": "Elon" 123 | } 124 | 125 | response: 126 | 400: | 127 | 128 | - name: Update user. Bad request. Empty firstName 129 | method: PATCH 130 | path: /users/e1afccea-5168-4735-84d4-cb96f6fb5d25 131 | 132 | request: | 133 | { 134 | "lastName": "Musk" 135 | } 136 | 137 | response: 138 | 400: | 139 | 140 | - name: Update user 141 | method: PATCH 142 | path: /users/e1afccea-5168-4735-84d4-cb96f6fb5d25 143 | 144 | request: | 145 | { 146 | "firstName": "Elon", 147 | "lastName": "Musk" 148 | } 149 | 150 | response: 151 | 200: | 152 | { 153 | "id":"e1afccea-5168-4735-84d4-cb96f6fb5d25", 154 | "firstName":"Elon", 155 | "lastName":"Musk" 156 | } 157 | 158 | - name: Delete user by ID 159 | method: DELETE 160 | path: /users/e1afccea-5168-4735-84d4-cb96f6fb5d25 161 | 162 | response: 163 | 204: | 164 | 165 | - name: Not Found 166 | method: GET 167 | path: / 168 | 169 | response: 170 | 404: | 171 | 172 | - name: Set status code 173 | method: GET 174 | path: /users 175 | headers: 176 | X-Set-Status-Code: 500 177 | 178 | response: 179 | 500: | 180 | -------------------------------------------------------------------------------- /test/testdata/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | 3 | info: 4 | title: Users dummy API 5 | version: 0.1.0 6 | 7 | paths: 8 | /users: 9 | post: 10 | requestBody: 11 | $ref: "#/components/requestBodies/UserBody" 12 | responses: 13 | '201': 14 | description: '' 15 | content: 16 | application/json: 17 | schema: 18 | $ref: '#/components/schemas/User' 19 | example: 20 | id: e1afccea-5168-4735-84d4-cb96f6fb5d25 21 | firstName: Elon 22 | lastName: Musk 23 | '500': 24 | description: '' 25 | get: 26 | responses: 27 | '200': 28 | description: '' 29 | content: 30 | application/json: 31 | schema: 32 | $ref: '#/components/schemas/Users' 33 | example: 34 | - id: e1afccea-5168-4735-84d4-cb96f6fb5d25 35 | firstName: Elon 36 | lastName: Musk 37 | - id: 472063cc-4c83-11ec-81d3-0242ac130003 38 | firstName: Sergey 39 | lastName: Brin 40 | /users/{userId}: 41 | get: 42 | parameters: 43 | - in: path 44 | name: userId 45 | description: '' 46 | required: true 47 | schema: 48 | type: string 49 | responses: 50 | '200': 51 | description: '' 52 | content: 53 | application/json: 54 | schema: 55 | $ref: '#/components/schemas/User' 56 | example: 57 | id: e1afccea-5168-4735-84d4-cb96f6fb5d25 58 | firstName: Elon 59 | lastName: Musk 60 | put: 61 | parameters: 62 | - in: path 63 | name: userId 64 | description: '' 65 | required: true 66 | schema: 67 | type: string 68 | requestBody: 69 | $ref: "#/components/requestBodies/UserBody" 70 | responses: 71 | '200': 72 | description: '' 73 | content: 74 | application/json: 75 | schema: 76 | $ref: '#/components/schemas/User' 77 | example: 78 | id: e1afccea-5168-4735-84d4-cb96f6fb5d25 79 | firstName: Elon 80 | lastName: Musk 81 | patch: 82 | parameters: 83 | - in: path 84 | name: userId 85 | description: '' 86 | required: true 87 | schema: 88 | type: string 89 | requestBody: 90 | $ref: "#/components/requestBodies/UserBody" 91 | responses: 92 | '200': 93 | description: '' 94 | content: 95 | application/json: 96 | schema: 97 | $ref: '#/components/schemas/User' 98 | example: 99 | id: e1afccea-5168-4735-84d4-cb96f6fb5d25 100 | firstName: Elon 101 | lastName: Musk 102 | delete: 103 | parameters: 104 | - in: path 105 | name: userId 106 | description: '' 107 | required: true 108 | schema: 109 | type: string 110 | responses: 111 | '204': 112 | description: '' 113 | 114 | components: 115 | schemas: 116 | User: 117 | type: object 118 | required: 119 | - id 120 | - firstName 121 | - lastName 122 | properties: 123 | id: 124 | type: string 125 | format: uuid 126 | example: e1afccea-5168-4735-84d4-cb96f6fb5d25 127 | firstName: 128 | type: string 129 | example: Elon 130 | lastName: 131 | type: string 132 | example: Musk 133 | Users: 134 | type: array 135 | items: 136 | $ref: '#/components/schemas/User' 137 | 138 | requestBodies: 139 | UserBody: 140 | content: 141 | application/json: 142 | schema: 143 | type: object 144 | required: 145 | - firstName 146 | - lastName 147 | properties: 148 | firstName: 149 | type: string 150 | lastName: 151 | type: string 152 | --------------------------------------------------------------------------------